From e7e5b6654a95044d4f0afe48cb81383086a14098 Mon Sep 17 00:00:00 2001 From: plguerradesigns Date: Tue, 10 Oct 2023 00:26:55 -0400 Subject: [PATCH] All Basic Features Implemented --- .vscode/settings.json | 6 + analysis_options.yaml | 258 +++++- lib/constants/strings.dart | 77 +- lib/main.dart | 14 +- lib/models/contact.dart | 68 +- lib/models/education.dart | 119 +-- lib/models/experience.dart | 135 +-- lib/models/generic.dart | 38 + lib/models/resume.dart | 262 ++++-- lib/pages/input_form.dart | 780 +++++++++++------- lib/pages/pdf_viewer.dart | 17 +- lib/pages/split_view.dart | 292 ++++--- lib/services/pdf_generator.dart | 596 +++++++------ lib/widgets/contact_entry.dart | 47 ++ lib/widgets/contact_form_field.dart | 47 -- lib/widgets/custom_entry.dart | 93 +++ lib/widgets/date_range_entry.dart | 48 ++ lib/widgets/education_entry.dart | 84 ++ lib/widgets/education_form_field.dart | 169 ---- lib/widgets/experience_entry.dart | 92 +++ lib/widgets/experience_form_field.dart | 176 ---- lib/widgets/flutter_spinner.dart | 5 +- lib/widgets/form_text_field.dart | 50 -- lib/widgets/frosted_container.dart | 43 + lib/widgets/generic_text_field.dart | 64 ++ lib/widgets/icon_image_selection_dialog.dart | 223 ----- ...stom_icon_button.dart => icon_picker.dart} | 19 +- lib/widgets/image_file_picker.dart | 44 + pubspec.lock | 403 ++++++--- pubspec.yaml | 65 +- web/index.html | 4 + web/manifest.json | 8 +- 32 files changed, 2426 insertions(+), 1920 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 lib/models/generic.dart create mode 100644 lib/widgets/contact_entry.dart delete mode 100644 lib/widgets/contact_form_field.dart create mode 100644 lib/widgets/custom_entry.dart create mode 100644 lib/widgets/date_range_entry.dart create mode 100644 lib/widgets/education_entry.dart delete mode 100644 lib/widgets/education_form_field.dart create mode 100644 lib/widgets/experience_entry.dart delete mode 100644 lib/widgets/experience_form_field.dart delete mode 100644 lib/widgets/form_text_field.dart create mode 100644 lib/widgets/frosted_container.dart create mode 100644 lib/widgets/generic_text_field.dart delete mode 100644 lib/widgets/icon_image_selection_dialog.dart rename lib/widgets/{custom_icon_button.dart => icon_picker.dart} (60%) create mode 100644 lib/widgets/image_file_picker.dart diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..078f798 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "lerp", + "Reorderable" + ] +} \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..37a52f1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,29 +1,239 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. +# Specify analysis options. # -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. +# For a list of lints, see: https://dart.dev/lints +# For guidelines on configuring static analysis, see: +# https://dart.dev/guides/language/analysis-options -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +analyzer: + language: + strict-casts: true + strict-raw-types: true + errors: + # allow self-reference to deprecated members (we do this because otherwise we have + # to annotate every member in every test, assert, etc, when we deprecate something) + deprecated_member_use_from_same_package: ignore + exclude: + - "bin/cache/**" + # Ignore protoc generated files + - "dev/conductor/lib/proto/*" linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + # This list is derived from the list of all available lints located at + # https://github.com/dart-lang/linter/blob/main/example/all.yaml + - always_declare_return_types + - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 + - always_specify_types + # - always_use_package_imports # we do this commonly + - annotate_overrides + # - avoid_annotating_with_dynamic # conflicts with always_specify_types + - avoid_bool_literals_in_conditional_expressions + # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 + # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 + # - avoid_classes_with_only_static_members # we do this commonly for `abstract final class`es + - avoid_double_and_int_checks + - avoid_dynamic_calls + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + # - avoid_final_parameters # incompatible with prefer_final_parameters + - avoid_function_literals_in_foreach_calls + # - avoid_implementing_value_types # see https://github.com/dart-lang/linter/issues/4558 + - avoid_init_to_null + - avoid_js_rounded_ints + # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it + - avoid_print + # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + # - avoid_types_on_closure_parameters # conflicts with always_specify_types + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + # - cascade_invocations # doesn't match the typical style of this repo + - cast_nullable_to_non_nullable + # - close_sinks # not reliable enough + - collection_methods_unrelated_type + - combinators_ordering + # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 + - conditional_uri_does_not_exist + # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + # - deprecated_member_use_from_same_package # we allow self-references to deprecated members + # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) + - directives_ordering + # - discarded_futures # too many false positives, similar to unawaited_futures + # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + # - join_return_with_assignment # not required by flutter style + - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + # - lines_longer_than_80_chars # not required by flutter style + # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 + # - matching_super_parameters # blocked on https://github.com/dart-lang/language/issues/2509 + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + # - no_runtimeType_toString # ok in tests; we enable this only in packages/ + # - no_self_assignments + # - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + # - omit_local_variable_types # opposite of always_specify_types + # - one_member_abstracts # too many false positives + - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + # - parameter_assignments # we do this commonly + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + # - prefer_asserts_with_message # not required by flutter style + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + # - prefer_constructors_over_static_methods # far too many false positives + - prefer_contains + # - prefer_double_quotes # opposite of prefer_single_quotes + # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + # - prefer_final_parameters # adds too much verbosity + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml + - recursive_getters + # - require_trailing_commas # would be nice, but requires a lot of manual work: 10,000+ code locations would need to be reformatted by hand after bulk fix is applied + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + # - sort_pub_dependencies # prevents separating pinned transitive dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + # - type_annotate_public_apis # subset of always_specify_types + - type_init_formals + - type_literal_in_constant_pattern + # - unawaited_futures # too many false positives, especially with the way AnimationController works + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + # - unnecessary_final # conflicts with prefer_final_locals + - unnecessary_getters_setters + # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 + - unnecessary_late + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + - use_colored_box + # - use_decorated_box # leads to bugs: DecoratedBox and Container are not equivalent (Container inserts extra padding) + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 + - use_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review + - valid_regexps + - void_checks diff --git a/lib/constants/strings.dart b/lib/constants/strings.dart index 36d27cd..0c2396f 100644 --- a/lib/constants/strings.dart +++ b/lib/constants/strings.dart @@ -2,43 +2,54 @@ class Strings { Strings._(); // General - static const String resumeBuilder = "Resume Builder"; - static const String poweredByFlutter = "Powered by Flutter"; - static const String flutterResumeBuilder = "Flutter Resume Builder"; + static const String resumeBuilder = 'Resume Builder'; + static const String poweredByFlutter = 'Powered by Flutter'; + static const String flutterResumeBuilder = 'Flutter Resume Builder'; + static const String flutterUrl = 'https://flutter.dev/'; // Form - static const String addContactDetails = "ADD CONTACT"; - static const String contactDetails = "Contact Details"; - static const String contact = "Contact"; - static const String experience = "Experience"; - static const String education = "Education"; - static const String addExperience = "ADD EXPERIENCE"; - static const String addEducation = "ADD EDUCATION"; - static const String skills = "Skills"; - static const String summary = "Summary"; - static const String name = "Name"; - static const String location = "Location"; - static const String institution = "Institution"; - static const String degree = "Degree"; - static const String position = "Position"; - static const String company = "Company"; - static const String jobDescription = "Job Description"; - static const String selectIconOrAddImage = "Select an icon or add an image."; - static const String imageURL = "Image URL"; - static const String or = "OR"; - static const String selectIcon = "SELECT ICON"; - static const String cancel = "CANCEL"; - static const String ok = "OK"; + static const String contactDetails = 'Contact Details'; + static const String contact = 'Contact'; + static const String experience = 'Experience'; + static const String education = 'Education'; + static const String skills = 'Skills'; + static const String summary = 'Summary'; + static const String name = 'Name'; + static const String location = 'Location'; + static const String institution = 'Institution'; + static const String degree = 'Degree'; + static const String position = 'Position'; + static const String company = 'Company'; + static const String jobDescription = 'Job Description'; + static const String selectIconOrAddImage = 'Select an icon or add an image.'; + static const String imageURL = 'Image URL'; + static const String or = 'OR'; + static const String selectIcon = 'SELECT ICON'; + static const String cancel = 'CANCEL'; + static const String delete = 'DELETE'; + static const String ok = 'OK'; static const String failedToLoadNetworkImage = - "Failed to load network image."; - static const String addImageBeforeEnabling = "Add an image before enabling."; + 'Failed to load network image.'; + static const String addImageBeforeEnabling = 'Add an image before enabling.'; + static const String removeSection = 'Remove Section'; + static const String hideAllEntries = 'Hide All Entries'; + static const String showAllEntries = 'Show All Entries'; + static const String newEntry = 'New Entry'; + static const String moveSectionUp = 'Move Section Up'; + static const String moveSectionDown = 'Move Section Down'; + static String deleteSection(String sectionName) => + 'Delete $sectionName Section?'; + static const String addNewSection = 'Add New Section'; + static const String startDate = 'Start Date'; + static const String endDate = 'End Date'; + static const String title = 'Title'; + static const String subtitle = 'Subtitle'; + static const String description = 'Description'; - // Image Icon Selection Widget - static const String icon = "Icon"; - static const String image = "Image"; - static const String uploadImage = "UPLOAD IMAGE"; + static const String deleteSectionWarning = + 'Are you sure you want to delete this section?\nThis action cannot be undone.'; // Split view - static const String recompile = "RECOMPILE"; - static const String download = "DOWNLOAD"; + static const String recompile = 'RECOMPILE'; + static const String download = 'DOWNLOAD'; } diff --git a/lib/main.dart b/lib/main.dart index a201dab..0dfdf25 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,21 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:flutter_resume_builder/constants/strings.dart'; -import 'package:flutter_resume_builder/pages/split_view.dart'; +import 'constants/strings.dart'; +import 'pages/split_view.dart'; void main() { runApp(const FlutterResumeBuilder()); } class FlutterResumeBuilder extends StatelessWidget { - const FlutterResumeBuilder({Key? key}) : super(key: key); + const FlutterResumeBuilder({super.key}); - // This widget is the root of the application. @override Widget build(BuildContext context) { return MaterialApp( title: Strings.flutterResumeBuilder, - theme: ThemeData(primarySwatch: Colors.blue, brightness: Brightness.dark), - home: SplitView(), + theme: ThemeData( + brightness: Brightness.dark, + useMaterial3: true, + ), + home: const SplitView(), ); } } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 2da7ab5..3c6489c 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -1,63 +1,17 @@ import 'package:flutter/cupertino.dart'; -import 'package:pdf/widgets.dart' as pdf; -import 'package:printing/printing.dart'; -class Contact extends ChangeNotifier { - late String _details; - - late IconData _iconData; - - late pdf.ImageProvider _imageProvider; - - late String _imageURL; - - late bool _showImage; - - String get details => _details; - - IconData get iconData => _iconData; - - String get imageURL => _imageURL; - - bool get showImage => _showImage; - - pdf.ImageProvider get imageProvider => _imageProvider; - - set showImage(bool showImage) { - _showImage = showImage; - notifyListeners(); - } - - set details(String details) { - _details = details; - notifyListeners(); +/// A contact entry. +class Contact { + Contact({ + String value = '', + this.iconData = CupertinoIcons.phone, + }) { + textController.text = value; } - set iconData(IconData icon) { - _iconData = icon; - _showImage = false; - notifyListeners(); - } - - Future loadImage(String url) async { - _imageURL = url; - try { - _imageProvider = await networkImage(url); - _showImage = true; - notifyListeners(); - return true; - } catch (error) { - _imageURL = ''; - _showImage = false; - notifyListeners(); - return false; - } - } + /// The controller for the text field. + TextEditingController textController = TextEditingController(); - Contact({String? details, IconData? icon, String? imageURL}) { - this.details = details ??= ""; - iconData = icon ??= CupertinoIcons.phone; - _showImage = false; - loadImage(imageURL ??= ''); - } + /// The icon to display for this contact. + IconData iconData = CupertinoIcons.phone; } diff --git a/lib/models/education.dart b/lib/models/education.dart index 659b0ed..9c083ad 100644 --- a/lib/models/education.dart +++ b/lib/models/education.dart @@ -1,94 +1,33 @@ import 'package:flutter/cupertino.dart'; -import 'package:printing/printing.dart'; -import 'package:pdf/widgets.dart' as pdf; +/// A education history entry. class Education extends ChangeNotifier { - late IconData _iconData; - - late String _institution; - - late String _degree; - - late DateTime _startDate; - - late DateTime _endDate; - - late String _imageURL; - - late pdf.ImageProvider _imageProvider; - - late bool _showImage; - - String get institution => _institution; - - String get degree => _degree; - - DateTime get startDate => _startDate; - - DateTime get endDate => _endDate; - - IconData get iconData => _iconData; - - String get imageURL => _imageURL; - - bool get showImage => _showImage; - - pdf.ImageProvider get imageProvider => _imageProvider; - - set showImage(bool showImage) { - _showImage = showImage; - notifyListeners(); - } - - set institution(String institution) { - _institution = institution; - notifyListeners(); - } - - set degree(String degree) { - _degree = degree; - notifyListeners(); - } - - set startDate(DateTime dateTime) { - _startDate = dateTime; - notifyListeners(); - } - - set endDate(DateTime dateTime) { - _endDate = dateTime; - notifyListeners(); - } - - set iconData(IconData icon) { - _iconData = icon; - _showImage = false; - notifyListeners(); - } - - loadImage(String url) async { - _imageURL = url; - try { - _imageProvider = await networkImage(url); - _showImage = true; - notifyListeners(); - return true; - } catch (error) { - _imageURL = ''; - _showImage = false; - notifyListeners(); - return false; - } - } - - Education({institution, degree, startDate, endDate, icon, imageURL}) { - this.institution = institution ??= ""; - this.degree = degree ??= ""; - this.startDate = - startDate ??= DateTime.now().subtract(const Duration(days: 365 * 4)); - this.endDate = endDate ??= DateTime.now(); - iconData = icon ??= CupertinoIcons.book; - _showImage = false; - loadImage(imageURL ??= ''); - } + Education({ + String institution = '', + String degree = '', + String startDate = '', + String endDate = '', + String location = '', + }) { + institutionController.text = institution; + degreeController.text = degree; + startDateController.text = startDate; + endDateController.text = endDate; + locationController.text = location; + } + + /// The controller for the institution field. + TextEditingController institutionController = TextEditingController(); + + /// The controller for the degree field. + TextEditingController degreeController = TextEditingController(); + + /// The controller for the start date field. + TextEditingController startDateController = TextEditingController(); + + /// The controller for the end date field. + TextEditingController endDateController = TextEditingController(); + + /// The controller for the location field. + TextEditingController locationController = TextEditingController(); } diff --git a/lib/models/experience.dart b/lib/models/experience.dart index 1dc11f6..196234b 100644 --- a/lib/models/experience.dart +++ b/lib/models/experience.dart @@ -1,105 +1,38 @@ import 'package:flutter/cupertino.dart'; -import 'package:pdf/widgets.dart' as pdf; -import 'package:printing/printing.dart'; +/// A professional experience entry. class Experience extends ChangeNotifier { - late String _company; - - late String _position; - - late String _description; - - late DateTime _startDate; - - late DateTime _endDate; - - late IconData _iconData; - - late String _imageURL; - - late pdf.ImageProvider _imageProvider; - - late bool _showImage; - - String get company => _company; - - String get position => _position; - - String get description => _description; - - String get imageURL => _imageURL; - - bool get showImage => _showImage; - - DateTime get startDate => _startDate; - - DateTime get endDate => _endDate; - - IconData get iconData => _iconData; - - pdf.ImageProvider get imageProvider => _imageProvider; - - set showImage(bool showImage) { - _showImage = showImage; - notifyListeners(); - } - - set company(String company) { - _company = company; - notifyListeners(); - } - - set position(String position) { - _position = position; - notifyListeners(); - } - - set description(String description) { - _description = description; - notifyListeners(); - } - - set startDate(DateTime dateTime) { - _startDate = dateTime; - notifyListeners(); - } - - set endDate(DateTime dateTime) { - _endDate = dateTime; - notifyListeners(); - } - - set iconData(IconData icon) { - _iconData = icon; - _showImage = false; - notifyListeners(); - } - - loadImage(String url) async { - _imageURL = url; - try { - _imageProvider = await networkImage(url); - _showImage = true; - notifyListeners(); - return true; - } catch (error) { - _imageURL = ''; - _showImage = false; - notifyListeners(); - return false; - } - } - - Experience( - {company, position, description, startDate, endDate, icon, imageURL}) { - this.company = company ??= ''; - this.position = position ??= ''; - this.description = description ??= ''; - this.startDate = - startDate ??= DateTime.now().subtract(const Duration(days: 180)); - this.endDate = endDate ??= DateTime.now(); - iconData = icon ??= CupertinoIcons.building_2_fill; - _showImage = false; - loadImage(imageURL ??= ''); - } + Experience({ + String company = '', + String position = '', + String startDate = '', + String endDate = '', + String location = '', + String description = '', + }) { + companyController.text = company; + positionController.text = position; + startDateController.text = startDate; + endDateController.text = endDate; + locationController.text = location; + descriptionController.text = description; + } + + /// The controller for the company field. + TextEditingController companyController = TextEditingController(); + + /// The controller for the position field. + TextEditingController positionController = TextEditingController(); + + /// The controller for the start date field. + TextEditingController startDateController = TextEditingController(); + + /// The controller for the end date field. + TextEditingController endDateController = TextEditingController(); + + /// The controller for the location field. + TextEditingController locationController = TextEditingController(); + + /// The controller for the description field. + TextEditingController descriptionController = TextEditingController(); } diff --git a/lib/models/generic.dart b/lib/models/generic.dart new file mode 100644 index 0000000..0dab484 --- /dev/null +++ b/lib/models/generic.dart @@ -0,0 +1,38 @@ +import 'package:flutter/cupertino.dart'; + +/// A generic entry that can be used for custom sections. +class GenericEntry extends ChangeNotifier { + GenericEntry({ + String title = '', + String subtitle = '', + String startDate = '', + String endDate = '', + String location = '', + String description = '', + }) { + titleController.text = title; + subtitleController.text = subtitle; + startDateController.text = startDate; + endDateController.text = endDate; + locationController.text = location; + descriptionController.text = description; + } + + /// The controller for the title field. + TextEditingController titleController = TextEditingController(); + + /// The controller for the subtitle field. + TextEditingController subtitleController = TextEditingController(); + + /// The controller for the start date field. + TextEditingController startDateController = TextEditingController(); + + /// The controller for the end date field. + TextEditingController endDateController = TextEditingController(); + + /// The controller for the description field. + TextEditingController descriptionController = TextEditingController(); + + /// The controller for the location field. + TextEditingController locationController = TextEditingController(); +} diff --git a/lib/models/resume.dart b/lib/models/resume.dart index 0054d96..1c5c939 100644 --- a/lib/models/resume.dart +++ b/lib/models/resume.dart @@ -1,146 +1,226 @@ -import 'dart:collection'; +import 'dart:typed_data'; import 'package:flutter/cupertino.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:flutter_resume_builder/models/contact.dart'; -import 'package:flutter_resume_builder/models/education.dart'; -import 'package:flutter_resume_builder/models/experience.dart'; +import '../constants/strings.dart'; +import 'contact.dart'; +import 'education.dart'; +import 'experience.dart'; +import 'generic.dart'; +/// The resume being edited. class Resume extends ChangeNotifier { - final formKey = GlobalKey(); + Resume() { + nameController.text = ''; + locationController.text = ''; + contactList = [Contact(), Contact(), Contact(), Contact()]; + experiences = [ + Experience(), + Experience(), + ]; + educationHistory = [ + Education(), + Education(), + ]; + customSections = >[ + { + 'Projects': GenericEntry(), + }, + ]; + sectionOrder = [ + Strings.skills, + Strings.experience, + 'Projects', + Strings.education, + ]; + } - late String _name; + /// The form key for the resume. + final GlobalKey formKey = GlobalKey(); - late String _location; + /// The controller for the name field. + TextEditingController nameController = TextEditingController(); - late String _summary; + /// The controller for the location field. + TextEditingController locationController = TextEditingController(); - late List _contactList = []; + /// The list of contacts. + List contactList = []; - late List _experienceList = []; + /// The list of professional experiences. + List experiences = []; - late List _educationList = []; + /// The educational history. + List educationHistory = []; - late List _skillList = []; + /// The list of skills. + List skillTextControllers = []; - bool _showRecompileButton = true; + /// The list of custom (user-defined) sections. + List> customSections = + >[]; - Resume() { - name = ''; - location = ''; - summary = ''; - _contactList = [Contact(), Contact()]; - _experienceList = [Experience()]; - _educationList = [Education(), Education()]; - } + /// The order of the sections. + List sectionOrder = []; - String get name => _name; + /// The logo as bytes. + Uint8List? logoAsBytes; - bool get showRecompileButton => _showRecompileButton; + /// The list of hidden sections. + final List _hiddenSections = []; - String get location => _location; + /// Whether the section is visible. + bool sectionVisible(String sectionName) { + return !_hiddenSections.contains(sectionName); + } - String get summary => _summary; + /// Add a new professional experience entry. + void addExperience() { + experiences.add(Experience()); + notifyListeners(); + } - UnmodifiableListView get experienceList => - UnmodifiableListView(_experienceList); + /// Add a new education entry. + void addEducation() { + educationHistory.add(Education()); + notifyListeners(); + } - UnmodifiableListView get educationList => - UnmodifiableListView(_educationList); + /// Create a new custom section. + void addCustomSection() { + customSections.add({ + 'Title ${customSections.length + 1}': GenericEntry(), + }); + sectionOrder.add('Title ${customSections.length}'); + notifyListeners(); + } - UnmodifiableListView get contactList => - UnmodifiableListView(_contactList); + /// Toggle the visibility of a section. + void toggleSectionVisibility(String sectionName) { + if (_hiddenSections.contains(sectionName)) { + _hiddenSections.remove(sectionName); + } else { + _hiddenSections.add(sectionName); + } + notifyListeners(); + } - UnmodifiableListView get skillList => - UnmodifiableListView(_skillList); + /// Rename a custom section. + void renameCustomSection(String oldName, String newName) { + final int index = sectionOrder.indexOf(oldName); + sectionOrder.removeAt(index); + sectionOrder.insert(index, newName); + for (final Map element in customSections) { + if (element.containsKey(oldName)) { + final Map newMap = { + newName: element[oldName]! + }; + element.remove(oldName); + element.addAll(newMap); + } + } + notifyListeners(); + } - set contactList(List contactList) { - _contactList = contactList; - attachContactInfoListListeners(); + /// Add a logo to the resume. + void addLogo(Uint8List logoAsBytes) { + this.logoAsBytes = logoAsBytes; + notifyListeners(); } - set experienceList(List experienceList) { - _experienceList = experienceList; - attachExperienceListListeners(); + /// Whether the section can be moved up. + bool moveUpAllowed(String sectionName) { + return sectionOrder.indexOf(sectionName) > 0; } - set educationList(List educationList) { - _educationList = educationList; - attachEducationListListeners(); + /// Whether the section can be moved down. + bool moveDownAllowed(String sectionName) { + return sectionOrder.indexOf(sectionName) < sectionOrder.length - 1; } - set skillList(List skillList) { - _skillList = skillList; - notifyListeners(); + /// Whether the section can be removed. (Custom sections only) + bool removeAllowed(String sectionName) { + return customSections + .where((Map element) => + element.containsKey(sectionName)) + .isNotEmpty; } - set name(String name) { - _name = name; + /// Move the section up. + void moveUp(String sectionName) { + final int index = sectionOrder.indexOf(sectionName); + if (index <= 0) { + return; + } + sectionOrder.removeAt(index); + sectionOrder.insert(index - 1, sectionName); notifyListeners(); } - set summary(String summary) { - _summary = summary; + /// Move the section down. + void moveDown(String sectionName) { + final int index = sectionOrder.indexOf(sectionName); + if (index >= sectionOrder.length - 1) { + return; + } + sectionOrder.removeAt(index); + sectionOrder.insert(index + 1, sectionName); notifyListeners(); } - set showRecompileButton(bool value) { - _showRecompileButton = value; + /// Rearrange the contact info list. + void onReorderContactInfoList(int oldIndex, int newIndex) { + final Contact item = contactList.removeAt(oldIndex); + contactList.insert(newIndex, item); notifyListeners(); } - set location(String location) { - _location = location; + /// Rearrange the skills list. + void onReorderSkillsList(int oldIndex, int newIndex) { + final String item = skillTextControllers.removeAt(oldIndex).text; + skillTextControllers.insert(newIndex, TextEditingController(text: item)); notifyListeners(); } - void addContact() { - if (_contactList.length < 4) { - _contactList.add(Contact()); - notifyListeners(); + /// Rearrange the experience list. + void onReorderExperienceList(int oldIndex, int newIndex) { + newIndex = newIndex - 1; + if (newIndex < 0) { + newIndex = 0; } + final Experience item = experiences.removeAt(oldIndex); + experiences.insert(newIndex, item); + notifyListeners(); } - void addExperience() { - if (_experienceList.length < 4) { - _experienceList.add(Experience()); - notifyListeners(); + /// Rearrange the education list. + void onReorderEducationList(int oldIndex, int newIndex) { + newIndex = newIndex - 1; + if (newIndex < 0) { + newIndex = 0; } + final Education item = educationHistory.removeAt(oldIndex); + educationHistory.insert(newIndex, item); + notifyListeners(); } - void addEducation() { - if (_educationList.length < 4) { - _educationList.add(Education()); - notifyListeners(); - } + /// Rearrange a custom section list. + void onReorderCustomSectionList(int oldIndex, int newIndex) { + final Map item = customSections.removeAt(oldIndex); + customSections.insert(newIndex, item); + notifyListeners(); } - void attachContactInfoListListeners() { - for (var element in contactList) { - if (!element.hasListeners) { - element.addListener(() { - notifyListeners(); - }); - } - } - } + /// Delete a custom section. + void onDeleteCustomSection(String sectionName) { + sectionOrder.remove(sectionName); + customSections.removeWhere((Map element) => + element.containsKey(sectionName)); - void attachExperienceListListeners() { - for (var element in experienceList) { - if (!element.hasListeners) { - element.addListener(() { - notifyListeners(); - }); - } - } + notifyListeners(); } - void attachEducationListListeners() { - for (var element in educationList) { - if (!element.hasListeners) { - element.addListener(() { - notifyListeners(); - }); - } - } + /// Rebuild the resume/UI. + void rebuild() { + notifyListeners(); } } diff --git a/lib/pages/input_form.dart b/lib/pages/input_form.dart index 11b3f27..1891211 100644 --- a/lib/pages/input_form.dart +++ b/lib/pages/input_form.dart @@ -1,333 +1,537 @@ +import 'dart:ui'; + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_iconpicker/flutter_iconpicker.dart'; -import 'package:flutter_resume_builder/constants/strings.dart'; -import 'package:flutter_resume_builder/models/resume.dart'; -import 'package:flutter_resume_builder/widgets/contact_form_field.dart'; -import 'package:flutter_resume_builder/widgets/education_form_field.dart'; -import 'package:flutter_resume_builder/widgets/experience_form_field.dart'; -import 'package:flutter_resume_builder/widgets/icon_image_selection_dialog.dart'; +import 'package:flutter_reorderable_grid_view/entities/order_update_entity.dart'; +import 'package:flutter_reorderable_grid_view/widgets/widgets.dart'; +import 'package:provider/provider.dart'; -import '../widgets/form_text_field.dart'; +import '../constants/strings.dart'; +import '../models/education.dart'; +import '../models/experience.dart'; +import '../models/generic.dart'; +import '../models/resume.dart'; +import '../widgets/contact_entry.dart'; +import '../widgets/custom_entry.dart'; +import '../widgets/education_entry.dart'; +import '../widgets/experience_entry.dart'; +import '../widgets/frosted_container.dart'; +import '../widgets/generic_text_field.dart'; +import '../widgets/image_file_picker.dart'; +/// The input form for the resume. class InputForm extends StatefulWidget { - const InputForm({Key? key, required this.resume}) : super(key: key); - - final Resume resume; + const InputForm({super.key}); @override State createState() => _InputFormState(); } class _InputFormState extends State { - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(20), - child: FormBuilder( - key: widget.resume.formKey, + /// The key for the contact section. + final GlobalKey> _contactSectionKey = GlobalKey(); + + /// The key for the skill section. + final GlobalKey> _skillSectionKey = GlobalKey(); + + /// Form fields for requesting the user's name, location, and a logo. + Widget _header(Resume resume) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(4.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - Strings.contactDetails, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - FormTextField( + children: [ + GenericTextField( label: Strings.name, - name: Strings.name, - initialValue: widget.resume.name, - padding: const EdgeInsets.symmetric(vertical: 5), - onSubmitted: (value) { - widget.resume.name = value.toString(); - }, - ), - FormTextField( - label: Strings.location, - name: Strings.location, - initialValue: widget.resume.location, - padding: const EdgeInsets.symmetric(vertical: 5), - onSubmitted: (value) { - widget.resume.location = value.toString(); - }, - ), - const SizedBox(height: 10), - _getContactInfoInputList(), - const SizedBox(height: 20), - const Text( - Strings.experience, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), + roundedStyling: false, + controller: resume.nameController, + onSubmitted: (_) => resume.rebuild(), ), const SizedBox(height: 10), - _getExperienceList(), - const SizedBox(height: 10), - const Text( - Strings.education, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), + GenericTextField( + label: Strings.location, + roundedStyling: false, + controller: resume.locationController, + onSubmitted: (_) => resume.rebuild(), ), - const SizedBox(height: 10), - _getEducationList(), ], - )), - ), + ), + ), + ), + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.all(4.0), + child: ImageFilePicker( + logoFileBytes: resume.logoAsBytes, + onPressed: () async { + final FilePickerResult? result = + await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['jpg', 'png', 'jpeg'], + ); + if (result != null) { + resume.logoAsBytes = result.files.first.bytes; + resume.rebuild(); + } + }, + ), + ), + ], ); } - Widget _getContactInfoInputList() { - List contactInfoList = []; - for (int iterator = 0; - iterator < widget.resume.contactList.length; - iterator = iterator + 2) { - contactInfoList.add( - TableRow(children: [ - ContactFormField( - contact: widget.resume.contactList[iterator], - initialValue: widget.resume.contactList[iterator].details, - id: iterator, - padding: const EdgeInsets.only( - top: 5, - bottom: 5, - right: 5, + /// A section title with buttons for adding, removing, hiding, and reordering + /// the section. + Widget _sectionTitle({ + required String title, + required Resume resume, + required Function()? onAddPressed, + bool allowVisibilityToggle = false, + bool reOrderable = true, + bool titleEditable = false, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Row( + children: [ + if (titleEditable) + Expanded( + child: GenericTextField( + key: UniqueKey(), + label: '', + controller: TextEditingController(text: title), + roundedStyling: false, + onSubmitted: (String? value) { + resume.renameCustomSection(title, value.toString()); + }, + ), ), - onSubmitted: (value) { - widget.resume.contactList[iterator].details = value.toString(); - }, - onPressed: () => iconImageSelectionPopup( - iconData: widget.resume.contactList[iterator].iconData, - imageURL: widget.resume.contactList[iterator].imageURL, - showImage: widget.resume.contactList[iterator].showImage, - onCheckmarkButtonPressed: (showImage) { - widget.resume.contactList[iterator].showImage = showImage; - }, - onUploadImageButtonPressed: () {}, - onSelectIconButtonPressed: () async { - IconData? iconData = await FlutterIconPicker.showIconPicker( - context, - iconPackModes: [IconPack.cupertino]); - if (iconData != null) { - widget.resume.contactList[iterator].iconData = iconData; - } + if (!titleEditable) + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const Expanded( + flex: 2, + child: Divider( + indent: 10, + endIndent: 10, + color: Colors.white, + ), + ), + if (resume.removeAllowed(title)) + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) => + deleteSectionConfirmationDialog( + context: context, + resume: resume, + sectionName: title, + ), + ); }, - onImageURLSubmitted: (value) async { - bool successful = await widget.resume.contactList[iterator] - .loadImage(value.toString()); - // Handles snackbar async gap issue - if (!mounted) return; + tooltip: Strings.removeSection, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + iconSize: 18, + icon: const Icon(Icons.delete_outline), + ), + if (allowVisibilityToggle) + IconButton( + onPressed: () => resume.toggleSectionVisibility(title), + tooltip: resume.sectionVisible(title) + ? Strings.hideAllEntries + : Strings.showAllEntries, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + iconSize: 18, + icon: Icon( + resume.sectionVisible(title) + ? Icons.visibility + : Icons.visibility_off, + ), + ), + if (onAddPressed != null) + IconButton( + onPressed: onAddPressed, + tooltip: Strings.newEntry, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + iconSize: 18, + icon: const Icon(Icons.add), + ), + if (reOrderable) + IconButton( + onPressed: resume.moveUpAllowed(title) + ? () => resume.moveUp(title) + : null, + tooltip: Strings.moveSectionUp, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + iconSize: 18, + icon: const Icon(Icons.expand_less_outlined), + ), + if (reOrderable) + IconButton( + onPressed: resume.moveDownAllowed(title) + ? () => resume.moveDown(title) + : null, + tooltip: Strings.moveSectionDown, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + iconSize: 18, + icon: const Icon(Icons.expand_more_outlined), + ), + ], + ), + ); + } - if (!successful) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - Strings.failedToLoadNetworkImage, - style: TextStyle(color: Colors.grey[100]), - ), - backgroundColor: Colors.red[800], - )); - } - }, + /// A grid of contact fields. + Widget _contactSection(Resume resume) { + return ReorderableBuilder( + longPressDelay: const Duration(milliseconds: 250), + key: _contactSectionKey, + builder: (List children) { + return GridView.custom( + key: _contactSectionKey, + shrinkWrap: true, + childrenDelegate: SliverChildListDelegate(children), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + mainAxisExtent: 74, + crossAxisCount: 2, + mainAxisSpacing: 10, + crossAxisSpacing: 10, ), + ); + }, + children: List.generate( + resume.contactList.length, + (int index) => ContactEntry( + key: UniqueKey(), + contact: resume.contactList[index], + onTextSubmitted: (String? value) { + resume.rebuild(); + }, + onIconButtonPressed: () async { + final IconData? iconData = await FlutterIconPicker.showIconPicker( + context, + iconPackModes: [IconPack.cupertino]); + if (iconData != null) { + resume.contactList[index].iconData = iconData; + } + resume.rebuild(); + }, ), - iterator + 1 < widget.resume.contactList.length - ? Padding( - padding: const EdgeInsets.only(left: 5), - child: ContactFormField( - contact: widget.resume.contactList[iterator + 1], - initialValue: - widget.resume.contactList[iterator + 1].details, - id: iterator + 1, - padding: const EdgeInsets.symmetric(vertical: 5), - onSubmitted: (value) { - widget.resume.contactList[iterator + 1].details = - value.toString(); - }, - onPressed: () => iconImageSelectionPopup( - iconData: - widget.resume.contactList[iterator + 1].iconData, - imageURL: - widget.resume.contactList[iterator + 1].imageURL, - showImage: - widget.resume.contactList[iterator + 1].showImage, - onCheckmarkButtonPressed: (showImage) { - widget.resume.contactList[iterator + 1].showImage = - showImage; - }, - onUploadImageButtonPressed: () {}, - onSelectIconButtonPressed: () async { - IconData? iconData = - await FlutterIconPicker.showIconPicker(context, - iconPackModes: [IconPack.cupertino]); - if (iconData != null) { - widget.resume.contactList[iterator + 1].iconData = - iconData; - } - }, - onImageURLSubmitted: (value) async { - bool successful = await widget - .resume.contactList[iterator + 1] - .loadImage(value.toString()); - - // Handles snackbar async gap issue - if (!mounted) return; + ), + onReorder: (List orderUpdateEntities) { + for (final OrderUpdateEntity element in orderUpdateEntities) { + resume.onReorderContactInfoList(element.oldIndex, element.newIndex); + } + }); + } - if (!successful) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - Strings.failedToLoadNetworkImage, - style: TextStyle(color: Colors.grey[100]), - ), - backgroundColor: Colors.red[800], - )); - } - }, + /// A grid of skill fields. + Widget _skillsSection(Resume resume) { + return Column( + children: [ + _sectionTitle( + title: Strings.skills, + resume: resume, + allowVisibilityToggle: true, + onAddPressed: () { + setState(() { + resume.skillTextControllers.add(TextEditingController()); + }); + }, + ), + Opacity( + opacity: resume.sectionVisible(Strings.skills) ? 1 : 0.5, + child: ReorderableBuilder( + longPressDelay: const Duration(milliseconds: 250), + key: _skillSectionKey, + enableScrollingWhileDragging: false, + onReorder: (List orderUpdateEntities) { + for (final OrderUpdateEntity element in orderUpdateEntities) { + resume.onReorderSkillsList(element.oldIndex, element.newIndex); + } + }, + builder: (List children) { + return GridView.custom( + physics: const NeverScrollableScrollPhysics(), + key: _skillSectionKey, + shrinkWrap: true, + childrenDelegate: SliverChildListDelegate(children), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + mainAxisExtent: 74, + crossAxisCount: 5, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + ); + }, + children: List.generate( + resume.skillTextControllers.length, + (int index) => FrostedContainer( + key: UniqueKey(), + child: TextFormField( + controller: resume.skillTextControllers[index], + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), ), ), - ) - : Container(), - ]), - ); - } - return Table(children: contactInfoList); + onFieldSubmitted: (String value) { + resume.rebuild(); + }, + ), + ), + ), + ), + ), + ], + ); } - Widget _getExperienceList() { + /// A list of experience fields. + Widget _experienceSection(Resume resume) { return Column( - children: [ - for (int iterator = 0; - iterator < widget.resume.experienceList.length; - iterator++) - ExperienceFormField( - id: iterator, - experience: widget.resume.experienceList[iterator], - onIconButtonPressed: () => iconImageSelectionPopup( - iconData: widget.resume.experienceList[iterator].iconData, - imageURL: widget.resume.experienceList[iterator].imageURL, - showImage: widget.resume.experienceList[iterator].showImage, - onCheckmarkButtonPressed: (showImage) { - widget.resume.experienceList[iterator].showImage = showImage; - }, - onUploadImageButtonPressed: () {}, - onSelectIconButtonPressed: () async { - IconData? iconData = await FlutterIconPicker.showIconPicker( - context, - iconPackModes: [IconPack.cupertino]); - if (iconData != null) { - widget.resume.experienceList[iterator].iconData = iconData; - } - }, - onImageURLSubmitted: (value) async { - bool successful = await widget.resume.experienceList[iterator] - .loadImage(value.toString()); - // Handles snackbar async gap issue - if (!mounted) return; + children: [ + _sectionTitle( + title: Strings.experience, + resume: resume, + onAddPressed: () { + setState(() { + resume.experiences.add(Experience()); + }); + }, + ), + ReorderableList( + itemCount: resume.experiences.length, + shrinkWrap: true, + proxyDecorator: _proxyDecorator, + onReorder: (int oldIndex, int newIndex) { + resume.onReorderExperienceList(oldIndex, newIndex); + }, + itemBuilder: (BuildContext context, int index) { + return ReorderableDragStartListener( + key: Key('${Strings.experience}$index'), + index: index, + child: ExperienceEntry( + experience: resume.experiences[index], + onSubmitted: (_) { + resume.rebuild(); + }, + ), + ); + }, + ), + ], + ); + } - if (!successful) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - Strings.failedToLoadNetworkImage, - style: TextStyle(color: Colors.grey[100]), - ), - backgroundColor: Colors.red[800], - )); - } - }, - ), - ) + /// A list of education fields. + Widget _educationSection(Resume resume) { + return Column( + children: [ + _sectionTitle( + title: Strings.education, + resume: resume, + onAddPressed: () { + setState(() { + resume.educationHistory.add(Education()); + }); + }, + ), + ReorderableList( + itemCount: resume.educationHistory.length, + shrinkWrap: true, + proxyDecorator: _proxyDecorator, + onReorder: (int oldIndex, int newIndex) { + resume.onReorderEducationList(oldIndex, newIndex); + }, + itemBuilder: (BuildContext context, int index) { + return ReorderableDragStartListener( + key: Key('${Strings.education}$index'), + index: index, + child: EducationEntry( + education: resume.educationHistory[index], + onSubmitted: (_) { + resume.rebuild(); + }, + ), + ); + }, + ), ], ); } - Widget _getEducationList() { - return Row( - children: [ - for (int iterator = 0; - iterator < widget.resume.educationList.length; - iterator++) - EducationFormField( - education: widget.resume.educationList[iterator], - id: iterator, - padding: EdgeInsets.only(left: iterator > 0 ? 10 : 0), - onIconButtonPressed: () => iconImageSelectionPopup( - iconData: widget.resume.educationList[iterator].iconData, - imageURL: widget.resume.educationList[iterator].imageURL, - showImage: widget.resume.educationList[iterator].showImage, - onCheckmarkButtonPressed: (showImage) { - widget.resume.educationList[iterator].showImage = showImage; - }, - onUploadImageButtonPressed: () {}, - onSelectIconButtonPressed: () async { - IconData? iconData = await FlutterIconPicker.showIconPicker( - context, - iconPackModes: [IconPack.cupertino]); - if (iconData != null) { - widget.resume.educationList[iterator].iconData = iconData; - } - }, - onImageURLSubmitted: (value) async { - bool successful = await widget.resume.educationList[iterator] - .loadImage(value.toString()); - // Handles snackbar async gap issue - if (!mounted) return; + /// A list of custom fields. + Widget _customSection({required String title, required Resume resume}) { + final List genericSection = []; + for (final Map section in resume.customSections) { + if (section.containsKey(title)) { + genericSection.add(section[title]!); + } + } - if (!successful) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - Strings.failedToLoadNetworkImage, - style: TextStyle(color: Colors.grey[100]), - ), - backgroundColor: Colors.red[800], - )); - } - }, - ), - ) + return Column( + children: [ + _sectionTitle( + title: title, + resume: resume, + titleEditable: true, + allowVisibilityToggle: true, + onAddPressed: () { + setState(() { + resume.customSections.add( + { + title: GenericEntry(), + }, + ); + }); + }, + ), + Opacity( + opacity: resume.sectionVisible(title) ? 1 : 0.5, + child: ReorderableList( + itemCount: genericSection.length, + shrinkWrap: true, + proxyDecorator: _proxyDecorator, + onReorder: (int oldIndex, int newIndex) { + resume.onReorderCustomSectionList(oldIndex, newIndex); + }, + itemBuilder: (BuildContext context, int index) { + return ReorderableDragStartListener( + key: Key('$title$index'), + index: index, + child: CustomEntry( + genericSection: genericSection[index], + onSubmitted: (_) { + resume.rebuild(); + }, + ), + ); + }, + ), + ), ], ); } - iconImageSelectionPopup({ - required IconData iconData, - required String imageURL, - required bool showImage, - required Function()? onSelectIconButtonPressed, - required Function(String?) onImageURLSubmitted, - required Function(bool)? onCheckmarkButtonPressed, - required Function()? onUploadImageButtonPressed, - }) { - showDialog( - context: context, - builder: (BuildContext context) => IconImageSelectionDialog( - icon: iconData, - imageURL: imageURL, - onSelectIconButtonPressed: onSelectIconButtonPressed, - onImageURLSubmitted: (url) { - Navigator.of(context).pop(); - onImageURLSubmitted(url); - }, - onCheckmarkButtonPressed: (showImage) { - if (showImage && imageURL.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - Strings.addImageBeforeEnabling, - style: TextStyle(color: Colors.grey[100]), + /// Adds a shadow to the widget being reordered in a list. + Widget _proxyDecorator(Widget child, int index, Animation animation) { + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + final double animValue = Curves.easeInOut.transform(animation.value); + final double elevation = lerpDouble(0, 6, animValue)!; + return Transform.rotate( + angle: lerpDouble(0, -0.025, animValue)!, + child: Material( + elevation: elevation, + color: Colors.transparent, + shadowColor: Colors.black54, + child: child, + ), + ); + }, + child: child, + ); + } + + /// A confirmation dialog for deleting a custom section. + Widget deleteSectionConfirmationDialog( + {required BuildContext context, + required Resume resume, + required String sectionName}) { + return AlertDialog( + title: Text(Strings.deleteSection(sectionName)), + content: const Text( + Strings.deleteSectionWarning, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text(Strings.cancel), + ), + TextButton( + onPressed: () { + resume.onDeleteCustomSection(sectionName); + Navigator.of(context).pop(); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text( + Strings.delete, + ), + ), + ], + ); + } + + /// Returns a list of sections in the order they should be displayed. + List _orderedSections(Resume resume) { + final List sections = []; + for (final String sectionTitle in resume.sectionOrder) { + switch (sectionTitle) { + case Strings.skills: + sections.add(_skillsSection(resume)); + break; + case Strings.experience: + sections.add(_experienceSection(resume)); + break; + case Strings.education: + sections.add(_educationSection(resume)); + break; + default: + sections.add(_customSection(title: sectionTitle, resume: resume)); + } + } + return sections; + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (BuildContext context, Resume resume, Widget? child) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20), + child: FormBuilder( + key: resume.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _header(resume), + const SizedBox(height: 10), + _contactSection(resume), + ..._orderedSections(resume), + const SizedBox(height: 20), + OutlinedButton( + onPressed: resume.addCustomSection, + child: Text( + Strings.addNewSection.toUpperCase(), ), - backgroundColor: Colors.orange[800], - )); - } else { - onCheckmarkButtonPressed!(showImage); - } - }, - onUploadImageButtonPressed: onUploadImageButtonPressed, - showImage: showImage, - )); + ), + ], + )), + ), + ); + }); } } diff --git a/lib/pages/pdf_viewer.dart b/lib/pages/pdf_viewer.dart index 902fab0..930314f 100644 --- a/lib/pages/pdf_viewer.dart +++ b/lib/pages/pdf_viewer.dart @@ -1,10 +1,16 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; -import 'package:flutter_resume_builder/services/pdf_generator.dart'; -import 'package:flutter_resume_builder/widgets/flutter_spinner.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; +import '../services/pdf_generator.dart'; +import '../widgets/flutter_spinner.dart'; + +/// Displays the generated PDF. class PDFViewer extends StatefulWidget { - const PDFViewer({Key? key, required this.pdfGenerator}) : super(key: key); + const PDFViewer({super.key, required this.pdfGenerator}); + + /// The PDF generator to use. final PDFGenerator pdfGenerator; @override @@ -12,7 +18,9 @@ class PDFViewer extends StatefulWidget { } class PDFViewerState extends State { + /// The PDF generator. late PDFGenerator pdfGenerator; + @override void initState() { super.initState(); @@ -23,7 +31,6 @@ class PDFViewerState extends State { Widget build(BuildContext context) { return FutureBuilder( future: pdfGenerator.generateResumeAsPDF(), - initialData: null, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState != ConnectionState.done) { return Container( @@ -36,7 +43,7 @@ class PDFViewerState extends State { return Theme( data: Theme.of(context).copyWith(brightness: Brightness.light), child: SfPdfViewer.memory( - snapshot.data, + snapshot.data as Uint8List, pageLayoutMode: PdfPageLayoutMode.single, ), ); diff --git a/lib/pages/split_view.dart b/lib/pages/split_view.dart index a4dadb8..f4c4ca3 100644 --- a/lib/pages/split_view.dart +++ b/lib/pages/split_view.dart @@ -1,32 +1,115 @@ import 'dart:convert'; import 'dart:html' as html; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_resume_builder/constants/strings.dart'; -import 'package:flutter_resume_builder/models/contact.dart'; -import 'package:flutter_resume_builder/models/education.dart'; -import 'package:flutter_resume_builder/models/experience.dart'; -import 'package:flutter_resume_builder/models/resume.dart'; -import 'package:flutter_resume_builder/pages/input_form.dart'; -import 'package:flutter_resume_builder/pages/pdf_viewer.dart'; -import 'package:flutter_resume_builder/services/pdf_generator.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../constants/strings.dart'; +import '../models/contact.dart'; +import '../models/education.dart'; +import '../models/experience.dart'; +import '../models/generic.dart'; +import '../models/resume.dart'; +import '../services/pdf_generator.dart'; +import 'input_form.dart'; +import 'pdf_viewer.dart'; +/// Split view of the resume builder (input form and PDF viewer). class SplitView extends StatefulWidget { - const SplitView({Key? key}) : super(key: key); + const SplitView({super.key}); @override State createState() => SplitViewState(); } -class RecompileIntent extends Intent { - const RecompileIntent(); -} - class SplitViewState extends State { + /// The resume to use. final Resume _resume = Resume(); + /// The PDF generator. late PDFGenerator pdfGenerator; + + /// Populates the resume with sample data. + void _populateSampleResume() { + _resume.nameController.text = 'John Doe'; + _resume.locationController.text = 'San Francisco, CA'; + _resume.skillTextControllers = [ + TextEditingController(text: 'Flutter'), + TextEditingController(text: 'Dart'), + TextEditingController(text: 'Python'), + TextEditingController(text: 'Java'), + TextEditingController(text: 'C++'), + ]; + _resume.contactList = [ + Contact(value: 'johndoe@email.com', iconData: CupertinoIcons.mail), + Contact(value: 'linkedin.com/in/jdoe', iconData: CupertinoIcons.link), + Contact(value: '123-456-7890'), + Contact( + value: 'example.com/portfolio/jdoe', iconData: CupertinoIcons.globe), + ]; + _resume.experiences = [ + Experience( + company: 'Example Holdings Inc.', + position: 'Software Engineer', + startDate: '01/2020', + endDate: 'Present', + location: 'San Francisco, CA', + description: + '• Installed and configured software applications and tested solutions for effectiveness.\n• Worked with project managers, developers, quality assurance and customers to resolve\n technical issues.\n• Interfaced with cross-functional team of business analysts, developers and technical\n support professionals to determine comprehensive list of requirement specifications for\n new applications.', + ), + Experience( + company: 'Example Technologies', + position: 'Software Development Associate', + startDate: '01/2019', + endDate: '12/2019', + location: 'San Francisco, CA', + description: + '• Administered government-supported community development programs and promoted\n department programs and services.\n• Worked closely with clients to establish problem specifications and system designs.\n• Developed next generation integration platform for internal applications.', + ), + Experience( + position: 'Web Development Intern', + company: 'Example Appraisal Services', + startDate: '06/2018', + endDate: '08/2018', + location: 'San Francisco, CA', + description: + '• Participated with preparation of design documents for trackwork, including alignments,\n specifications, criteria details and estimates.\n• Collaborated with senior engineers on projects and offered insight.\n• Engaged in software development utilizing wide range of technological tools and\n industrial Ethernet-based protocols.', + ) + ]; + _resume.educationHistory = [ + Education( + institution: 'Example University', + degree: 'MS, Software Engineering', + startDate: '08/2018', + endDate: '12/2019', + location: 'San Francisco, CA', + ), + Education( + institution: 'Example State University', + degree: 'BS, Computer Science', + startDate: '08/2014', + endDate: '05/2018', + location: 'San Francisco, CA', + ), + ]; + _resume.customSections = >[ + { + 'Projects': GenericEntry( + title: 'Inventory Management System', + description: + '• Developed a mobile application for a local business to manage their inventory and sales.\n• Utilized Flutter and Firebase to create a cross-platform application for Android and iOS.\n• Implemented a barcode scanner to scan products and update inventory in real-time.\n• Designed a user interface to display sales and inventory data in a visually appealing manner.'), + }, + { + 'Projects': GenericEntry( + title: 'Project Management System', + description: + '• Built a web application to manage and track the progress of projects.\n• Utilized Flutter and GitHub Pages to create a web application for desktop and mobile.\n') + } + ]; + } + @override void initState() { super.initState(); @@ -36,40 +119,82 @@ class SplitViewState extends State { @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( + return ChangeNotifierProvider.value( value: _resume, child: Scaffold( appBar: AppBar( title: Row( - children: const [ - Text(Strings.resumeBuilder), - SizedBox(width: 15), - Text( - Strings.poweredByFlutter, - style: TextStyle(fontSize: 15, color: Colors.white30), + children: [ + const Text(Strings.resumeBuilder), + TextButton( + onPressed: () async { + if (await canLaunchUrl(Uri.parse(Strings.flutterUrl))) { + launchUrl( + Uri.parse(Strings.flutterUrl), + ); + } + }, + child: Text( + Strings.poweredByFlutter.toUpperCase(), + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.5), + ), + ), ), ], ), - actions: [ + actions: [ IconButton( onPressed: () async { - String docID = - "${DateTime.now().month}${DateTime.now().day}${DateTime.now().year.toString().substring(2)}-${DateTime.now().hour}${DateTime.now().minute}${DateTime.now().second}"; - final content = + final String docID = + '${DateTime.now().month}${DateTime.now().day}${DateTime.now().year.toString().substring(2)}-${DateTime.now().hour}${DateTime.now().minute}${DateTime.now().second}'; + final String content = base64Encode(await pdfGenerator.generateResumeAsPDF()); html.AnchorElement( href: - "data:application/octet-stream;charset=utf-16le;base64,$content") - ..setAttribute("download", - "${pdfGenerator.resume.name} Resume $docID.pdf") + 'data:application/octet-stream;charset=utf-16le;base64,$content') + ..setAttribute('download', + '${pdfGenerator.resume.nameController.text} Resume $docID.pdf') ..click(); }, tooltip: Strings.download, icon: const Icon(Icons.download)), + const SizedBox(width: 10), + MaterialButton( + color: Colors.green[700], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + onPressed: () { + _resume.formKey.currentState!.save(); + }, + minWidth: 25, + padding: const EdgeInsets.all(15), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.sync, + color: Colors.white, + size: 20, + ), + SizedBox(width: 5), + Text( + Strings.recompile, + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ) + ], + ), + ), + const SizedBox(width: 24), ], centerTitle: false, ), - body: Consumer(builder: (context, value, child) { + body: Consumer( + builder: (BuildContext context, Resume resume, Widget? child) { return Shortcuts( shortcuts: { LogicalKeySet( @@ -85,9 +210,9 @@ class SplitViewState extends State { ), }, child: Row( - children: [ - Flexible(child: InputForm(resume: _resume)), - Flexible(child: _pdfViewer()) + children: [ + const Flexible(child: InputForm()), + Flexible(child: PDFViewer(pdfGenerator: pdfGenerator)) ], ), ), @@ -95,106 +220,9 @@ class SplitViewState extends State { })), ); } +} - _populateSampleResume() { - _resume.name = "John Doe"; - _resume.location = 'California, USA'; - _resume.contactList = [ - Contact(details: 'johndoe@email.com', icon: CupertinoIcons.mail), - Contact(details: 'linkedin.com/in/jdoe', icon: CupertinoIcons.link), - Contact(details: '123-456-7890', icon: CupertinoIcons.phone), - Contact( - details: 'example.com/portfolio/jdoe', icon: CupertinoIcons.globe), - ]; - _resume.experienceList = [ - Experience( - company: 'Example Holdings Inc.', - position: 'Software Engineer', - startDate: DateTime.now().subtract(const Duration(days: 821)), - endDate: DateTime.now(), - imageURL: - "https://static.vecteezy.com/system/resources/previews/006/921/781/original/real-estate-logo-construction-architecture-building-logo-design-template-element-free-vector.jpg", - description: - "• Installed and configured software applications and tested solutions for effectiveness.\n• Worked with project managers, developers, quality assurance and customers to resolve\n technical issues.\n• Interfaced with cross-functional team of business analysts, developers and technical\n support professionals to determine comprehensive list of requirement specifications for\n new applications.", - ), - Experience( - company: 'Example Technologies', - position: 'Software Development Associate', - startDate: DateTime.now().subtract(const Duration(days: 1200)), - endDate: DateTime.now().subtract(const Duration(days: 821)), - imageURL: - "https://raw.githubusercontent.com/pyg-team/pyg_sphinx_theme/master/pyg_sphinx_theme/static/img/pyg_logo.png", - description: - "• Administered government-supported community development programs and promoted\n department programs and services.\n• Worked closely with clients to establish problem specifications and system designs.\n• Developed next generation integration platform for internal applications.", - ), - Experience( - position: 'Web Development Intern', - company: 'Example Appraisal Services', - startDate: DateTime.now().subtract(const Duration(days: 1350)), - endDate: DateTime.now().subtract(const Duration(days: 1200)), - imageURL: - "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSjcEUlFwbhGT4aFNs7CWkY-1PuDejQ_Zl5aQA54A1K2Y3QXGr7cQhmQ2QGkqZM6M_tF4M&usqp=CAU", - description: - "• Participated with preparation of design documents for trackwork, including alignments,\n specifications, criteria details and estimates.\n• Collaborated with senior engineers on projects and offered insight.\n• Engaged in software development utilizing wide range of technological tools and\n industrial Ethernet-based protocols.", - ) - ]; - _resume.educationList = [ - Education( - institution: 'Example University', - degree: 'MS, Software Engineering', - startDate: DateTime.now().subtract(const Duration(days: 2080)), - endDate: DateTime.now().subtract(const Duration(days: 1350)), - imageURL: - "https://media.istockphoto.com/id/1135962989/vector/university-campus-logo-with-text-space-for-your-slogan-tag-line-illustration.jpg?s=612x612&w=0&k=20&c=QRLY_rl52qazBfxNg1MvqSiQpyRhyizzIDR5waBvZKs=", - ), - Education( - institution: 'Example State University', - degree: 'BS, Computer Science', - startDate: DateTime.now().subtract(const Duration(days: 3540)), - endDate: DateTime.now().subtract(const Duration(days: 2080)), - ), - ]; - } - - Widget _pdfViewer() { - return Stack( - children: [ - PDFViewer(pdfGenerator: pdfGenerator), - _resume.showRecompileButton - ? Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.all(25.0), - child: MaterialButton( - color: Colors.green[700], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - onPressed: () { - _resume.formKey.currentState!.saveAndValidate(); - }, - minWidth: 25, - padding: const EdgeInsets.all(15), - child: Row( - mainAxisSize: MainAxisSize.min, - children: const [ - Icon( - Icons.sync, - color: Colors.white, - size: 20, - ), - SizedBox(width: 5), - Text( - Strings.recompile, - style: TextStyle( - fontSize: 16, fontWeight: FontWeight.bold), - ) - ], - ), - ), - ), - ) - : Container() - ], - ); - } +/// The intent to recompile the resume. +class RecompileIntent extends Intent { + const RecompileIntent(); } diff --git a/lib/services/pdf_generator.dart b/lib/services/pdf_generator.dart index adf97a0..156400f 100644 --- a/lib/services/pdf_generator.dart +++ b/lib/services/pdf_generator.dart @@ -1,321 +1,377 @@ -import 'package:flutter_resume_builder/constants/strings.dart'; -import 'package:flutter_resume_builder/models/education.dart'; -import 'package:flutter_resume_builder/models/experience.dart'; -import 'package:flutter_resume_builder/models/resume.dart'; +import 'dart:typed_data'; + +import 'package:flutter/cupertino.dart' as cupertino; import 'package:intl/intl.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart'; import 'package:printing/printing.dart'; -class PDFGenerator { - Resume resume; +import '../constants/strings.dart'; +import '../models/education.dart'; +import '../models/experience.dart'; +import '../models/generic.dart'; +import '../models/resume.dart'; +/// Generates a PDF from a [Resume]. +class PDFGenerator { PDFGenerator({required this.resume}); - generateResumeAsPDF() async { - final pdf = Document(); + /// The resume to be generated as PDF. + Resume resume; - pdf.addPage( - Page( - theme: ThemeData.withFont( - base: await PdfGoogleFonts.robotoRegular(), - bold: await PdfGoogleFonts.robotoBold(), - icons: await PdfGoogleFonts.cupertinoIcons(), - ), - pageFormat: PdfPageFormat.letter, - margin: const EdgeInsets.only(top: 50, left: 50, right: 50, bottom: 25), - build: (Context context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _name(), - _location(), - _contactGrid(), - // SizedBox(height: 8), - // _summary(), - SizedBox(height: 8), - _sectionLabel(Strings.experience), - SizedBox(height: 8), - _listOfExperiences(), - Spacer(), - _sectionLabel(Strings.education), - SizedBox(height: 8), - _listOfEducation(), - Spacer(), - _footer(), - ], - ); - }, + /// A label for a section. + Widget _sectionLabel(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + children: [ + Text( + text, + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + Expanded( + child: Divider( + indent: 8, + thickness: 0.5, + color: const PdfColor(0.45, 0.45, 0.45), + ), + ), + ], ), ); - return await pdf.save(); } - Widget _sectionLabel(String text) { - return Text(text, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)); + /// The resume header. + /// + /// Includes the name, location, image, and contact details. + Widget _header() { + return Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _name(), + _location(), + SizedBox(height: 8), + _contactGrid(), + ], + ), + if (resume.logoAsBytes != null) + Align( + alignment: Alignment.centerRight, + child: Image( + MemoryImage( + resume.logoAsBytes!, + ), + width: 75, + height: 75, + ), + ), + ], + ); } + /// The contact details of the user. + Widget _contactGrid() { + return Table( + children: [ + for (int iterator = 0; + iterator < resume.contactList.length; + iterator = iterator + 2) + TableRow( + children: [ + if (resume.contactList[iterator].textController.text.isNotEmpty) + Icon( + IconData(resume.contactList[iterator].iconData.codePoint), + size: 16, + color: const PdfColor(0.65, 0.65, 0.65), + ) + else + Container(width: 1), + Text( + resume.contactList[iterator].textController.text, + style: const TextStyle(fontSize: 12), + ), + if (iterator < resume.contactList.length - 1 && + resume + .contactList[iterator + 1].textController.text.isNotEmpty) + Icon( + IconData(resume.contactList[iterator + 1].iconData.codePoint), + size: 16, + color: const PdfColor(0.65, 0.65, 0.65), + ) + else + Container(), + Text( + iterator < resume.contactList.length - 1 + ? resume.contactList[iterator + 1].textController.text + : '', + style: const TextStyle(fontSize: 12), + ), + ], + ) + ], + ); + } + + /// The name of the user. Widget _name() { return Text( - resume.name, - style: const TextStyle(fontSize: 28), + resume.nameController.text, + style: const TextStyle(fontSize: 18), ); } + /// The location of the user. Widget _location() { - return Text( - resume.location, - style: const TextStyle(fontSize: 12), + return Row( + children: [ + Icon( + IconData(cupertino.CupertinoIcons.map_pin_ellipse.codePoint), + size: 14, + color: const PdfColor(0.65, 0.65, 0.65), + ), + SizedBox(width: 4), + Text( + resume.locationController.text, + style: const TextStyle(fontSize: 12), + ), + ], ); } - Widget _summary() { - return resume.summary.isEmpty + /// A date range in the format of `startDate - endDate`. + String _dateRange(String startDate, String endDate) { + if (startDate.isEmpty && endDate.isEmpty) { + return ''; + } else if (startDate.isEmpty) { + return endDate; + } else if (endDate.isEmpty) { + return startDate; + } else { + return '$startDate - $endDate'; + } + } + + /// A custom section. + Widget _customSection(int index, {required String sectionName}) { + return resume.customSections.isEmpty ? Container() : Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _sectionLabel(Strings.summary), - SizedBox(height: 8), - Text( - resume.summary, - style: const TextStyle(fontSize: 12), - ), + children: [ + _sectionLabel(sectionName), + for (final Map genericSection + in resume.customSections) + if (genericSection.keys.first == sectionName) + _genericEntryDetails(genericSection.values.first) ], ); } - Widget _contactGrid() { - return Column( - children: [ - SizedBox(height: 10), - Table( - children: [ - for (int iterator = 0; - iterator < resume.contactList.length; - iterator = iterator + 2) - TableRow( - verticalAlignment: TableCellVerticalAlignment.middle, - children: [ - resume.contactList[iterator].details.isEmpty - ? Container() - : resume.contactList[iterator].showImage - ? Image(resume.contactList[iterator].imageProvider, - width: 22, height: 22) - : Icon( - IconData(resume - .contactList[iterator].iconData.codePoint), - size: 22, - color: const PdfColor(0.65, 0.65, 0.65), - ), - Text( - resume.contactList[iterator].details, - style: const TextStyle(fontSize: 12), - ), - iterator < resume.contactList.length - 1 && - resume.contactList[iterator + 1].details.isNotEmpty - ? resume.contactList[iterator + 1].showImage - ? Image( - resume.contactList[iterator + 1].imageProvider, - width: 22, - height: 22) - : Icon( - IconData(resume.contactList[iterator + 1].iconData - .codePoint), - size: 22, - color: const PdfColor(0.65, 0.65, 0.65), - ) - : Container(), - Text( - iterator < resume.contactList.length - 1 - ? resume.contactList[iterator + 1].details - : "", - style: const TextStyle(fontSize: 12), + /// A generic entry. + Widget _genericEntryDetails(GenericEntry genericSection) { + return genericSection.titleController.text.isEmpty && + genericSection.descriptionController.text.isEmpty + ? Container() + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!(genericSection.titleController.text.isEmpty && + genericSection.startDateController.text.isEmpty && + genericSection.endDateController.text.isEmpty)) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + genericSection.titleController.text, + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + _dateRange( + genericSection.startDateController.text, + genericSection.endDateController.text, + ), + style: const TextStyle(fontSize: 12), + ), + ], + ), + if (!(genericSection.subtitleController.text.isEmpty && + genericSection.locationController.text.isEmpty)) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + genericSection.subtitleController.text, + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + genericSection.locationController.text, + style: const TextStyle(fontSize: 12), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), + child: Text( + genericSection.descriptionController.text, + style: const TextStyle( + fontSize: 12, + color: PdfColor(0.15, 0.15, 0.15), ), - ], - ) - ], - ), - ], - ); + ), + ), + ], + ); } - Widget _listOfExperiences() { + /// The professional experience section. + Widget _experienceSection() { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - for (Experience experience in resume.experienceList) - _experienceDetails(experience) + children: [ + _sectionLabel(Strings.experience), + for (final Experience experience in resume.experiences) + _experienceEntryDetails(experience) ], ); } - Widget _experienceDetails(Experience experience) { - return experience.position.isEmpty && experience.company.isEmpty + /// An experience entry. + Widget _experienceEntryDetails(Experience experience) { + return experience.companyController.text.isEmpty && + experience.positionController.text.isEmpty && + experience.descriptionController.text.isEmpty ? Container() - : Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - experience.showImage - ? Image(experience.imageProvider, width: 22, height: 22) - : Icon( - IconData(experience.iconData.codePoint), - size: 22, - color: const PdfColor(0.65, 0.65, 0.65), + : Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + experience.positionController.text, + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + _dateRange( + experience.startDateController.text, + experience.endDateController.text, ), - SizedBox(width: 8), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - experience.position, - style: - TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, ), + ), + ], + ), + if (experience.companyController.text.isNotEmpty && + experience.locationController.text.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ Text( - experience.company, - style: const TextStyle(fontSize: 14), + experience.companyController.text, ), Text( - _positionStartAndEndDate(experience), - style: const TextStyle(fontSize: 12), + experience.locationController.text, ), - SizedBox(height: 5), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: Text( - experience.description, - style: const TextStyle( - fontSize: 12, - color: PdfColor(0.15, 0.15, 0.15), - ), - ), - ), - SizedBox(height: 15), ], ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 2, vertical: 2), + child: Text( + experience.descriptionController.text, + style: const TextStyle( + fontSize: 12, + color: PdfColor(0.15, 0.15, 0.15), + ), + ), + ), + ], ), ], ); } - String _positionStartAndEndDate(Experience experience) { - String lengthText = - "${DateFormat('MMM yyyy').format(experience.startDate)} - "; - bool present = false; - - if (experience.endDate.difference(DateTime.now()).inDays >= 0) { - lengthText += "Present ("; - present = true; - } else { - lengthText += "${DateFormat('MMM yyyy').format(experience.endDate)} ("; - } - - int years = ((experience.endDate.difference(experience.startDate).inDays / - 30.417) ~/ - 12); - if (years > 0) { - lengthText += "$years year"; - if (years > 1) { - lengthText += "s"; - } - lengthText += " "; - } - - int months = - (experience.endDate.difference(experience.startDate).inDays ~/ 30.417) - - (12 * years); - if (months > 0) { - lengthText += "$months month"; - } - if (months > 1) { - lengthText += "s"; - } - if (present) { - lengthText += "+"; - } - lengthText += ")"; - - return lengthText; - } - - Widget _listOfEducation() { - return Row( - children: [ - for (Education education in resume.educationList) - _educationDetails(education) + /// The education section. + Widget _educationSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _sectionLabel(Strings.education), + for (final Education education in resume.educationHistory) + _educationEntryDetails(education) ], ); } - Widget _educationDetails(Education education) { - return Expanded( - child: Row( - children: [ - education.showImage - ? Image(education.imageProvider, width: 24, height: 24) - : Icon( - IconData(education.iconData.codePoint), - color: const PdfColor(0.7, 0.7, 0.7), - ), - SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + /// An education entry. + Widget _educationEntryDetails(Education education) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ Text( - education.institution, - style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + education.degreeController.text, + style: TextStyle(fontWeight: FontWeight.bold), ), Text( - education.degree, - style: const TextStyle(fontSize: 14), - ), - education.endDate.difference(DateTime.now()).inDays >= 0 - ? Text( - '${DateFormat('MMM yyyy').format(education.startDate)} - Present', - style: const TextStyle(fontSize: 12), - ) - : Text( - '${DateFormat('MMM yyyy').format(education.startDate)} - ${DateFormat('MMM yyyy').format(education.endDate)}', - style: const TextStyle(fontSize: 12), - ), + _dateRange( + education.startDateController.text, + education.endDateController.text, + ), + style: TextStyle(fontWeight: FontWeight.bold), + ) ], - ) + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(education.institutionController.text), + Text(education.locationController.text), + ], + ), ], ), ); } + /// The skills section. Widget _skillsList() { - return resume.skillList.isEmpty + return resume.skillTextControllers.isEmpty ? Container() - : Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _sectionLabel(Strings.skills), - SizedBox(height: 10), - Wrap( - alignment: WrapAlignment.center, - children: [ - for (int iterator = 0; - iterator < resume.skillList.length; - iterator++) - Text( - '${resume.skillList[iterator]}${iterator + 1 < resume.skillList.length ? " • " : ""}', - style: const TextStyle(fontSize: 10), - ) - ], - ) - ]); + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _sectionLabel(Strings.skills), + Wrap( + children: [ + for (int iterator = 0; + iterator < resume.skillTextControllers.length; + iterator++) + Text( + '${resume.skillTextControllers[iterator].text}${iterator + 1 < resume.skillTextControllers.length ? " • " : ""}', + ) + ], + ) + ]); } + /// The resume footer. Widget _footer() { return Stack( - children: [ + children: [ Align( alignment: Alignment.bottomCenter, child: Text( - '${resume.name} - Page 1 / 1', + '${resume.nameController.text} - Page 1 / 1', style: const TextStyle(color: PdfColor(0.5, 0.5, 0.5), fontSize: 10), ), @@ -331,4 +387,60 @@ class PDFGenerator { ], ); } + + /// The sections of the resume in the order they should be displayed. + List _getOrderedSections() { + final List sections = []; + int sectionIndex = 0; + for (final String sectionName in resume.sectionOrder) { + if (!resume.sectionVisible(sectionName)) { + continue; + } + switch (sectionName) { + case Strings.skills: + sections.add(_skillsList()); + break; + case Strings.experience: + sections.add(_experienceSection()); + break; + case Strings.education: + sections.add(_educationSection()); + break; + default: + sections.add(_customSection(sectionIndex, sectionName: sectionName)); + break; + } + sectionIndex++; + } + return sections; + } + + /// Generates the resume as a PDF. + Future generateResumeAsPDF() async { + final Document pdf = Document(); + + pdf.addPage( + Page( + theme: ThemeData.withFont( + base: await PdfGoogleFonts.robotoRegular(), + bold: await PdfGoogleFonts.robotoBold(), + icons: await PdfGoogleFonts.cupertinoIcons(), + ), + pageFormat: PdfPageFormat.letter, + margin: const EdgeInsets.only(top: 50, left: 50, right: 50, bottom: 25), + build: (Context context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _header(), + ..._getOrderedSections(), + Spacer(), + _footer(), + ], + ); + }, + ), + ); + return pdf.save(); + } } diff --git a/lib/widgets/contact_entry.dart b/lib/widgets/contact_entry.dart new file mode 100644 index 0000000..b370223 --- /dev/null +++ b/lib/widgets/contact_entry.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import '../constants/strings.dart'; +import '../models/contact.dart'; +import 'frosted_container.dart'; +import 'generic_text_field.dart'; +import 'icon_picker.dart'; + +/// A form field for a contact entry. +class ContactEntry extends StatelessWidget { + const ContactEntry({ + super.key, + required this.contact, + required this.onTextSubmitted, + required this.onIconButtonPressed, + }); + + /// The contact to use. + final Contact contact; + + /// The callback when the user submits the text field. + final Function(String?)? onTextSubmitted; + + /// The callback when the user presses the icon button. + final Function()? onIconButtonPressed; + + @override + Widget build(BuildContext context) { + return FrostedContainer( + child: Row( + children: [ + IconPicker( + iconData: contact.iconData, + onPressed: onIconButtonPressed, + ), + const SizedBox(width: 5), + Expanded( + child: GenericTextField( + label: Strings.contact, + controller: contact.textController, + onSubmitted: onTextSubmitted, + )) + ], + ), + ); + } +} diff --git a/lib/widgets/contact_form_field.dart b/lib/widgets/contact_form_field.dart deleted file mode 100644 index 5230160..0000000 --- a/lib/widgets/contact_form_field.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_resume_builder/constants/strings.dart'; -import 'package:flutter_resume_builder/models/contact.dart'; -import 'package:flutter_resume_builder/widgets/form_text_field.dart'; -import 'package:flutter_resume_builder/widgets/custom_icon_button.dart'; - -class ContactFormField extends StatelessWidget { - const ContactFormField({ - Key? key, - required this.contact, - required this.onSubmitted, - required this.onPressed, - required this.id, - required this.padding, - this.initialValue = '', - }) : super(key: key); - - final int id; - final Contact contact; - final String initialValue; - final Function(String?)? onSubmitted; - final Function()? onPressed; - final EdgeInsets padding; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - CustomIconButton( - iconData: contact.iconData, - imageURL: contact.imageURL, - showImage: contact.showImage, - onPressed: onPressed, - ), - const SizedBox(width: 5), - Expanded( - child: FormTextField( - name: '${Strings.contact}$id', - label: Strings.contact, - initialValue: initialValue, - padding: padding, - onSubmitted: onSubmitted, - )) - ], - ); - } -} diff --git a/lib/widgets/custom_entry.dart b/lib/widgets/custom_entry.dart new file mode 100644 index 0000000..e7ad2a0 --- /dev/null +++ b/lib/widgets/custom_entry.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +import '../constants/strings.dart'; +import '../models/generic.dart'; +import 'date_range_entry.dart'; +import 'frosted_container.dart'; +import 'generic_text_field.dart'; + +/// A form field for a generic entry. +class CustomEntry extends StatefulWidget { + const CustomEntry({ + super.key, + required this.genericSection, + required this.onSubmitted, + }); + + /// The generic section to use. + final GenericEntry genericSection; + + /// The callback when the user submits the text field. + final Function(String?)? onSubmitted; + + @override + State createState() => CustomEntryState(); +} + +class CustomEntryState extends State { + /// The callback when the user submits the text field. + Function(String?)? get onSubmitted => widget.onSubmitted; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: FrostedContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: GenericTextField( + label: Strings.title, + onSubmitted: onSubmitted, + controller: widget.genericSection.titleController, + ), + ), + const SizedBox(width: 8), + Expanded( + child: DateRangeEntry( + startDateController: + widget.genericSection.startDateController, + endDateController: widget.genericSection.endDateController, + onSubmitted: onSubmitted, + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + flex: 2, + child: GenericTextField( + label: Strings.subtitle, + onSubmitted: onSubmitted, + controller: widget.genericSection.subtitleController, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GenericTextField( + label: Strings.location, + controller: widget.genericSection.locationController, + onSubmitted: onSubmitted, + ), + ), + ], + ), + const SizedBox(height: 10), + GenericTextField( + label: Strings.description, + controller: widget.genericSection.descriptionController, + multiLine: true, + onSubmitted: onSubmitted, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/date_range_entry.dart b/lib/widgets/date_range_entry.dart new file mode 100644 index 0000000..be00204 --- /dev/null +++ b/lib/widgets/date_range_entry.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import '../constants/strings.dart'; +import 'generic_text_field.dart'; + +/// A form field for a date range. +class DateRangeEntry extends StatelessWidget { + const DateRangeEntry({ + super.key, + required this.startDateController, + required this.endDateController, + required this.onSubmitted, + }); + + /// The controller for the start date. + final TextEditingController startDateController; + + /// The controller for the end date. + final TextEditingController endDateController; + + /// The callback when the user submits the text field. + final Function(String?)? onSubmitted; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: GenericTextField( + controller: startDateController, + label: Strings.startDate, + onSubmitted: onSubmitted, + ), + ), + const Padding( + padding: EdgeInsets.all(4.0), + child: Text('-'), + ), + Expanded( + child: GenericTextField( + controller: endDateController, + label: Strings.endDate, + onSubmitted: onSubmitted, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/education_entry.dart b/lib/widgets/education_entry.dart new file mode 100644 index 0000000..8efe4cb --- /dev/null +++ b/lib/widgets/education_entry.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +import '../constants/strings.dart'; +import '../models/education.dart'; +import 'date_range_entry.dart'; +import 'frosted_container.dart'; +import 'generic_text_field.dart'; + +/// A form field for an education entry. +class EducationEntry extends StatefulWidget { + const EducationEntry({ + super.key, + required this.education, + required this.onSubmitted, + }); + + /// The callback when the user submits the text field. + final Function(String?)? onSubmitted; + + /// The education to use. + final Education education; + + @override + State createState() => EducationEntryState(); +} + +class EducationEntryState extends State { + /// The callback when the user submits the text field. + Function(String?)? get onSubmitted => widget.onSubmitted; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: FrostedContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: GenericTextField( + label: Strings.institution, + controller: widget.education.institutionController, + onSubmitted: onSubmitted, + ), + ), + const SizedBox(width: 8), + Expanded( + child: DateRangeEntry( + startDateController: widget.education.startDateController, + endDateController: widget.education.endDateController, + onSubmitted: onSubmitted, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: GenericTextField( + label: Strings.degree, + controller: widget.education.degreeController, + onSubmitted: onSubmitted, + ), + ), + const SizedBox(width: 8), + Expanded( + child: GenericTextField( + label: Strings.location, + controller: widget.education.locationController, + onSubmitted: onSubmitted, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/education_form_field.dart b/lib/widgets/education_form_field.dart deleted file mode 100644 index fe5941e..0000000 --- a/lib/widgets/education_form_field.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_resume_builder/constants/strings.dart'; -import 'package:flutter_resume_builder/models/education.dart'; -import 'package:flutter_resume_builder/widgets/custom_icon_button.dart'; -import 'package:flutter_resume_builder/widgets/form_text_field.dart'; -import 'package:intl/intl.dart'; -import 'package:syncfusion_flutter_datepicker/datepicker.dart'; - -class EducationFormField extends StatefulWidget { - const EducationFormField({ - super.key, - required this.id, - required this.education, - required this.onIconButtonPressed, - required this.padding, - }); - - final int id; - final Education education; - final Function()? onIconButtonPressed; - final EdgeInsets padding; - - @override - State createState() => EducationFormFieldState(); -} - -class EducationFormFieldState extends State { - bool editingStartDate = true; - Function(Object?)? onSubmit; - - @override - Widget build(BuildContext context) { - return Expanded( - child: Padding( - padding: widget.padding, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white24), - ), - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 5), - child: CustomIconButton( - iconData: widget.education.iconData, - imageURL: widget.education.imageURL, - showImage: widget.education.showImage, - onPressed: widget.onIconButtonPressed, - ), - ), - const SizedBox(width: 5), - Expanded( - child: Column( - children: [ - FormTextField( - label: Strings.institution, - name: '${Strings.institution}${widget.id}', - initialValue: widget.education.institution, - padding: const EdgeInsets.only(bottom: 5), - onSubmitted: (value) { - widget.education.institution = value.toString(); - }, - roundedCorners: false, - ), - FormTextField( - label: Strings.degree, - name: '${Strings.degree}${widget.id}', - initialValue: widget.education.degree, - padding: const EdgeInsets.all(0), - onSubmitted: (value) { - widget.education.degree = value.toString(); - }, - roundedCorners: false, - ), - const SizedBox(height: 10), - _startAndEndDate(), - ], - ), - ), - ], - ), - ), - ), - ); - } - - Widget _startAndEndDate() { - return Column( - children: [ - Row( - children: [ - _datePickerButton( - DateFormat('MMM yyyy').format(widget.education.startDate), - editingStartDate && onSubmit != null, () { - setState(() { - editingStartDate = true; - onSubmit = (dateTime) { - widget.education.startDate = - DateTime.parse(dateTime.toString()); - onSubmit = null; - }; - }); - }), - const SizedBox( - width: 20, - child: Divider( - indent: 5, - endIndent: 5, - color: Colors.white, - )), - _datePickerButton( - DateFormat('MMM yyyy').format(widget.education.endDate), - !editingStartDate && onSubmit != null, - () { - setState(() { - editingStartDate = false; - onSubmit = (dateTime) { - widget.education.endDate = - DateTime.parse(dateTime.toString()); - onSubmit = null; - }; - }); - }, - ) - ], - ), - AnimatedSize( - duration: const Duration(milliseconds: 500), - child: onSubmit != null - ? SfDateRangePicker( - view: DateRangePickerView.decade, - showNavigationArrow: true, - showActionButtons: true, - showTodayButton: true, - maxDate: DateTime.now(), - onSubmit: onSubmit, - toggleDaySelection: false, - selectionTextStyle: const TextStyle(color: Colors.white), - headerHeight: 60, - onCancel: () { - setState(() { - onSubmit = null; - }); - }, - ) - : Container(), - ), - ], - ); - } - - Widget _datePickerButton(String text, bool selected, Function()? onPressed) { - return OutlinedButton( - style: OutlinedButton.styleFrom( - side: BorderSide( - width: selected ? 1.5 : 1, - color: selected ? Colors.blue : Colors.white38), - ), - onPressed: onPressed, - child: Text( - text, - style: TextStyle(color: selected ? Colors.blue : Colors.white70), - ), - ); - } -} diff --git a/lib/widgets/experience_entry.dart b/lib/widgets/experience_entry.dart new file mode 100644 index 0000000..07ecf9b --- /dev/null +++ b/lib/widgets/experience_entry.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import '../constants/strings.dart'; +import '../models/experience.dart'; +import 'date_range_entry.dart'; +import 'frosted_container.dart'; +import 'generic_text_field.dart'; + +/// A form field for an experience entry. +class ExperienceEntry extends StatefulWidget { + const ExperienceEntry({ + super.key, + required this.experience, + required this.onSubmitted, + }); + + /// The experience to use. + final Experience experience; + + /// The callback when the user submits the text field. + final Function(String?)? onSubmitted; + + @override + State createState() => ExperienceEntryState(); +} + +class ExperienceEntryState extends State { + /// The callback when the user submits the text field. + Function(String?)? get onSubmitted => widget.onSubmitted; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: FrostedContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: GenericTextField( + label: Strings.position, + onSubmitted: onSubmitted, + controller: widget.experience.positionController, + ), + ), + const SizedBox(width: 8), + Expanded( + child: DateRangeEntry( + startDateController: widget.experience.startDateController, + endDateController: widget.experience.endDateController, + onSubmitted: onSubmitted, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + flex: 2, + child: GenericTextField( + label: Strings.company, + onSubmitted: onSubmitted, + controller: widget.experience.companyController, + ), + ), + const SizedBox(width: 8), + Expanded( + child: GenericTextField( + label: Strings.location, + onSubmitted: onSubmitted, + controller: widget.experience.locationController, + ), + ), + ], + ), + const SizedBox(height: 16), + GenericTextField( + label: Strings.jobDescription, + controller: widget.experience.descriptionController, + multiLine: true, + onSubmitted: onSubmitted, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/experience_form_field.dart b/lib/widgets/experience_form_field.dart deleted file mode 100644 index d116e8e..0000000 --- a/lib/widgets/experience_form_field.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_resume_builder/constants/strings.dart'; -import 'package:flutter_resume_builder/models/experience.dart'; -import 'package:flutter_resume_builder/widgets/custom_icon_button.dart'; -import 'package:flutter_resume_builder/widgets/form_text_field.dart'; -import 'package:intl/intl.dart'; -import 'package:syncfusion_flutter_datepicker/datepicker.dart'; - -class ExperienceFormField extends StatefulWidget { - const ExperienceFormField({ - Key? key, - required this.id, - required this.experience, - required this.onIconButtonPressed, - }) : super(key: key); - - final int id; - final Experience experience; - final Function()? onIconButtonPressed; - - @override - State createState() => ExperienceFormFieldState(); -} - -class ExperienceFormFieldState extends State { - bool editingStartDate = true; - Function(Object?)? onSubmit; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 15), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white30), - ), - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 5), - child: CustomIconButton( - iconData: widget.experience.iconData, - imageURL: widget.experience.imageURL, - showImage: widget.experience.showImage, - onPressed: widget.onIconButtonPressed, - ), - ), - const SizedBox(width: 5), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormTextField( - label: Strings.position, - name: '${Strings.position}${widget.id}', - padding: const EdgeInsets.only(bottom: 5), - onSubmitted: (value) { - widget.experience.position = value.toString(); - }, - roundedCorners: false, - initialValue: widget.experience.position, - ), - FormTextField( - label: Strings.company, - name: '${Strings.company}${widget.id}', - padding: const EdgeInsets.all(0), - onSubmitted: (value) { - widget.experience.company = value.toString(); - }, - roundedCorners: false, - initialValue: widget.experience.company, - ), - const SizedBox(height: 10), - _startAndEndDate(), - const SizedBox(height: 10), - FormTextField( - label: Strings.jobDescription, - name: '${Strings.jobDescription}${widget.id}', - initialValue: widget.experience.description, - maxLines: 5, - padding: const EdgeInsets.only(top: 5), - onSubmitted: (value) { - widget.experience.description = value.toString(); - }, - ), - ], - )) - ], - ), - ), - ); - } - - Widget _startAndEndDate() { - return Column( - children: [ - Row( - children: [ - _datePickerButton( - DateFormat('MMM yyyy').format(widget.experience.startDate), - editingStartDate && onSubmit != null, () { - setState(() { - editingStartDate = true; - onSubmit = (dateTime) { - widget.experience.startDate = - DateTime.parse(dateTime.toString()); - onSubmit = null; - }; - }); - }), - const SizedBox( - width: 20, - child: Divider( - indent: 6, - endIndent: 6, - color: Colors.white, - )), - _datePickerButton( - DateFormat('MMM yyyy').format(widget.experience.endDate), - !editingStartDate && onSubmit != null, - () { - setState(() { - editingStartDate = false; - onSubmit = (dateTime) { - widget.experience.endDate = - DateTime.parse(dateTime.toString()); - onSubmit = null; - }; - }); - }, - ) - ], - ), - AnimatedSize( - duration: const Duration(milliseconds: 500), - child: onSubmit != null - ? SfDateRangePicker( - view: DateRangePickerView.decade, - showNavigationArrow: true, - showActionButtons: true, - showTodayButton: true, - maxDate: DateTime.now(), - onSubmit: onSubmit, - toggleDaySelection: false, - selectionTextStyle: const TextStyle(color: Colors.white), - headerHeight: 60, - onCancel: () { - setState(() { - onSubmit = null; - }); - }, - ) - : Container(), - ), - ], - ); - } - - Widget _datePickerButton(String text, bool selected, Function()? onPressed) { - return OutlinedButton( - style: OutlinedButton.styleFrom( - side: BorderSide( - width: selected ? 1.5 : 1, - color: selected ? Colors.blue : Colors.white38), - ), - onPressed: onPressed, - child: Text( - text, - style: TextStyle(color: selected ? Colors.blue : Colors.white70), - ), - ); - } -} diff --git a/lib/widgets/flutter_spinner.dart b/lib/widgets/flutter_spinner.dart index dff144a..6da49e2 100644 --- a/lib/widgets/flutter_spinner.dart +++ b/lib/widgets/flutter_spinner.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; +/// A spinner that uses the Flutter logo. class FlutterSpinner extends StatelessWidget { const FlutterSpinner({super.key}); @override Widget build(BuildContext context) { - return Opacity( + return const Opacity( opacity: 0.5, child: Stack( alignment: Alignment.center, - children: const [ + children: [ Padding( padding: EdgeInsets.only(right: 8.0), child: FlutterLogo( diff --git a/lib/widgets/form_text_field.dart b/lib/widgets/form_text_field.dart deleted file mode 100644 index 88609eb..0000000 --- a/lib/widgets/form_text_field.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; - -class FormTextField extends StatelessWidget { - const FormTextField({ - super.key, - required this.label, - required this.name, - required this.onSubmitted, - required this.padding, - this.maxLines = 1, - this.roundedCorners = true, - this.initialValue = '', - }); - - final String label; - final String name; - final int maxLines; - final bool roundedCorners; - final String initialValue; - final EdgeInsets padding; - final Function(String?)? onSubmitted; - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding, - child: FormBuilderTextField( - name: name, - maxLines: maxLines, - initialValue: initialValue, - style: const TextStyle(color: Colors.white70), - decoration: roundedCorners - ? InputDecoration( - label: Text(label), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - ), - ) - : InputDecoration( - label: Text(label), - border: const UnderlineInputBorder(), - contentPadding: const EdgeInsets.only(top: 5), - ), - onSubmitted: onSubmitted, - onSaved: onSubmitted, - ), - ); - } -} diff --git a/lib/widgets/frosted_container.dart b/lib/widgets/frosted_container.dart new file mode 100644 index 0000000..cd7a499 --- /dev/null +++ b/lib/widgets/frosted_container.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +/// A container with a frosted glass effect. +class FrostedContainer extends StatelessWidget { + const FrostedContainer({ + super.key, + required this.child, + this.borderRadiusAmount = 16, + this.padding = const EdgeInsets.all(8.0), + }); + + /// The widget to display inside the container. + final Widget child; + + /// The padding to apply to the container. + final EdgeInsets padding; + + /// The amount of border radius to apply to the container. + final double borderRadiusAmount; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(borderRadiusAmount), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), + child: Container( + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.4), + borderRadius: BorderRadius.circular(borderRadiusAmount), + ), + child: Padding( + padding: padding, + child: child, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/generic_text_field.dart b/lib/widgets/generic_text_field.dart new file mode 100644 index 0000000..ef25d4e --- /dev/null +++ b/lib/widgets/generic_text_field.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +/// A generic form text field. +class GenericTextField extends StatefulWidget { + const GenericTextField({ + super.key, + required this.label, + required this.controller, + required this.onSubmitted, + this.multiLine = false, + this.roundedStyling = true, + }); + + /// The label for the text field. + final String label; + + /// The controller for the text field. + final TextEditingController controller; + + /// Whether to use the rounded styling. + final bool roundedStyling; + + /// Whether the text field is multi-line. + final bool multiLine; + + /// The callback when the user submits the text field. + final Function(String?)? onSubmitted; + + @override + State createState() => _GenericTextFieldState(); +} + +class _GenericTextFieldState extends State { + @override + Widget build(BuildContext context) { + return FormBuilderTextField( + name: UniqueKey().toString(), + minLines: widget.multiLine ? 2 : 1, + maxLines: widget.multiLine || widget.multiLine ? 15 : 1, + controller: widget.controller, + style: !widget.roundedStyling + ? const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ) + : const TextStyle(fontSize: 14), + decoration: widget.roundedStyling + ? InputDecoration( + label: Text(widget.label), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ) + : InputDecoration( + label: Text(widget.label), + border: const UnderlineInputBorder(), + contentPadding: EdgeInsets.zero, + ), + onSubmitted: widget.onSubmitted, + onSaved: widget.onSubmitted, + ); + } +} diff --git a/lib/widgets/icon_image_selection_dialog.dart b/lib/widgets/icon_image_selection_dialog.dart deleted file mode 100644 index 12d4bbb..0000000 --- a/lib/widgets/icon_image_selection_dialog.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:flutter_resume_builder/constants/strings.dart'; -import 'package:flutter_resume_builder/widgets/form_text_field.dart'; - -class IconImageSelectionDialog extends StatelessWidget { - const IconImageSelectionDialog({ - super.key, - this.icon, - this.imageURL, - required this.showImage, - required this.onSelectIconButtonPressed, - required this.onImageURLSubmitted, - required this.onCheckmarkButtonPressed, - required this.onUploadImageButtonPressed, - }); - final bool showImage; - final IconData? icon; - final String? imageURL; - final Function()? onUploadImageButtonPressed; - final Function(bool)? onCheckmarkButtonPressed; - final Function()? onSelectIconButtonPressed; - final Function(String?)? onImageURLSubmitted; - static final formKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - title: const Text( - Strings.selectIconOrAddImage, - textAlign: TextAlign.center, - ), - content: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - card( - title: Strings.icon, - selected: !showImage, - onCheckmarkButtonPressed: onCheckmarkButtonPressed, - context: context, - image: Icon( - icon!, - size: 42, - ), - content: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - OutlinedButton( - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - side: const BorderSide(width: 1, color: Colors.white30), - ), - onPressed: () { - Navigator.of(context).pop(); - onSelectIconButtonPressed!(); - }, - child: const Padding( - padding: EdgeInsets.all(17.0), - child: Text( - Strings.selectIcon, - style: TextStyle(color: Colors.white70), - ), - ), - ) - ], - ), - ), - const Text( - Strings.or, - style: TextStyle(fontSize: 20, color: Colors.white54), - ), - card( - title: Strings.image, - selected: showImage, - onCheckmarkButtonPressed: onCheckmarkButtonPressed, - context: context, - image: imageURL!.isEmpty - ? Container( - width: 42, - height: 42, - color: Colors.white12, - child: const Icon( - Icons.upload_sharp, - color: Colors.white30, - ), - ) - : Image.network( - imageURL!, - width: 42, - height: 42, - ), - content: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormBuilder( - key: formKey, - child: FormTextField( - initialValue: imageURL ?? "", - label: Strings.imageURL, - name: Strings.imageURL, - padding: const EdgeInsets.all(0), - onSubmitted: onImageURLSubmitted, - ), - ), - Flexible( - child: Row( - children: const [ - Expanded( - child: Divider( - indent: 5, - endIndent: 5, - thickness: 0.6, - color: Colors.white60, - )), - Padding( - padding: EdgeInsets.symmetric(vertical: 4.0), - child: Text( - Strings.or, - style: TextStyle(color: Colors.white60), - ), - ), - Expanded( - child: Divider( - thickness: 0.6, - indent: 5, - endIndent: 5, - color: Colors.white60, - )), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: OutlinedButton( - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - side: const BorderSide(width: 1, color: Colors.white38), - ), - onPressed: onUploadImageButtonPressed, - child: const Padding( - padding: EdgeInsets.all(17.0), - child: Text( - Strings.uploadImage, - style: TextStyle(color: Colors.white70), - ), - ), - ), - ) - ], - ), - ), - ], - ), - ); - } - - Widget card({ - required String title, - required bool selected, - required Function(bool)? onCheckmarkButtonPressed, - required Widget image, - required Widget content, - required BuildContext context, - }) { - return Stack( - alignment: Alignment.topRight, - fit: StackFit.passthrough, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: SizedBox( - width: 300, - height: 350, - child: Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white38)), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - const SizedBox(height: 10), - Text( - title, - textAlign: TextAlign.center, - style: - const TextStyle(fontSize: 20, color: Colors.white70), - ), - const SizedBox(height: 30), - Center(child: image), - Expanded(child: content), - ], - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - icon: const Icon( - Icons.check_circle, - ), - iconSize: 38, - onPressed: () { - Navigator.of(context).pop(); - onCheckmarkButtonPressed!(title == Strings.image); - }, - color: selected ? Colors.green.withOpacity(0.75) : Colors.black12, - hoverColor: Colors.transparent, - ), - ), - ], - ); - } -} diff --git a/lib/widgets/custom_icon_button.dart b/lib/widgets/icon_picker.dart similarity index 60% rename from lib/widgets/custom_icon_button.dart rename to lib/widgets/icon_picker.dart index f3ae597..d1b8abe 100644 --- a/lib/widgets/custom_icon_button.dart +++ b/lib/widgets/icon_picker.dart @@ -1,18 +1,17 @@ import 'package:flutter/material.dart'; -class CustomIconButton extends StatelessWidget { - const CustomIconButton({ - Key? key, +/// A button that displays an icon. The icon can be changed. +class IconPicker extends StatelessWidget { + const IconPicker({ + super.key, this.iconData, - this.imageURL, required this.onPressed, - required this.showImage, - }) : super(key: key); + }); - final bool showImage; + /// The icon to display. final IconData? iconData; - final String? imageURL; + /// The callback when the user presses the button. final Function()? onPressed; @override @@ -25,10 +24,10 @@ class CustomIconButton extends StatelessWidget { borderRadius: BorderRadius.circular(10), ), child: Padding( - padding: const EdgeInsets.all(3.0), + padding: const EdgeInsets.all(6.0), child: IconButton( onPressed: onPressed, - icon: showImage ? Image.network(imageURL!) : Icon(iconData), + icon: Icon(iconData), ), ), ); diff --git a/lib/widgets/image_file_picker.dart b/lib/widgets/image_file_picker.dart new file mode 100644 index 0000000..cbe67fe --- /dev/null +++ b/lib/widgets/image_file_picker.dart @@ -0,0 +1,44 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; + +/// A button that displays an image. The image can be changed. +class ImageFilePicker extends StatelessWidget { + const ImageFilePicker({ + super.key, + required this.logoFileBytes, + required this.onPressed, + }); + + /// The image to display as a byte array. + final Uint8List? logoFileBytes; + + /// The callback when the user presses the button. + final Function()? onPressed; + + @override + Widget build(BuildContext context) { + return Container( + height: 118, + width: 118, + decoration: BoxDecoration( + border: Border.all( + color: Colors.white38, + ), + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: IconButton( + onPressed: onPressed, + icon: logoFileBytes == null + ? const Icon( + Icons.person, + size: 50, + ) + : Image.memory(logoFileBytes!), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 65f0696..180f8cf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,86 +5,146 @@ packages: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: ca12e6c9ac022f33fd89128e7007fb5e97ab6e814d4fa05dd8d4f2db1e3c69cb + url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.5" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.11.0" barcode: dependency: transitive description: name: barcode - url: "https://pub.dartlang.org" + sha256: "789f898eef0bd88312470bdb2cc996f895ad7dd5f89e9adde84b204546a90b45" + url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" + bidi: + dependency: transitive + description: + name: bidi + sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + url: "https://pub.dev" + source: hosted + version: "2.0.10" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.2" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659" + url: "https://pub.dev" + source: hosted + version: "9.0.3" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + url: "https://pub.dev" + source: hosted + version: "5.5.0" flutter: dependency: "direct main" description: flutter @@ -94,30 +154,50 @@ packages: dependency: "direct main" description: name: flutter_form_builder - url: "https://pub.dartlang.org" + sha256: "8973beed34b6d951d36bf688b52e9e3040b47b763c35c320bd6f4c2f6b13f3a2" + url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "9.1.1" flutter_iconpicker: dependency: "direct main" description: name: flutter_iconpicker - url: "https://pub.dartlang.org" + sha256: a51d1c8ed5447334652d6fe6d004f1d361184d124e982762373f9be6a78a18b6 + url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.4" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c + url: "https://pub.dev" + source: hosted + version: "2.0.16" + flutter_reorderable_grid_view: + dependency: "direct main" + description: + name: flutter_reorderable_grid_view + sha256: ac92a49a8411adfda40f75eff44d0958ee879b0f9d6776bb6154c74aa166aaf1 + url: "https://pub.dev" + source: hosted + version: "4.0.0" flutter_spinkit: dependency: "direct main" description: name: flutter_spinkit - url: "https://pub.dartlang.org" + sha256: b39c753e909d4796906c5696a14daf33639a76e017136c8d82bf3e620ce5bb8e + url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -132,133 +212,160 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - url: "https://pub.dartlang.org" + sha256: "5fb789145cae1f4c3245c58b3f8fb287d055c26323879eab57a7bf0cfd1e45f3" + url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.5.0" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" image: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.1.3" intl: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.7" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.5.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" pdf: dependency: "direct main" description: name: pdf - url: "https://pub.dartlang.org" + sha256: "9f75fc7f5580ea5e635b5724de58fb27f684c9ad03ed46fdc1aac768e4557315" + url: "https://pub.dev" source: hosted - version: "3.8.3" + version: "3.10.4" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.4.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.dev" + source: hosted + version: "2.1.6" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.7.3" printing: dependency: "direct main" description: name: printing - url: "https://pub.dartlang.org" + sha256: e7c383dca95ee7b88c02dc1c66638628d3dcdc2fb2cc47e7a595facd47e46b56 + url: "https://pub.dev" source: hosted - version: "5.9.3" + version: "5.11.0" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.0.5" qr: dependency: transitive description: name: qr - url: "https://pub.dartlang.org" + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" source: hosted version: "3.0.1" sky_engine: @@ -270,184 +377,242 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" syncfusion_flutter_core: dependency: transitive description: name: syncfusion_flutter_core - url: "https://pub.dartlang.org" + sha256: "505a07deaeb326ae9af5ee4e50937b7563cbb639982e760c26f3511b4fa535d5" + url: "https://pub.dev" source: hosted - version: "20.3.52" + version: "23.1.39" syncfusion_flutter_datepicker: dependency: "direct main" description: name: syncfusion_flutter_datepicker - url: "https://pub.dartlang.org" + sha256: a706c0ea4fbe6ce1c6a63f11d048bddd4c6fa96164bb9baf9fd45aa4f328899f + url: "https://pub.dev" source: hosted - version: "20.3.52" + version: "23.1.39" syncfusion_flutter_pdf: dependency: transitive description: name: syncfusion_flutter_pdf - url: "https://pub.dartlang.org" + sha256: df8cc9ce3efe14c8d229bcac93b16d361ac1c0e01b9700ee75fe1bdfce440f63 + url: "https://pub.dev" source: hosted - version: "20.3.48" + version: "23.1.39" syncfusion_flutter_pdfviewer: dependency: "direct main" description: name: syncfusion_flutter_pdfviewer - url: "https://pub.dartlang.org" + sha256: "202a8f39dc80b5e9f24dd59cb3979aae73fa5cd2533597d07baaf4638084eadc" + url: "https://pub.dev" source: hosted - version: "20.3.48" + version: "23.1.39" + syncfusion_flutter_signaturepad: + dependency: transitive + description: + name: syncfusion_flutter_signaturepad + sha256: "26a1d4bfd1d16fac091f1671c9426e75830545fccd46eb5a71db948f7e17bcdf" + url: "https://pub.dev" + source: hosted + version: "23.1.39" syncfusion_pdfviewer_macos: dependency: transitive description: name: syncfusion_pdfviewer_macos - url: "https://pub.dartlang.org" + sha256: "176a44ee71e07dac4ca1e662eb9d778217287a53343f65747a34254821c13fd4" + url: "https://pub.dev" source: hosted - version: "20.3.48" + version: "23.1.39" syncfusion_pdfviewer_platform_interface: dependency: transitive description: name: syncfusion_pdfviewer_platform_interface - url: "https://pub.dartlang.org" + sha256: d9fe70d59dc3d11b225291b458eb86785048b32f827723818b02133fce79db7d + url: "https://pub.dev" source: hosted - version: "20.3.48" + version: "23.1.39" syncfusion_pdfviewer_web: dependency: transitive description: name: syncfusion_pdfviewer_web - url: "https://pub.dartlang.org" + sha256: "26df36c18aa1b11d30cdbf578c5e3f51db875092c8cbe96a3d6e18bc35e5d4f9" + url: "https://pub.dev" source: hosted - version: "20.3.48" + version: "23.1.39" syncfusion_pdfviewer_windows: dependency: transitive description: name: syncfusion_pdfviewer_windows - url: "https://pub.dartlang.org" + sha256: "0d7259c645f6572f39923c9492d8349d7ffd51d263bdbc39e7b231fbda73abe7" + url: "https://pub.dev" source: hosted - version: "20.3.48" + version: "23.1.39" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.6.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + url: "https://pub.dev" source: hosted - version: "6.1.6" + version: "6.1.14" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + url: "https://pub.dev" source: hosted - version: "6.0.19" + version: "6.1.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.6" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.7" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.20" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.8" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "0.1.4-beta" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.3.0" sdks: - dart: ">=2.17.1 <3.0.0" - flutter: ">=2.10.0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 53dc6b6..b64b1f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,73 +1,36 @@ name: flutter_resume_builder -description: A new Flutter project. +description: A resume builder app made with flutter. -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.1.0+2 +version: 0.2.0+3 environment: sdk: ">=2.17.1 <3.0.0" -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - pdf: ^3.8.3 - intl: ^0.17.0 - printing: ^5.9.3 + pdf: ^3.10.4 + intl: ^0.18.1 + printing: ^5.11.0 provider: ^6.0.2 flutter_spinkit: ^5.1.0 - cupertino_icons: ^1.0.2 - flutter_iconpicker: ^3.2.1 - flutter_form_builder: ^7.2.1 + cupertino_icons: ^1.0.5 + flutter_iconpicker: ^3.2.4 + flutter_form_builder: ^9.1.1 font_awesome_flutter: ^10.1.0 - syncfusion_flutter_pdfviewer: ^20.3.48 - syncfusion_flutter_datepicker: ^20.3.52 + syncfusion_flutter_pdfviewer: ^23.1.39 + syncfusion_flutter_datepicker: ^23.1.39 + flutter_reorderable_grid_view: ^4.0.0 + file_picker: ^5.5.0 + url_launcher: ^6.1.14 dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^2.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg diff --git a/web/index.html b/web/index.html index 5d133ee..1394c62 100644 --- a/web/index.html +++ b/web/index.html @@ -54,5 +54,9 @@ }); }); + + diff --git a/web/manifest.json b/web/manifest.json index 077f2e6..cecb040 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "flutter_resume_builder", - "short_name": "flutter_resume_builder", + "name": "Flutter Resume Builder", + "short_name": "Resume Builder", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "A new Flutter project.", + "description": "A resume builder app made with Flutter.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ @@ -32,4 +32,4 @@ "purpose": "maskable" } ] -} +} \ No newline at end of file