From 7261dca3fef5b825e9160f4f21a5e92b91a0faab Mon Sep 17 00:00:00 2001 From: Ziyad Farhan <56755783+ZiyadF296@users.noreply.github.com> Date: Tue, 1 Feb 2022 18:30:55 +0300 Subject: [PATCH] Major app UI polish and performance improvements with isolates and threads management - Major performance improvements with pub service. - Faster load time and background update for pub packages. - Efficient use of cache for pub packages - Isolated fetching pub packages to a different thread - Fixes and better isolate threads management - Animated stage and coming soon labels - Performance improvements in home page load times - Polished UI in home page - New pinning projects feature - New collapsing grouped tiles in workflow and projects tab. - Collapsed project groups are persistent - Major other improvements and enhancements with load times and background checks. - UI polish for light mode buttons and containers (minor fixes for dark mode) - Staged workflows tab as beta from alpha --- installer/bin/installer.dart | 6 + installer/pubspec.yaml | 12 + lib/app/constants/constants.dart | 3 - lib/app/constants/shared_pref.dart | 7 +- .../about/sections/changelog.dart | 1 - .../about/sections/contributors.dart | 47 +- .../flutter/change_channel.dart | 10 +- .../flutter/flutter_doctor.dart | 1 - .../flutter/flutter_requirements.dart | 1 - .../dialog_templates/logs/build_logs.dart | 11 +- .../dialog_templates/other/clear_cache.dart | 8 +- .../dialog_templates/project/common/name.dart | 1 - .../project/dart/sections/template.dart | 1 - .../project/flutter/sections/pre_config.dart | 2 - .../settings/sections/discover.dart | 1 - .../settings/sections/editors.dart | 2 - .../settings/sections/github.dart | 72 ++ .../settings/sections/overview.dart | 1 - .../settings/sections/projects.dart | 1 - .../settings/sections/troubleshoot.dart | 8 +- lib/components/widgets/inputs/text_field.dart | 7 +- lib/components/widgets/ui/activity_tile.dart | 38 - lib/components/widgets/ui/coming_soon.dart | 58 +- lib/components/widgets/ui/info_widget.dart | 4 - .../widgets/ui/linear_progress_indicator.dart | 4 +- .../widgets/ui/round_container.dart | 2 +- lib/components/widgets/ui/stage_tile.dart | 83 +- lib/core/models/projects.model.dart | 4 + lib/main.dart | 14 - lib/meta/utils/app_theme.dart | 2 +- lib/meta/utils/bin/utils/projects.search.dart | 50 +- lib/meta/utils/check_new_version.dart | 8 +- lib/meta/utils/clear_old_logs.dart | 2 + lib/meta/views/dialogs/drive_error.dart | 1 - .../notifications/notification_tile.dart | 1 - lib/meta/views/dialogs/open_project.dart | 3 +- .../views/dialogs/update_fluttermatic.dart | 2 - .../setup/components/tool_installed.dart | 4 - .../setup/components/windows_controls.dart | 3 - .../views/setup/sections/install_java.dart | 2 - .../tabs/components/horizontal_axis.dart | 123 ++- lib/meta/views/tabs/home.dart | 15 +- lib/meta/views/tabs/search.dart | 4 +- .../sections/home/elements/setup_guide.dart | 13 +- .../home/version_tiles/dart_version.dart | 107 +-- .../home/version_tiles/flutter_version.dart | 110 +-- .../home/version_tiles/git_version.dart | 66 +- .../home/version_tiles/java_version.dart | 133 +++- .../home/version_tiles/studio_version.dart | 67 +- .../home/version_tiles/vsc_version.dart | 68 +- .../projects/dialogs/delete_projects.dart | 1 - .../projects/elements/project_tile.dart | 79 +- .../projects/models/projects.services.dart | 98 ++- .../tabs/sections/projects/projects.dart | 265 +++++-- .../tabs/sections/pub/elements/pub_tile.dart | 5 +- .../sections/pub/models/package_dialog.dart | 714 +++++++++--------- .../tabs/sections/pub/models/pkg_data.dart | 141 ++-- lib/meta/views/tabs/sections/pub/pub.dart | 285 ++++--- .../sections/workflows/elements/tile.dart | 174 +++-- .../workflows/models/workflows.services.dart | 8 +- .../tabs/sections/workflows/workflow.dart | 71 +- .../workflows/action_settings/deploy_web.dart | 5 - .../components/build_mode_selector.dart | 1 - .../views/workflows/runner/status/error.dart | 1 - .../workflows/runner/status/startup.dart | 1 - .../workflows/runner/status/success.dart | 1 - .../views/workflows/sections/actions.dart | 51 +- .../workflows/sections/configure_actions.dart | 38 +- lib/meta/views/workflows/sections/info.dart | 7 +- .../workflows/sections/reorder_actions.dart | 192 ++--- lib/meta/views/workflows/startup.dart | 7 +- .../views/workflows/views/confirm_delete.dart | 1 - .../workflows/views/existing_workflows.dart | 5 +- .../views/workflows/views/log_history.dart | 14 +- .../workflows/views/workflow_options.dart | 275 ++++--- scripts/bin/script.dart | 12 +- 76 files changed, 2190 insertions(+), 1456 deletions(-) create mode 100644 installer/bin/installer.dart create mode 100644 installer/pubspec.yaml delete mode 100644 lib/components/widgets/ui/activity_tile.dart diff --git a/installer/bin/installer.dart b/installer/bin/installer.dart new file mode 100644 index 00000000..e9063b53 --- /dev/null +++ b/installer/bin/installer.dart @@ -0,0 +1,6 @@ +import 'dart:io'; + +void main(List args) { + print('NOT IMPLEMENTED'); + exit(1); +} diff --git a/installer/pubspec.yaml b/installer/pubspec.yaml new file mode 100644 index 00000000..cee4cdae --- /dev/null +++ b/installer/pubspec.yaml @@ -0,0 +1,12 @@ +name: fm_installer +description: The FlutterMatic installer that helps install a new version of FlutterMatic. + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + process_run: ^0.12.3+2 + +dev_dependencies: + flutter_lints: ^1.0.4 + import_sorter: ^4.6.0 \ No newline at end of file diff --git a/lib/app/constants/constants.dart b/lib/app/constants/constants.dart index a1223648..b0fa74f9 100644 --- a/lib/app/constants/constants.dart +++ b/lib/app/constants/constants.dart @@ -9,7 +9,6 @@ import 'package:flutter/material.dart'; import 'package:process_run/shell.dart'; // 🌎 Project imports: -import 'package:fluttermatic/components/widgets/ui/activity_tile.dart'; import 'package:fluttermatic/core/models/flutter_sdk.model.dart'; import 'package:fluttermatic/core/models/fluttermatic.model.dart'; @@ -40,8 +39,6 @@ String? tagName; /// SHA for vscode String? sha; -List bgActivityTiles = []; - // OS String platform = Platform.operatingSystem; String osName = Platform.operatingSystem; diff --git a/lib/app/constants/shared_pref.dart b/lib/app/constants/shared_pref.dart index f6be7b65..97aceaaa 100644 --- a/lib/app/constants/shared_pref.dart +++ b/lib/app/constants/shared_pref.dart @@ -32,8 +32,8 @@ class SPConst { static String gitVersion = 'GIT_VERSION'; // Java Setup - static String javaVersion = 'JAVA_VERSION'; static String javaPath = 'JAVA_PATH'; + static String javaVersion = 'JAVA_VERSION'; // Android Studio Setup static String aStudioPath = 'ANDROID_STUDIO_PATH'; @@ -48,6 +48,11 @@ class SPConst { static String lastProjectsReload = 'LAST_PROJECTS_RELOAD'; static String projectRefresh = 'PROJECT_REFRESH_INTERVALS'; + // Tile Collapse Memory + static String dartProjectsCollapsed = 'DART_PROJECTS_COLLAPSED'; + static String pinnedProjectsCollapsed = 'PINNED_PROJECTS_COLLAPSED'; + static String flutterProjectsCollapsed = 'FLUTTER_PROJECTS_COLLAPSED'; + // Last Check Updates static String lastGitUpdateCheck = 'LAST_GIT_UPDATE_CHECK'; static String lastJavaUpdateCheck = 'LAST_JAVA_UPDATE_CHECK'; diff --git a/lib/components/dialog_templates/about/sections/changelog.dart b/lib/components/dialog_templates/about/sections/changelog.dart index 201cab25..4b440998 100644 --- a/lib/components/dialog_templates/about/sections/changelog.dart +++ b/lib/components/dialog_templates/about/sections/changelog.dart @@ -106,7 +106,6 @@ class _ChangelogAboutSectionState extends State { return Padding( padding: const EdgeInsets.only(bottom: 10), child: RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: MarkdownBody( data: _data[index], selectable: true, diff --git a/lib/components/dialog_templates/about/sections/contributors.dart b/lib/components/dialog_templates/about/sections/contributors.dart index 5677306a..8e59ff59 100644 --- a/lib/components/dialog_templates/about/sections/contributors.dart +++ b/lib/components/dialog_templates/about/sections/contributors.dart @@ -26,6 +26,11 @@ class ContributorsAboutSection extends StatefulWidget { } class _ContributorsAboutSectionState extends State { + static const List<_ContributorTile> _contributors = <_ContributorTile>[ + _ContributorTile('56755783'), // Ziyad + _ContributorTile('35523357'), // Minnu + ]; + @override Widget build(BuildContext context) { return TabViewTabHeadline( @@ -33,7 +38,6 @@ class _ContributorsAboutSectionState extends State { content: [ RoundContainer( width: double.infinity, - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Expanded( @@ -51,7 +55,6 @@ class _ContributorsAboutSectionState extends State { ), HSeparators.small(), RectangleButton( - color: Colors.blueGrey.withOpacity(0.4), width: 90, onPressed: () => launch('https://github.com/FlutterMatic/desktop'), @@ -63,7 +66,6 @@ class _ContributorsAboutSectionState extends State { VSeparators.xSmall(), if (_failedRequest) RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), width: double.infinity, child: Column( children: [ @@ -84,28 +86,24 @@ class _ContributorsAboutSectionState extends State { ), ) else - Column( - children: const [ - ContributorTile('56755783'), // Ziyad - ContributorTile('35523357'), // Minnu - ], - ), + Column(children: _contributors), ], ); } } -// Just trying to get the data from the github api. -class ContributorTile extends StatefulWidget { +// Will get the contributor user information from the GitHub api and return +// a widget that displays the information. +class _ContributorTile extends StatefulWidget { final String gitHubId; - const ContributorTile(this.gitHubId, {Key? key}) : super(key: key); + const _ContributorTile(this.gitHubId, {Key? key}) : super(key: key); @override _ContributorTileState createState() => _ContributorTileState(); } -class _ContributorTileState extends State { +class _ContributorTileState extends State<_ContributorTile> { // Utils bool _loading = true; bool _failed = false; @@ -125,6 +123,7 @@ class _ContributorTileState extends State { Uri.parse('https://api.github.com/user/${widget.gitHubId}'), headers: _header, ); + if (_result.statusCode == 200 && mounted) { dynamic _responseJSON = json.decode(_result.body); setState(() { @@ -159,12 +158,15 @@ class _ContributorTileState extends State { child: RectangleButton( height: 65, width: double.infinity, - color: Colors.blueGrey.withOpacity(0.2), + color: Colors.blueGrey.withOpacity(0.1), padding: const EdgeInsets.symmetric(horizontal: 10), onPressed: () => launch('https://www.github.com/$_userId'), - child: _loading - ? const Spinner(size: 15, thickness: 2) - : Row( + child: Builder( + builder: (_) { + if (_loading) { + return const Spinner(size: 15, thickness: 2); + } else { + return Row( children: [ CircleAvatar( backgroundImage: NetworkImage(_profileURL, scale: 5), @@ -183,13 +185,12 @@ class _ContributorTileState extends State { ], ), ), - Icon( - Icons.arrow_forward_ios_rounded, - color: Colors.blueGrey.withOpacity(0.4), - size: 18, - ), + const Icon(Icons.arrow_forward_ios_rounded, size: 18), ], - ), + ); + } + }, + ), ), ); } diff --git a/lib/components/dialog_templates/flutter/change_channel.dart b/lib/components/dialog_templates/flutter/change_channel.dart index dea0ea3f..741a0e03 100644 --- a/lib/components/dialog_templates/flutter/change_channel.dart +++ b/lib/components/dialog_templates/flutter/change_channel.dart @@ -166,9 +166,13 @@ class _ChangeFlutterChannelDialogState options: const ['Master', 'Stable', 'Beta', 'Dev'], ), VSeparators.small(), - informationWidget( - 'We recommend staying on the stable channel for best development experience unless it\'s necessary.', - type: InformationType.warning, + AnimatedOpacity( + opacity: _initializing ? 0.1 : 1, + duration: const Duration(milliseconds: 300), + child: informationWidget( + 'We recommend staying on the stable channel for best development experience unless it\'s necessary.', + type: InformationType.warning, + ), ), VSeparators.small(), if (_switching) diff --git a/lib/components/dialog_templates/flutter/flutter_doctor.dart b/lib/components/dialog_templates/flutter/flutter_doctor.dart index 1c72022e..9ecb9db1 100644 --- a/lib/components/dialog_templates/flutter/flutter_doctor.dart +++ b/lib/components/dialog_templates/flutter/flutter_doctor.dart @@ -94,7 +94,6 @@ class _FlutterDoctorDialogState extends State { ), if (_done) ...[ RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: _status.map((String e) { diff --git a/lib/components/dialog_templates/flutter/flutter_requirements.dart b/lib/components/dialog_templates/flutter/flutter_requirements.dart index ce56ec6f..ce461862 100644 --- a/lib/components/dialog_templates/flutter/flutter_requirements.dart +++ b/lib/components/dialog_templates/flutter/flutter_requirements.dart @@ -34,7 +34,6 @@ class FlutterRequirementsDialog extends StatelessWidget { _linuxTemplate() else RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Column( children: [ VSeparators.xSmall(), diff --git a/lib/components/dialog_templates/logs/build_logs.dart b/lib/components/dialog_templates/logs/build_logs.dart index 687e7507..be39013c 100644 --- a/lib/components/dialog_templates/logs/build_logs.dart +++ b/lib/components/dialog_templates/logs/build_logs.dart @@ -71,10 +71,12 @@ Future _generateReportOnIsolate(List data) async { logDir: Directory(_basePath)); _port.send(true); + return; } catch (_, s) { await logger.file(LogTypeTag.error, 'Failed to generate issue report. $_', stackTraces: s, logDir: Directory(_basePath)); _port.send(false); + return; } } @@ -100,7 +102,7 @@ class _BuildLogsDialogState extends State { Future _generateReport() async { try { - await Isolate.spawn(_generateReportOnIsolate, [ + Isolate _i = await Isolate.spawn(_generateReportOnIsolate, [ _generatePort.sendPort, (await getApplicationSupportDirectory()).path, _savePath, @@ -122,6 +124,7 @@ class _BuildLogsDialogState extends State { type: SnackBarType.error, ), ); + _i.kill(); return; } @@ -136,6 +139,8 @@ class _BuildLogsDialogState extends State { await shell.run('xdg-open ' + _savePath!); } + _i.kill(); + await Future.delayed(const Duration(seconds: 5)); if (mounted) { @@ -155,7 +160,8 @@ class _BuildLogsDialogState extends State { ); setState(() => _savePath = null); - + + _i.kill(); _generatePort.close(); return; } @@ -193,7 +199,6 @@ class _BuildLogsDialogState extends State { ), VSeparators.normal(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/components/dialog_templates/other/clear_cache.dart b/lib/components/dialog_templates/other/clear_cache.dart index 6399c3c0..79730e7f 100644 --- a/lib/components/dialog_templates/other/clear_cache.dart +++ b/lib/components/dialog_templates/other/clear_cache.dart @@ -1,6 +1,13 @@ +// 🎯 Dart imports: import 'dart:io'; +// 🐦 Flutter imports: import 'package:flutter/material.dart'; + +// 📦 Package imports: +import 'package:path_provider/path_provider.dart'; + +// 🌎 Project imports: import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/components/dialog_templates/dialog_header.dart'; import 'package:fluttermatic/components/widgets/buttons/rectangle_button.dart'; @@ -14,7 +21,6 @@ import 'package:fluttermatic/core/services/logs.dart'; import 'package:fluttermatic/main.dart'; import 'package:fluttermatic/meta/utils/app_theme.dart'; import 'package:fluttermatic/meta/utils/shared_pref.dart'; -import 'package:path_provider/path_provider.dart'; class ClearCacheDialog extends StatefulWidget { const ClearCacheDialog({Key? key}) : super(key: key); diff --git a/lib/components/dialog_templates/project/common/name.dart b/lib/components/dialog_templates/project/common/name.dart index a4a983c0..368a3a60 100644 --- a/lib/components/dialog_templates/project/common/name.dart +++ b/lib/components/dialog_templates/project/common/name.dart @@ -133,7 +133,6 @@ class _ProjectNameSectionState extends State { 'Your project name can only include lower-case English letters (a-z) and underscores (_).'), VSeparators.normal(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Expanded( diff --git a/lib/components/dialog_templates/project/dart/sections/template.dart b/lib/components/dialog_templates/project/dart/sections/template.dart index 15735329..3b75feef 100644 --- a/lib/components/dialog_templates/project/dart/sections/template.dart +++ b/lib/components/dialog_templates/project/dart/sections/template.dart @@ -42,7 +42,6 @@ class _DartProjectTemplateSectionState 'Choose your Dart template which will be used to generate your project.'), VSeparators.small(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: SelectTile( defaultValue: widget.selectedTemplate, options: diff --git a/lib/components/dialog_templates/project/flutter/sections/pre_config.dart b/lib/components/dialog_templates/project/flutter/sections/pre_config.dart index 4f4fff90..ddac0b3e 100644 --- a/lib/components/dialog_templates/project/flutter/sections/pre_config.dart +++ b/lib/components/dialog_templates/project/flutter/sections/pre_config.dart @@ -45,7 +45,6 @@ class _FlutterProjectPreConfigSectionState extends State[ Expanded( @@ -146,7 +145,6 @@ class _FlutterProjectPreConfigSectionState extends State[ Row( diff --git a/lib/components/dialog_templates/settings/sections/discover.dart b/lib/components/dialog_templates/settings/sections/discover.dart index 89663e2f..0d9ea0ff 100644 --- a/lib/components/dialog_templates/settings/sections/discover.dart +++ b/lib/components/dialog_templates/settings/sections/discover.dart @@ -66,7 +66,6 @@ class DiscoverSettingsSection extends StatelessWidget { VSeparators.small(), RoundContainer( width: double.infinity, - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ const Expanded( diff --git a/lib/components/dialog_templates/settings/sections/editors.dart b/lib/components/dialog_templates/settings/sections/editors.dart index 4f07276c..47681883 100644 --- a/lib/components/dialog_templates/settings/sections/editors.dart +++ b/lib/components/dialog_templates/settings/sections/editors.dart @@ -126,7 +126,6 @@ class _EditorsSettingsSectionState extends State { ), VSeparators.small(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: CheckBoxElement( onChanged: (bool? val) async { setState(() => _askEditorAlways = (val ?? true)); @@ -134,7 +133,6 @@ class _EditorsSettingsSectionState extends State { .pref .setBool(SPConst.askEditorAlways, val ?? true); - print(SharedPref().pref.getBool(SPConst.askEditorAlways)); }, value: _askEditorAlways, text: 'Always ask me which editor to use', diff --git a/lib/components/dialog_templates/settings/sections/github.dart b/lib/components/dialog_templates/settings/sections/github.dart index 7a227448..ad026eac 100644 --- a/lib/components/dialog_templates/settings/sections/github.dart +++ b/lib/components/dialog_templates/settings/sections/github.dart @@ -2,16 +2,19 @@ import 'package:flutter/material.dart'; // 📦 Package imports: +import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; // 🌎 Project imports: import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/components/dialog_templates/dialog_header.dart'; import 'package:fluttermatic/components/dialog_templates/logs/build_logs.dart'; +import 'package:fluttermatic/components/widgets/buttons/action_options.dart'; import 'package:fluttermatic/components/widgets/buttons/rectangle_button.dart'; import 'package:fluttermatic/components/widgets/ui/dialog_template.dart'; import 'package:fluttermatic/components/widgets/ui/info_widget.dart'; import 'package:fluttermatic/components/widgets/ui/information_widget.dart'; +import 'package:fluttermatic/components/widgets/ui/round_container.dart'; import 'package:fluttermatic/components/widgets/ui/tab_view.dart'; class GitHubSettingsSection extends StatelessWidget { @@ -110,6 +113,75 @@ class GitHubSettingsSection extends StatelessWidget { VSeparators.small(), infoWidget(context, 'We are open-source! We would love to see you make some pull requests to this tool!'), + VSeparators.normal(), + const Text('FlutterMatic Programs'), + VSeparators.small(), + informationWidget( + 'If you are interested in joining our preview programs (switch to Beta or Alpha), you can do so here. We recommend that you stay in the Stable channel for the best experience.'), + VSeparators.normal(), + RoundContainer( + child: Row( + children: [ + const Text('Current Build is '), + Expanded( + child: Text(appBuild.substring(0, 1).toUpperCase() + + appBuild.substring(1).toLowerCase()), + ), + HSeparators.normal(), + const Icon(Icons.check_rounded, color: kGreenColor, size: 15), + ], + ), + ), + VSeparators.small(), + ActionOptions( + actionButtonBuilder: (_, ActionOptionsObject action) { + switch (action.title) { + case 'Stable': + return Padding( + padding: const EdgeInsets.only(right: 10), + child: Tooltip( + message: + 'This is the stage that we recommend you stay in for the best experience. We will be releasing stable updates only to this channel.', + child: SvgPicture.asset(Assets.done, height: 15), + ), + ); + case 'Beta': + return Padding( + padding: const EdgeInsets.only(right: 10), + child: Tooltip( + message: + 'This is more stable than Alpha, but less stable than the normal release. Join if you are interested in seeing upcoming features earlier.', + child: SvgPicture.asset(Assets.warn, height: 15), + ), + ); + case 'Alpha': + return Padding( + padding: const EdgeInsets.only(right: 10), + child: Tooltip( + message: + 'This preview stage is not recommended unless you are ok with risky unstable builds.', + child: SvgPicture.asset(Assets.error, height: 15), + ), + ); + default: + return const SizedBox.shrink(); + } + }, + actions: [ + if (appBuild.substring(0, 1).toUpperCase() + + appBuild.substring(1).toLowerCase() != + 'Stable') + ActionOptionsObject('Stable', () {}), + if (appBuild.substring(0, 1).toUpperCase() + + appBuild.substring(1).toLowerCase() != + 'Beta') + ActionOptionsObject('Beta', () {}), + if (appBuild.substring(0, 1).toUpperCase() + + appBuild.substring(1).toLowerCase() != + 'Alpha') + ActionOptionsObject('Alpha', () {}), + ], + ), ], ); } diff --git a/lib/components/dialog_templates/settings/sections/overview.dart b/lib/components/dialog_templates/settings/sections/overview.dart index 6217a9f3..382d8360 100644 --- a/lib/components/dialog_templates/settings/sections/overview.dart +++ b/lib/components/dialog_templates/settings/sections/overview.dart @@ -31,7 +31,6 @@ class _OverviewSettingsSectionState extends State { allowContentScroll: false, content: [ RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: CheckBoxElement( onChanged: (bool? value) async { await SharedPref() diff --git a/lib/components/dialog_templates/settings/sections/projects.dart b/lib/components/dialog_templates/settings/sections/projects.dart index 7f17daaf..edb2f159 100644 --- a/lib/components/dialog_templates/settings/sections/projects.dart +++ b/lib/components/dialog_templates/settings/sections/projects.dart @@ -77,7 +77,6 @@ class _ProjectsSettingsSectionState extends State { ), ), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Expanded( diff --git a/lib/components/dialog_templates/settings/sections/troubleshoot.dart b/lib/components/dialog_templates/settings/sections/troubleshoot.dart index 79a83c9d..143f7aae 100644 --- a/lib/components/dialog_templates/settings/sections/troubleshoot.dart +++ b/lib/components/dialog_templates/settings/sections/troubleshoot.dart @@ -75,7 +75,6 @@ class _TroubleShootSettingsSectionState ), ), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -128,11 +127,8 @@ class _TroubleShootSettingsSectionState VSeparators.small(), Row( children: [ - Container( - width: 2, - height: 60, - color: Colors.blueGrey.withOpacity(0.2), - ), + const RoundContainer( + child: SizedBox.shrink(), width: 2, height: 60), HSeparators.xSmall(), Expanded( child: Wrap( diff --git a/lib/components/widgets/inputs/text_field.dart b/lib/components/widgets/inputs/text_field.dart index a75edc8c..005d5c58 100644 --- a/lib/components/widgets/inputs/text_field.dart +++ b/lib/components/widgets/inputs/text_field.dart @@ -2,8 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +// 📦 Package imports: +import 'package:provider/src/provider.dart'; + // 🌎 Project imports: import 'package:fluttermatic/app/constants/constants.dart'; +import 'package:fluttermatic/core/notifiers/theme.notifier.dart'; class CustomTextField extends StatelessWidget { final String? hintText; @@ -83,7 +87,8 @@ class CustomTextField extends StatelessWidget { borderRadius: BorderRadius.circular(5), ), suffixIcon: suffixIcon, - fillColor: Colors.blueGrey.withOpacity(0.2), + fillColor: Colors.blueGrey.withOpacity( + context.read().isDarkTheme ? 0.2 : 0.1), filled: true, hintText: hintText, counterStyle: TextStyle( diff --git a/lib/components/widgets/ui/activity_tile.dart b/lib/components/widgets/ui/activity_tile.dart deleted file mode 100644 index b78082ce..00000000 --- a/lib/components/widgets/ui/activity_tile.dart +++ /dev/null @@ -1,38 +0,0 @@ -// 🐦 Flutter imports: -import 'package:flutter/material.dart'; - -// 🌎 Project imports: -import 'package:fluttermatic/app/constants/constants.dart'; -import 'package:fluttermatic/components/widgets/ui/spinner.dart'; - -class BgActivityTile extends StatelessWidget { - final String title; - final String activityId; - - const BgActivityTile({ - Key? key, - required this.title, - required this.activityId, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - VSeparators.normal(), - Row( - children: [ - Expanded(child: Text(title)), - const SizedBox(width: 8), - const Spinner(thickness: 2, size: 15), - ], - ), - VSeparators.normal(), - ColoredBox( - color: Colors.blueGrey.withOpacity(0.5), - child: const SizedBox(width: double.infinity, height: 2), - ), - ], - ); - } -} diff --git a/lib/components/widgets/ui/coming_soon.dart b/lib/components/widgets/ui/coming_soon.dart index 4404b5c3..e23e4271 100644 --- a/lib/components/widgets/ui/coming_soon.dart +++ b/lib/components/widgets/ui/coming_soon.dart @@ -1,28 +1,52 @@ +// 🐦 Flutter imports: import 'package:flutter/material.dart'; + +// 🌎 Project imports: import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; -class ComingSoonTile extends StatelessWidget { +class ComingSoonTile extends StatefulWidget { const ComingSoonTile({Key? key}) : super(key: key); + @override + State createState() => _ComingSoonTileState(); +} + +class _ComingSoonTileState extends State { + bool _isVisible = false; + + @override + void initState() { + Future.delayed(const Duration(milliseconds: 300)).then((_) { + if (mounted) { + setState(() => _isVisible = true); + } + }); + super.initState(); + } + @override Widget build(BuildContext context) { - return RoundContainer( - padding: const EdgeInsets.fromLTRB(5, 3, 5, 3), - color: kGreenColor.withOpacity(0.1), - radius: 50, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const RoundContainer( - height: 5, - width: 5, - color: kGreenColor, - child: SizedBox.shrink(), - ), - HSeparators.xSmall(), - const Text('Coming Soon', style: TextStyle(color: kGreenColor)), - ], + return AnimatedOpacity( + opacity: _isVisible ? 0.6 : 0, + duration: const Duration(milliseconds: 500), + child: RoundContainer( + padding: const EdgeInsets.fromLTRB(5, 3, 5, 3), + color: kGreenColor.withOpacity(0.1), + radius: 50, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const RoundContainer( + height: 5, + width: 5, + color: kGreenColor, + child: SizedBox.shrink(), + ), + HSeparators.xSmall(), + const Text('Coming Soon', style: TextStyle(color: kGreenColor)), + ], + ), ), ); } diff --git a/lib/components/widgets/ui/info_widget.dart b/lib/components/widgets/ui/info_widget.dart index ce705643..092d26a7 100644 --- a/lib/components/widgets/ui/info_widget.dart +++ b/lib/components/widgets/ui/info_widget.dart @@ -3,13 +3,9 @@ import 'package:flutter/material.dart'; // 🌎 Project imports: import 'package:fluttermatic/components/widgets/ui/round_container.dart'; -import 'package:fluttermatic/meta/utils/app_theme.dart'; Widget infoWidget(BuildContext context, String text) { return RoundContainer( - color: Theme.of(context).isDarkTheme - ? Colors.blueGrey.withOpacity(0.2) - : AppTheme.lightCardColor, radius: 5, child: Row( children: [ diff --git a/lib/components/widgets/ui/linear_progress_indicator.dart b/lib/components/widgets/ui/linear_progress_indicator.dart index 8ca4e13b..41a24deb 100644 --- a/lib/components/widgets/ui/linear_progress_indicator.dart +++ b/lib/components/widgets/ui/linear_progress_indicator.dart @@ -21,9 +21,7 @@ class CustomLinearProgressIndicator extends StatelessWidget { ? const Color(0xff262F34) : Colors.white, borderRadius: BorderRadius.circular(5), - border: Border.all( - color: Colors.blueGrey.withOpacity(0.4), - ), + border: Border.all(color: Colors.blueGrey.withOpacity(0.4)), ), padding: const EdgeInsets.all(10), child: ClipRRect( diff --git a/lib/components/widgets/ui/round_container.dart b/lib/components/widgets/ui/round_container.dart index cbbe94fe..b043aeea 100644 --- a/lib/components/widgets/ui/round_container.dart +++ b/lib/components/widgets/ui/round_container.dart @@ -35,7 +35,7 @@ class RoundContainer extends StatelessWidget { decoration: BoxDecoration( color: color ?? (Theme.of(context).isDarkTheme - ? AppTheme.darkCardColor + ? Colors.blueGrey.withOpacity(0.2) : AppTheme.lightCardColor), border: Border.all(color: borderColor!, width: borderWith), borderRadius: BorderRadius.circular(radius ?? 5), diff --git a/lib/components/widgets/ui/stage_tile.dart b/lib/components/widgets/ui/stage_tile.dart index 383add03..9fd26413 100644 --- a/lib/components/widgets/ui/stage_tile.dart +++ b/lib/components/widgets/ui/stage_tile.dart @@ -12,44 +12,65 @@ import 'package:fluttermatic/components/widgets/ui/dialog_template.dart'; import 'package:fluttermatic/components/widgets/ui/information_widget.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; -class StageTile extends StatelessWidget { +class StageTile extends StatefulWidget { final StageType stageType; const StageTile({Key? key, this.stageType = StageType.beta}) : super(key: key); + @override + State createState() => _StageTileState(); +} + +class _StageTileState extends State { + bool _isVisible = false; + + @override + void initState() { + Future.delayed(const Duration(milliseconds: 300)).then((_) { + if (mounted) { + setState(() => _isVisible = true); + } + }); + super.initState(); + } + @override Widget build(BuildContext context) { - return InkWell( - onTap: stageType == StageType.prerelease - ? null - : () { - switch (stageType) { - case StageType.beta: - showDialog( - context: context, - builder: (_) => const _BetaInfoDialog(), - ); - break; - case StageType.alpha: - showDialog( - context: context, - builder: (_) => const _AlphaInfoDialog(), - ); - break; - case StageType.prerelease: - break; - } - }, - child: Tooltip( - message: _message(stageType), - child: RoundContainer( - borderColor: kGreenColor, - color: Colors.transparent, - padding: const EdgeInsets.all(6), - child: Text( - _name(stageType), - style: const TextStyle(color: kGreenColor, fontSize: 12), + return AnimatedOpacity( + opacity: _isVisible ? 0.6 : 0, + duration: const Duration(milliseconds: 500), + child: InkWell( + onTap: widget.stageType == StageType.prerelease + ? null + : () { + switch (widget.stageType) { + case StageType.beta: + showDialog( + context: context, + builder: (_) => const _BetaInfoDialog(), + ); + break; + case StageType.alpha: + showDialog( + context: context, + builder: (_) => const _AlphaInfoDialog(), + ); + break; + case StageType.prerelease: + break; + } + }, + child: Tooltip( + message: _message(widget.stageType), + child: RoundContainer( + borderColor: kGreenColor, + color: Colors.transparent, + padding: const EdgeInsets.all(6), + child: Text( + _name(widget.stageType), + style: const TextStyle(color: kGreenColor, fontSize: 12), + ), ), ), ), diff --git a/lib/core/models/projects.model.dart b/lib/core/models/projects.model.dart index ba9c5293..b418ad06 100644 --- a/lib/core/models/projects.model.dart +++ b/lib/core/models/projects.model.dart @@ -3,12 +3,14 @@ class ProjectObject { final DateTime modDate; final String? description; final String path; + final bool pinned; const ProjectObject({ required this.name, required this.modDate, required this.path, required this.description, + required this.pinned, }); // Ability to convert to JSON. @@ -18,6 +20,7 @@ class ProjectObject { 'modDate': modDate.toIso8601String(), 'description': description, 'path': path, + 'pinned': pinned, }; } @@ -28,6 +31,7 @@ class ProjectObject { modDate: DateTime.parse(json['modDate'] as String), description: json['description'] as String?, path: json['path'] as String, + pinned: json['pinned'] as bool, ); } } diff --git a/lib/main.dart b/lib/main.dart index 3afff8bd..7662eb83 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,7 +25,6 @@ import 'package:fluttermatic/meta/utils/app_theme.dart'; import 'package:fluttermatic/meta/utils/shared_pref.dart'; import 'package:fluttermatic/meta/views/setup/components/windows_controls.dart'; import 'package:fluttermatic/meta/views/tabs/home.dart'; -import 'package:fluttermatic/meta/views/tabs/sections/pub/models/pkg_data.dart'; import 'meta/views/setup/screens/setup_view.dart'; Future main() async { @@ -105,8 +104,6 @@ class _FlutterMaticMainState extends State { // when the connection changes. await ConnectionNotifier().initConnectivity(); - // if (kDebugMode) await SharedPref().pref.clear(); - await SharedPref() .pref .setString(SPConst.appVersion, appVersion.toString()); @@ -192,17 +189,6 @@ class _FlutterMaticMainState extends State { } setState(() => _isChecking = false); - - // Get the package every hour to avoid loading it and waiting when the - // user goes to the packages tab in home. - while (mounted) { - await PkgViewData.getInitialPackages(); - - await logger.file(LogTypeTag.info, - 'Background fetched the pub list for performance improvements.'); - - await Future.delayed(const Duration(hours: 1)); - } } catch (_, s) { await logger.file(LogTypeTag.error, 'Failed to initialize data fetch. $_', stackTraces: s); diff --git a/lib/meta/utils/app_theme.dart b/lib/meta/utils/app_theme.dart index 9cf693f1..34ef70f0 100644 --- a/lib/meta/utils/app_theme.dart +++ b/lib/meta/utils/app_theme.dart @@ -42,7 +42,7 @@ class AppTheme { canvasColor: darkBackgroundColor, primaryColor: darkBackgroundColor, backgroundColor: darkBackgroundColor, - unselectedWidgetColor: Colors.blueGrey.withOpacity(0.4), + unselectedWidgetColor: Colors.blueGrey.withOpacity(0.3), scaffoldBackgroundColor: darkBackgroundColor, primaryColorLight: const Color(0xFF2D333A), focusColor: const Color(0xFF444C56), diff --git a/lib/meta/utils/bin/utils/projects.search.dart b/lib/meta/utils/bin/utils/projects.search.dart index 592404eb..f0781cfb 100644 --- a/lib/meta/utils/bin/utils/projects.search.dart +++ b/lib/meta/utils/bin/utils/projects.search.dart @@ -34,40 +34,57 @@ class ProjectSearchUtils { }) async { try { if (cache.projectsPath != null) { + // We will get the projects from cache so that we can keep the pinned + // projects status alive. + List _cachedProjects = + await getProjectsFromCache(supportDir); + List _projects = []; // Gets all the files in the path - List files = Directory.fromUri( + List _files = Directory.fromUri( Uri.file(cache.projectsPath!)) .listSync(recursive: true) .where((FileSystemEntity e) => e.path.endsWith('\\pubspec.yaml')) .toList(); // Adds to the projects list the parent path of the pubspec.yaml file - for (FileSystemEntity file in files) { + for (FileSystemEntity file in _files) { String _parentName = file.parent.path.split('\\').last; + // We will skip if this project is an example project for a project. if (_parentName == 'example') { - continue; + if (File(file.parent.parent.path + '\\pubspec.yaml').existsSync()) { + continue; + } } + // Extracts the pubspec file so we can check its attributes PubspecInfo _pubspec = extractPubspec( lines: await File(file.path).readAsLines(), path: file.path); + // Make sure this project contains a valid pubspec.yaml for it to + // be considered as a project which we will display. if (_pubspec.isValid) { _projects.add(ProjectObject( path: file.parent.path, name: _parentName, description: _pubspec.description, modDate: file.statSync().modified, + // We will keep the pinned status alive by checking if the + // project is in the cache and is pinned in the cache. + pinned: _cachedProjects.any( + (ProjectObject p) => p.path == file.parent.path && p.pinned), )); } } - // Sets the cache for the projects. + // Sets the cache for the projects locally on the system. await File(getProjectCachePath(supportDir)).writeAsString( jsonEncode(_projects.map((_) => _.toJson()).toList())); + // Updates the time the cache was updated so that we can refetch on + // time intervals. await ProjectServicesModel.updateProjectCache( cache: ProjectCacheResult( projectsPath: null, @@ -80,13 +97,16 @@ class ProjectSearchUtils { return _projects; } else { - await logger.file(LogTypeTag.info, + // The projects path to search was not set, so we will return an empty + // list and log this warning. + await logger.file(LogTypeTag.warning, 'Tried to get projects when the projects directory is not set.', logDir: Directory(supportDir)); return []; } } catch (_, s) { - await logger.file(LogTypeTag.error, 'Couldn\'t fetch projects from path', + await logger.file( + LogTypeTag.error, 'Couldn\'t fetch projects from path: $_', stackTraces: s, logDir: Directory(supportDir)); return []; } @@ -96,15 +116,14 @@ class ProjectSearchUtils { String supportDir) async { try { if (await hasCache(supportDir)) { - // Gets the projects from the cache. - List _projectsFromCache = (jsonDecode( - await File(getProjectCachePath(supportDir)).readAsString(), - ) as List) - // ignore: unnecessary_lambdas - .map((_) => ProjectObject.fromJson(_)) - .toList(); + // Gets the projects from the cache and decodes the JSON. + List _projectsCache = jsonDecode( + await File(getProjectCachePath(supportDir)).readAsString()) + as List; - return _projectsFromCache; + return _projectsCache + .map((_) => ProjectObject.fromJson(_ as Map)) + .toList(); } else { await logger.file(LogTypeTag.warning, 'Tried to get projects when the projects cache is not set. Should request to fetch in background as an initial fetch from path.', @@ -112,7 +131,8 @@ class ProjectSearchUtils { return []; } } catch (_, s) { - await logger.file(LogTypeTag.error, 'Couldn\'t fetch from projects cache', + await logger.file( + LogTypeTag.error, 'Couldn\'t fetch from projects cache: $_', stackTraces: s, logDir: Directory(supportDir)); return []; } diff --git a/lib/meta/utils/check_new_version.dart b/lib/meta/utils/check_new_version.dart index d48cd342..281c681f 100644 --- a/lib/meta/utils/check_new_version.dart +++ b/lib/meta/utils/check_new_version.dart @@ -35,10 +35,10 @@ Future checkNewFlutterMaticVersion(List data) async { // is no asset for this release on this platform, then it means // that the release is not targeted for this platform and perhaps // it's only a fix for a specific platform. - String _downloadUrl = - (jsonDecode(_result.body) as List>) - .firstWhere((Map asset) { - if (asset['name'].toLowerCase() == _platform) { + String _downloadUrl = (jsonDecode(_result.body) as List) + .firstWhere((dynamic asset) { + if ((asset as Map)['name'].toLowerCase() == + _platform) { _isTargeted = true; return true; } diff --git a/lib/meta/utils/clear_old_logs.dart b/lib/meta/utils/clear_old_logs.dart index ae903818..a7543192 100644 --- a/lib/meta/utils/clear_old_logs.dart +++ b/lib/meta/utils/clear_old_logs.dart @@ -55,9 +55,11 @@ Future clearOldLogs(List data) async { } _port.send(true); + return; } catch (_, s) { await logger.file(LogTypeTag.error, 'Failed to delete old logs: $_', stackTraces: s, logDir: Directory(_path)); _port.send(false); + return; } } diff --git a/lib/meta/views/dialogs/drive_error.dart b/lib/meta/views/dialogs/drive_error.dart index 789888f8..9d574eb0 100644 --- a/lib/meta/views/dialogs/drive_error.dart +++ b/lib/meta/views/dialogs/drive_error.dart @@ -31,7 +31,6 @@ class SystemDriveErrorDialog extends StatelessWidget { ), VSeparators.normal(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/meta/views/dialogs/notifications/notification_tile.dart b/lib/meta/views/dialogs/notifications/notification_tile.dart index 54f53015..2c3da502 100644 --- a/lib/meta/views/dialogs/notifications/notification_tile.dart +++ b/lib/meta/views/dialogs/notifications/notification_tile.dart @@ -41,7 +41,6 @@ class _NotificationTileState extends State { 'Notification tapped: ${widget.notification.id}'); }, child: RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/meta/views/dialogs/open_project.dart b/lib/meta/views/dialogs/open_project.dart index 867b8048..0bed7f4c 100644 --- a/lib/meta/views/dialogs/open_project.dart +++ b/lib/meta/views/dialogs/open_project.dart @@ -131,6 +131,7 @@ class _OpenProjectInEditorState extends State { : Colors.transparent, padding: EdgeInsets.zero, child: RectangleButton( + color: Colors.transparent, height: 100, onPressed: () => setState(() => _selectedEditor = 'code'), child: Center( @@ -160,6 +161,7 @@ class _OpenProjectInEditorState extends State { : Colors.transparent, padding: EdgeInsets.zero, child: RectangleButton( + color: Colors.transparent, height: 100, onPressed: () => setState(() => _selectedEditor = 'studio64'), @@ -185,7 +187,6 @@ class _OpenProjectInEditorState extends State { ), VSeparators.normal(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: CheckBoxElement( onChanged: (bool? val) async { val = !(val ?? false); diff --git a/lib/meta/views/dialogs/update_fluttermatic.dart b/lib/meta/views/dialogs/update_fluttermatic.dart index d5e6d3b0..c8169b16 100644 --- a/lib/meta/views/dialogs/update_fluttermatic.dart +++ b/lib/meta/views/dialogs/update_fluttermatic.dart @@ -109,7 +109,6 @@ class _UpdateFlutterMaticDialogState extends State { type: InformationType.green), VSeparators.normal(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Container(width: 2, height: 20, color: kGreenColor), @@ -261,7 +260,6 @@ class _UpdateInstructionsDialog extends StatelessWidget { type: InformationType.green), VSeparators.normal(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/meta/views/setup/components/tool_installed.dart b/lib/meta/views/setup/components/tool_installed.dart index 6719aa36..c440ae2f 100644 --- a/lib/meta/views/setup/components/tool_installed.dart +++ b/lib/meta/views/setup/components/tool_installed.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; // 🌎 Project imports: import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; -import 'package:fluttermatic/meta/utils/app_theme.dart'; Widget setUpToolInstalled( BuildContext context, { @@ -12,9 +11,6 @@ Widget setUpToolInstalled( required String message, }) { return RoundContainer( - color: Theme.of(context).isDarkTheme - ? Colors.blueGrey.withOpacity(0.2) - : AppTheme.lightCardColor, padding: const EdgeInsets.all(15), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/meta/views/setup/components/windows_controls.dart b/lib/meta/views/setup/components/windows_controls.dart index 5ae31071..afa1e098 100644 --- a/lib/meta/views/setup/components/windows_controls.dart +++ b/lib/meta/views/setup/components/windows_controls.dart @@ -24,7 +24,6 @@ Widget windowControls(BuildContext context, {bool disabled = false}) { _control( context, icon: Icons.remove_rounded, - radius: const BorderRadius.only(bottomLeft: Radius.circular(10)), onPressed: () => appWindow.minimize(), ), _control( @@ -49,7 +48,6 @@ Widget _control( BuildContext context, { required IconData icon, required VoidCallback onPressed, - BorderRadius radius = BorderRadius.zero, _HoverType hoverType = _HoverType.normal, }) { return SizedBox( @@ -58,7 +56,6 @@ Widget _control( focusColor: Colors.transparent, splashColor: Colors.transparent, highlightColor: Colors.transparent, - shape: RoundedRectangleBorder(borderRadius: radius), hoverColor: hoverType == _HoverType.normal ? Colors.blueGrey.withOpacity(0.2) : AppTheme.errorColor, diff --git a/lib/meta/views/setup/sections/install_java.dart b/lib/meta/views/setup/sections/install_java.dart index cda6afdc..ef3b945a 100644 --- a/lib/meta/views/setup/sections/install_java.dart +++ b/lib/meta/views/setup/sections/install_java.dart @@ -116,7 +116,6 @@ Widget installJava( : Colors.black), ), hoverColor: AppTheme.errorColor, - color: Colors.blueGrey.withOpacity(0.2), onPressed: () { Navigator.pop(context); onSkip(); @@ -135,7 +134,6 @@ Widget installJava( ? Colors.white : Colors.black), ), - color: Colors.blueGrey.withOpacity(0.2), onPressed: () => Navigator.pop(context), ), ), diff --git a/lib/meta/views/tabs/components/horizontal_axis.dart b/lib/meta/views/tabs/components/horizontal_axis.dart index 6e8d63b8..6f546e91 100644 --- a/lib/meta/views/tabs/components/horizontal_axis.dart +++ b/lib/meta/views/tabs/components/horizontal_axis.dart @@ -12,6 +12,8 @@ class HorizontalAxisView extends StatefulWidget { final bool isVertical; final Widget? action; final bool canCollapse; + final bool isCollapsedInitially; + final Function(bool isCollapsed)? onCollapse; const HorizontalAxisView({ Key? key, @@ -19,7 +21,9 @@ class HorizontalAxisView extends StatefulWidget { required this.content, this.isVertical = false, this.canCollapse = false, + this.isCollapsedInitially = false, this.action, + this.onCollapse, }) : super(key: key); @override @@ -27,41 +31,66 @@ class HorizontalAxisView extends StatefulWidget { } class _HorizontalAxisViewState extends State { - bool _collapsed = false; + late bool _collapsed = widget.isCollapsedInitially; final ScrollController _controller = ScrollController(); + bool _hoveringOnHiddenTile = false; + @override Widget build(BuildContext context) { Size _size = MediaQuery.of(context).size; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Text( - widget.title + - (widget.canCollapse && _collapsed ? ' - Collapsed' : ''), - style: const TextStyle(fontSize: 20), + GestureDetector( + onDoubleTap: () { + setState(() => _collapsed = !_collapsed); + if (widget.onCollapse != null) { + widget.onCollapse!(_collapsed); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + widget.title + + (widget.canCollapse && _collapsed ? ' - Collapsed' : ''), + style: const TextStyle(fontSize: 20), + ), ), - ), - HSeparators.normal(), - if (widget.action != null) widget.action!, - if (widget.canCollapse) ...[ HSeparators.normal(), - SquareButton( - size: 30, - icon: Icon( - _collapsed - ? Icons.keyboard_arrow_up_rounded - : Icons.keyboard_arrow_down_rounded, - size: 18), - onPressed: () => setState(() => _collapsed = !_collapsed), - ), - ] - ], + if (widget.action != null) widget.action!, + if (widget.canCollapse) ...[ + HSeparators.xSmall(), + const RoundContainer( + width: 2, + height: 10, + padding: EdgeInsets.zero, + child: SizedBox.shrink(), + ), + HSeparators.xSmall(), + SquareButton( + tooltip: _collapsed ? 'Expand' : 'Collapse', + size: 20, + color: Colors.transparent, + hoverColor: Colors.transparent, + icon: Icon( + _collapsed + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + size: 18), + onPressed: () { + setState(() => _collapsed = !_collapsed); + if (widget.onCollapse != null) { + widget.onCollapse!(_collapsed); + } + }, + ), + ] + ], + ), ), if (!_collapsed) ...[ VSeparators.large(), @@ -97,17 +126,39 @@ class _HorizontalAxisViewState extends State { ), ] else ...[ VSeparators.small(), - RoundContainer( - width: double.infinity, - // padding: EdgeInsets.zero, - color: Colors.blueGrey.withOpacity(0.2), - child: Row( - children: [ - const Icon(Icons.disabled_by_default_rounded), - HSeparators.small(), - Text(widget.content.length.toString() + - ' item${widget.content.length > 1 ? 's' : ''} hidden'), - ], + MouseRegion( + onEnter: (_) => setState(() => _hoveringOnHiddenTile = true), + onExit: (_) => setState(() => _hoveringOnHiddenTile = false), + child: GestureDetector( + onDoubleTap: () { + setState(() => _collapsed = !_collapsed); + if (widget.onCollapse != null) { + widget.onCollapse!(_collapsed); + } + }, + child: RoundContainer( + width: double.infinity, + child: Row( + children: [ + const Icon(Icons.disabled_by_default_rounded), + HSeparators.small(), + Expanded( + child: Text(widget.content.length.toString() + + ' item${widget.content.length > 1 ? 's' : ''} hidden'), + ), + HSeparators.normal(), + AnimatedOpacity( + opacity: _hoveringOnHiddenTile ? 1 : 0, + duration: const Duration(milliseconds: 100), + child: const Text( + 'Double tap to expand', + maxLines: 1, + style: TextStyle(color: Colors.grey), + ), + ), + ], + ), + ), ), ), ], diff --git a/lib/meta/views/tabs/home.dart b/lib/meta/views/tabs/home.dart index 775ab9a2..5dbbf27e 100644 --- a/lib/meta/views/tabs/home.dart +++ b/lib/meta/views/tabs/home.dart @@ -53,12 +53,11 @@ class _HomeScreenState extends State { // Tabs late HomeTabObject _selectedTab; - late final List _tabs = const [ + static const List _tabs = [ HomeTabObject('Home', Assets.home, HomeMainSection()), HomeTabObject('Projects', Assets.project, HomeProjectsSection()), HomeTabObject('Pub Packages', Assets.package, HomePubSection()), - HomeTabObject( - 'Workflows', Assets.workflow, HomeWorkflowSections(), 'Alpha'), + HomeTabObject('Workflows', Assets.workflow, HomeWorkflowSections(), 'Beta'), ]; Future _checkUpdates() async { @@ -80,7 +79,7 @@ class _HomeScreenState extends State { continue; } - await Isolate.spawn(checkNewFlutterMaticVersion, [ + Isolate _i = await Isolate.spawn(checkNewFlutterMaticVersion, [ _checkUpdatesPort.sendPort, (await getApplicationSupportDirectory()).path, Platform.operatingSystem.toLowerCase() @@ -88,6 +87,7 @@ class _HomeScreenState extends State { _checkUpdatesPort.asBroadcastStream().listen((dynamic message) { if (mounted) { + _i.kill(); setState(() { _updateAvailable = message[0]; _updateDownloadUrl = message[1]; @@ -110,12 +110,15 @@ class _HomeScreenState extends State { // old to avoid clogging up the FlutterMatic app data space. await Future.delayed(const Duration(seconds: 5)); - await Isolate.spawn(clearOldLogs, [ + Isolate _i = await Isolate.spawn(clearOldLogs, [ _clearLogsPort.sendPort, (await getApplicationSupportDirectory()).path ]); - _clearLogsPort.asBroadcastStream().listen((_) => _clearLogsPort.close()); + _clearLogsPort.asBroadcastStream().listen((_) { + _i.kill(); + _clearLogsPort.close(); + }); } catch (_, s) { await logger.file(LogTypeTag.error, 'Couldn\'t clear logs: $_', stackTraces: s); diff --git a/lib/meta/views/tabs/search.dart b/lib/meta/views/tabs/search.dart index 5e31726e..9cd35bc8 100644 --- a/lib/meta/views/tabs/search.dart +++ b/lib/meta/views/tabs/search.dart @@ -195,9 +195,7 @@ class _HomeSearchComponentState extends State { ? const Color(0xff262F34) : Colors.white, borderRadius: BorderRadius.circular(5), - border: Border.all( - color: Colors.blueGrey.withOpacity(0.4), - ), + border: Border.all(color: Colors.blueGrey.withOpacity(0.4)), ), padding: const EdgeInsets.all(10), child: SingleChildScrollView( diff --git a/lib/meta/views/tabs/sections/home/elements/setup_guide.dart b/lib/meta/views/tabs/sections/home/elements/setup_guide.dart index d663b39e..aa70320e 100644 --- a/lib/meta/views/tabs/sections/home/elements/setup_guide.dart +++ b/lib/meta/views/tabs/sections/home/elements/setup_guide.dart @@ -7,7 +7,6 @@ import 'package:fluttermatic/app/constants/shared_pref.dart'; import 'package:fluttermatic/components/dialog_templates/project/select.dart'; import 'package:fluttermatic/components/dialog_templates/settings/settings.dart'; import 'package:fluttermatic/components/widgets/buttons/square_button.dart'; -import 'package:fluttermatic/components/widgets/ui/info_widget.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; import 'package:fluttermatic/components/widgets/ui/snackbar_tile.dart'; import 'package:fluttermatic/meta/utils/app_theme.dart'; @@ -98,6 +97,7 @@ class _HomeSetupGuideTileState extends State { RoundContainer( padding: const EdgeInsets.all(20), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ @@ -166,7 +166,7 @@ class _HomeSetupGuideTileState extends State { }, ); }).toList(), - infoWidget(context, + const Text( 'As we are preparing for more upcoming features, we will be adding more guides here.'), ], ), @@ -287,10 +287,15 @@ class __GuideItemState extends State<_GuideItem> { return Padding( padding: const EdgeInsets.only(bottom: 15), child: MouseRegion( - onHover: (_) => setState(() => _isHovering = true), + onHover: (_) { + if (!widget.isDone) { + setState(() => _isHovering = true); + } + }, onExit: (_) => setState(() => _isHovering = false), child: InkWell( - onTap: () => widget.onPressed(widget.context), + hoverColor: Colors.transparent, + onTap: widget.isDone ? null : () => widget.onPressed(widget.context), child: Row( children: [ Container( diff --git a/lib/meta/views/tabs/sections/home/version_tiles/dart_version.dart b/lib/meta/views/tabs/sections/home/version_tiles/dart_version.dart index aeb1c1eb..9249e80e 100644 --- a/lib/meta/views/tabs/sections/home/version_tiles/dart_version.dart +++ b/lib/meta/views/tabs/sections/home/version_tiles/dart_version.dart @@ -66,8 +66,9 @@ class _HomeFlutterVersionStateTile extends State { Future _load() async { while (mounted) { Directory _logPath = await getApplicationSupportDirectory(); - await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) - .timeout(const Duration(minutes: 1), onTimeout: () async { + Isolate _i = + await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) + .timeout(const Duration(minutes: 1), onTimeout: () async { await logger.file(LogTypeTag.error, 'Dart version check timeout'); setState(() => _error = true); @@ -76,6 +77,7 @@ class _HomeFlutterVersionStateTile extends State { if (mounted && !_listening) { _port.listen((dynamic data) { + _i.kill(); setState(() => _listening = true); if (mounted) { setState(() { @@ -142,48 +144,65 @@ class _HomeFlutterVersionStateTile extends State { ], ), VSeparators.normal(), - HoverMessageWithIconAction( - message: _doneLoading - ? (_version == null - ? 'Dart is not installed on your device' - : 'Dart is up to date on channel ${_channel.toLowerCase()}') - : '...', - icon: Icon( - _doneLoading - ? (_version == null ? Icons.error : Icons.check_rounded) - : Icons.lock_clock, - color: _doneLoading - ? (_version == null ? AppTheme.errorColor : kGreenColor) - : kYellowColor, - size: 15), + IgnorePointer( + ignoring: _version == null && _doneLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _version == null && _doneLoading ? 0.2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HoverMessageWithIconAction( + message: _doneLoading + ? (_version == null + ? 'Dart is not installed on your device' + : 'Dart is up to date on channel ${_channel.toLowerCase()}') + : '...', + icon: Icon( + _doneLoading + ? (_version == null + ? Icons.error + : Icons.check_rounded) + : Icons.lock_clock, + color: _doneLoading + ? (_version == null + ? AppTheme.errorColor + : kGreenColor) + : kYellowColor, + size: 15), + ), + VSeparators.normal(), + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastDartUpdateCheck) + ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastDartUpdateCheck) ?? '...'))}' + : 'Never checked for new updates before', + icon: const Icon(Icons.refresh_rounded, + color: kGreenColor, size: 15), + onPressed: () { + showDialog( + context: context, + builder: (_) => const _UpdatingDartDialog(), + ); + }, + ), + VSeparators.normal(), + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastDartUpdate) + ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastDartUpdate) ?? '...'))}' + : 'Never updated before', + icon: const Icon(Icons.check_rounded, + color: kGreenColor, size: 15), + ), + ], + ), + ), ), VSeparators.normal(), - if (_doneLoading && _version != null || - !_doneLoading) ...[ - HoverMessageWithIconAction( - message: SharedPref() - .pref - .containsKey(SPConst.lastDartUpdateCheck) - ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastDartUpdateCheck) ?? DateTime.now().toString()))}' - : 'Never checked for new updates before', - icon: const Icon(Icons.refresh_rounded, - color: kGreenColor, size: 15), - onPressed: () { - showDialog( - context: context, - builder: (_) => const _UpdatingDartDialog(), - ); - }, - ), - VSeparators.normal(), - HoverMessageWithIconAction( - message: SharedPref().pref.containsKey(SPConst.lastDartUpdate) - ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastDartUpdate) ?? DateTime.now().toString()))}' - : 'Never updated before', - icon: const Icon(Icons.check_rounded, - color: kGreenColor, size: 15), - ), - VSeparators.normal(), + if (_version != null || !_doneLoading) Row( children: [ Expanded( @@ -210,8 +229,8 @@ class _HomeFlutterVersionStateTile extends State { ), ), ], - ), - ] else + ) + else RectangleButton( width: double.infinity, child: const Text('Install Dart'), diff --git a/lib/meta/views/tabs/sections/home/version_tiles/flutter_version.dart b/lib/meta/views/tabs/sections/home/version_tiles/flutter_version.dart index aacd9c92..50139f7e 100644 --- a/lib/meta/views/tabs/sections/home/version_tiles/flutter_version.dart +++ b/lib/meta/views/tabs/sections/home/version_tiles/flutter_version.dart @@ -40,6 +40,7 @@ Future _check(List data) async { _result.version?.toString(), _result.channel, ]); + return; } class HomeFlutterVersionTile extends StatefulWidget { @@ -63,8 +64,9 @@ class _HomeFlutterVersionStateTile extends State { Future _load() async { while (mounted) { Directory _logPath = await getApplicationSupportDirectory(); - await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) - .timeout(const Duration(minutes: 1), onTimeout: () async { + Isolate _i = + await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) + .timeout(const Duration(minutes: 1), onTimeout: () async { await logger.file(LogTypeTag.error, 'Flutter version check timeout'); setState(() => _error = true); @@ -73,6 +75,7 @@ class _HomeFlutterVersionStateTile extends State { if (mounted && !_listening) { _port.listen((dynamic data) { + _i.kill(); setState(() => _listening = true); if (mounted) { setState(() { @@ -138,50 +141,65 @@ class _HomeFlutterVersionStateTile extends State { ], ), VSeparators.normal(), - HoverMessageWithIconAction( - message: _doneLoading - ? (_version == null - ? 'Flutter is not installed on your device' - : 'Flutter is up to date on channel ${_channel.toLowerCase()}') - : '...', - icon: Icon( - _doneLoading - ? (_version == null ? Icons.error : Icons.check_rounded) - : Icons.lock_clock, - color: _doneLoading - ? (_version == null ? AppTheme.errorColor : kGreenColor) - : kYellowColor, - size: 15), + IgnorePointer( + ignoring: _version == null && _doneLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _version == null && _doneLoading ? 0.2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HoverMessageWithIconAction( + message: _doneLoading + ? (_version == null + ? 'Flutter is not installed on your device' + : 'Flutter is up to date on channel ${_channel.toLowerCase()}') + : '...', + icon: Icon( + _doneLoading + ? (_version == null + ? Icons.error + : Icons.check_rounded) + : Icons.lock_clock, + color: _doneLoading + ? (_version == null + ? AppTheme.errorColor + : kGreenColor) + : kYellowColor, + size: 15), + ), + VSeparators.normal(), + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastFlutterUpdateCheck) + ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastFlutterUpdateCheck) ?? '...'))}' + : 'Never checked for new updates before', + icon: const Icon(Icons.refresh_rounded, + color: kGreenColor, size: 15), + onPressed: () { + showDialog( + context: context, + builder: (_) => const UpdateFlutterDialog(), + ); + }, + ), + VSeparators.normal(), + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastFlutterUpdate) + ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastFlutterUpdate) ?? '...'))}' + : 'Never updated before', + icon: const Icon(Icons.check_rounded, + color: kGreenColor, size: 15), + ), + ], + ), + ), ), VSeparators.normal(), - if (_doneLoading && _version != null || - !_doneLoading) ...[ - HoverMessageWithIconAction( - message: SharedPref() - .pref - .containsKey(SPConst.lastFlutterUpdateCheck) - ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastFlutterUpdateCheck) ?? DateTime.now().toString()))}' - : 'Never checked for new updates before', - icon: const Icon(Icons.refresh_rounded, - color: kGreenColor, size: 15), - onPressed: () { - showDialog( - context: context, - builder: (_) => const UpdateFlutterDialog(), - ); - }, - ), - VSeparators.normal(), - HoverMessageWithIconAction( - message: SharedPref() - .pref - .containsKey(SPConst.lastFlutterUpdate) - ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastFlutterUpdate) ?? DateTime.now().toString()))}' - : 'Never updated before', - icon: const Icon(Icons.check_rounded, - color: kGreenColor, size: 15), - ), - VSeparators.normal(), + if (_version != null || !_doneLoading) Row( children: [ Expanded( @@ -208,8 +226,8 @@ class _HomeFlutterVersionStateTile extends State { ), ), ], - ), - ] else + ) + else RectangleButton( width: double.infinity, child: const Text('Install Flutter'), diff --git a/lib/meta/views/tabs/sections/home/version_tiles/git_version.dart b/lib/meta/views/tabs/sections/home/version_tiles/git_version.dart index 9799565d..26d793a2 100644 --- a/lib/meta/views/tabs/sections/home/version_tiles/git_version.dart +++ b/lib/meta/views/tabs/sections/home/version_tiles/git_version.dart @@ -37,6 +37,7 @@ Future _check(List data) async { _port.send([ _result.version?.toString(), ]); + return; } class HomeGitVersionTile extends StatefulWidget { @@ -59,8 +60,9 @@ class _HomeGitVersionStateTile extends State { Future _load() async { while (mounted) { Directory _logPath = await getApplicationSupportDirectory(); - await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) - .timeout(const Duration(minutes: 1), onTimeout: () async { + Isolate _i = + await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) + .timeout(const Duration(minutes: 1), onTimeout: () async { await logger.file(LogTypeTag.error, 'Git version check timeout'); setState(() => _error = true); @@ -69,6 +71,7 @@ class _HomeGitVersionStateTile extends State { if (mounted && !_listening) { _port.listen((dynamic data) { + _i.kill(); setState(() => _listening = true); if (mounted) { setState(() { @@ -134,35 +137,48 @@ class _HomeGitVersionStateTile extends State { ], ), VSeparators.normal(), - if (_doneLoading && _version != null || - !_doneLoading) ...[ - HoverMessageWithIconAction( - message: SharedPref() - .pref - .containsKey(SPConst.lastGitUpdateCheck) - ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastGitUpdateCheck) ?? DateTime.now().toString()))}' - : 'Never checked for new updates before', - icon: const Icon(Icons.refresh_rounded, - color: kGreenColor, size: 15), - onPressed: () {}, - // TODO: Show update git dialog - ), - VSeparators.normal(), - HoverMessageWithIconAction( - message: SharedPref().pref.containsKey(SPConst.lastGitUpdate) - ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastGitUpdate) ?? DateTime.now().toString()))}' - : 'Never updated before', - icon: const Icon(Icons.check_rounded, - color: kGreenColor, size: 15), + IgnorePointer( + ignoring: _version == null && _doneLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _version == null && _doneLoading ? 0.2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastGitUpdateCheck) + ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastGitUpdateCheck) ?? '...'))}' + : 'Never checked for new updates before', + icon: const Icon(Icons.refresh_rounded, + color: kGreenColor, size: 15), + onPressed: () {}, + // TODO: Show update git dialog + ), + VSeparators.normal(), + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastGitUpdate) + ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastGitUpdate) ?? '...'))}' + : 'Never updated before', + icon: const Icon(Icons.check_rounded, + color: kGreenColor, size: 15), + ), + ], + ), ), - VSeparators.normal(), + ), + VSeparators.normal(), + if (_version != null || !_doneLoading) RectangleButton( child: const Text('Check Updates'), width: double.infinity, onPressed: () {}, // TODO: Show update git dialog - ), - ] else + ) + else RectangleButton( width: double.infinity, child: const Text('Install Git'), diff --git a/lib/meta/views/tabs/sections/home/version_tiles/java_version.dart b/lib/meta/views/tabs/sections/home/version_tiles/java_version.dart index 5e46251f..e359cbad 100644 --- a/lib/meta/views/tabs/sections/home/version_tiles/java_version.dart +++ b/lib/meta/views/tabs/sections/home/version_tiles/java_version.dart @@ -7,6 +7,9 @@ import 'package:flutter/material.dart'; // 📦 Package imports: import 'package:flutter_svg/flutter_svg.dart'; +import 'package:fluttermatic/components/dialog_templates/dialog_header.dart'; +import 'package:fluttermatic/components/widgets/ui/dialog_template.dart'; +import 'package:fluttermatic/components/widgets/ui/information_widget.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pub_semver/src/version.dart'; @@ -15,7 +18,6 @@ import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/app/constants/enum.dart'; import 'package:fluttermatic/components/dialog_templates/other/install_tool.dart'; import 'package:fluttermatic/components/widgets/buttons/rectangle_button.dart'; -import 'package:fluttermatic/components/widgets/ui/information_widget.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; import 'package:fluttermatic/components/widgets/ui/shimmer.dart'; import 'package:fluttermatic/components/widgets/ui/stage_tile.dart'; @@ -36,6 +38,7 @@ Future _check(List data) async { _port.send([ _result.version?.toString(), ]); + return; } class HomeJavaVersionTile extends StatefulWidget { @@ -58,8 +61,9 @@ class _HomeFlutterVersionStateTile extends State { Future _load() async { while (mounted) { Directory _logPath = await getApplicationSupportDirectory(); - await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) - .timeout(const Duration(minutes: 1), onTimeout: () async { + Isolate _i = + await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) + .timeout(const Duration(minutes: 1), onTimeout: () async { await logger.file(LogTypeTag.error, 'Java version check timeout'); setState(() => _error = true); @@ -68,6 +72,7 @@ class _HomeFlutterVersionStateTile extends State { if (mounted && !_listening) { _port.listen((dynamic data) { + _i.kill(); setState(() => _listening = true); if (mounted) { setState(() { @@ -133,29 +138,70 @@ class _HomeFlutterVersionStateTile extends State { ], ), VSeparators.normal(), - HoverMessageWithIconAction( - message: _doneLoading - ? (_version == null - ? 'Java is not installed' - : 'Java is installed') - : '...', - icon: Icon( - _doneLoading - ? (_version == null - ? Icons.warning - : Icons.check_rounded) - : Icons.lock_clock, - color: _doneLoading - ? (_version == null ? AppTheme.errorColor : kGreenColor) - : kYellowColor, - size: 15), + IgnorePointer( + ignoring: _version == null && _doneLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _version == null && _doneLoading ? 0.2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HoverMessageWithIconAction( + message: _doneLoading + ? (_version == null + ? 'Java is not installed' + : 'Java is installed') + : '...', + icon: Icon( + _doneLoading + ? (_version == null + ? Icons.warning + : Icons.check_rounded) + : Icons.lock_clock, + color: _doneLoading + ? (_version == null + ? AppTheme.errorColor + : kGreenColor) + : kYellowColor, + size: 15, + ), + ), + VSeparators.normal(), + HoverMessageWithIconAction( + message: _doneLoading + ? (_version == null + ? 'Install Java for Android development' + : 'Java for Android development') + : '...', + icon: Icon( + _doneLoading + ? (_version == null + ? Icons.download_rounded + : Icons.check_rounded) + : Icons.lock_clock, + color: _doneLoading + ? (_version == null + ? AppTheme.errorColor + : kGreenColor) + : kYellowColor, + size: 15), + onPressed: () { + showDialog( + context: context, + builder: (_) => const InstallToolDialog( + tool: SetUpTab.installJava), + ); + }, + ), + ], + ), + ), ), VSeparators.normal(), - if (_doneLoading && _version == null) ...[ - HoverMessageWithIconAction( - message: 'Install Java', - icon: const Icon(Icons.download_rounded, - color: kGreenColor, size: 15), + if (_doneLoading && _version == null) + RectangleButton( + width: double.infinity, + child: const Text('Install Java'), onPressed: () { showDialog( context: context, @@ -163,24 +209,18 @@ class _HomeFlutterVersionStateTile extends State { const InstallToolDialog(tool: SetUpTab.installJava), ); }, - ), - VSeparators.normal(), + ) + else RectangleButton( width: double.infinity, - child: const Text('Install Java'), + child: const Text('Learn more'), onPressed: () { showDialog( context: context, - builder: (_) => - const InstallToolDialog(tool: SetUpTab.installJava), + builder: (_) => const _JavaAndroidDevelopment(), ); }, ), - ] else - informationWidget( - 'Java is specifically targeted at Android development. When using some plugins, Java helps avoid common issues with Android plugins for Flutter.', - type: InformationType.green, - ), ], ), ), @@ -188,3 +228,28 @@ class _HomeFlutterVersionStateTile extends State { ); } } + +class _JavaAndroidDevelopment extends StatelessWidget { + const _JavaAndroidDevelopment({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return DialogTemplate( + child: Column( + children: [ + const DialogHeader(title: 'Java'), + informationWidget( + 'Java is specifically targeted at Android development. When using some plugins, Java helps avoid common issues with Android plugins for Flutter.', + type: InformationType.green, + ), + VSeparators.normal(), + RectangleButton( + width: double.infinity, + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ); + } +} diff --git a/lib/meta/views/tabs/sections/home/version_tiles/studio_version.dart b/lib/meta/views/tabs/sections/home/version_tiles/studio_version.dart index 77bc0128..b5abdf92 100644 --- a/lib/meta/views/tabs/sections/home/version_tiles/studio_version.dart +++ b/lib/meta/views/tabs/sections/home/version_tiles/studio_version.dart @@ -37,6 +37,7 @@ Future _check(List data) async { _port.send([ _result.version?.toString(), ]); + return; } class HomeStudioVersionTile extends StatefulWidget { @@ -59,8 +60,9 @@ class _HomeStudioVersionStateTile extends State { Future _load() async { while (mounted) { Directory _logPath = await getApplicationSupportDirectory(); - await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) - .timeout(const Duration(minutes: 1), onTimeout: () async { + Isolate _i = + await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) + .timeout(const Duration(minutes: 1), onTimeout: () async { await logger.file( LogTypeTag.error, 'Android Studio version check timeout'); setState(() => _error = true); @@ -70,6 +72,7 @@ class _HomeStudioVersionStateTile extends State { if (mounted && !_listening) { _port.listen((dynamic data) { + _i.kill(); setState(() => _listening = true); if (mounted) { setState(() { @@ -135,37 +138,47 @@ class _HomeStudioVersionStateTile extends State { ], ), VSeparators.normal(), - if (_doneLoading && _version != null || - !_doneLoading) ...[ - HoverMessageWithIconAction( - message: SharedPref() - .pref - .containsKey(SPConst.lastAndroidStudioUpdateCheck) - ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastAndroidStudioUpdateCheck) ?? DateTime.now().toString()))}' - : 'Never checked for new updates before', - icon: const Icon(Icons.refresh_rounded, - color: kGreenColor, size: 15), - onPressed: () {}, - // TODO: Show update studio dialog - ), - VSeparators.normal(), - HoverMessageWithIconAction( - message: SharedPref() - .pref - .containsKey(SPConst.lastAndroidStudioUpdate) - ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastAndroidStudioUpdate) ?? DateTime.now().toString()))}' - : 'Never updated before', - icon: const Icon(Icons.check_rounded, - color: kGreenColor, size: 15), + IgnorePointer( + ignoring: _version == null && _doneLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _version == null && _doneLoading ? 0.2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HoverMessageWithIconAction( + message: SharedPref().pref.containsKey( + SPConst.lastAndroidStudioUpdateCheck) + ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastAndroidStudioUpdateCheck) ?? '...'))}' + : 'Never checked for new updates before', + icon: const Icon(Icons.refresh_rounded, + color: kGreenColor, size: 15), + onPressed: () {}, + // TODO: Show update studio dialog + ), + VSeparators.normal(), + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastAndroidStudioUpdate) + ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastAndroidStudioUpdate) ?? '...'))}' + : 'Never updated before', + icon: const Icon(Icons.check_rounded, + color: kGreenColor, size: 15), + ), + ], + ), ), - VSeparators.normal(), + ), + VSeparators.normal(), + if (_version != null || !_doneLoading) RectangleButton( child: const Text('Check Updates'), width: double.infinity, onPressed: () {}, // TODO: Show update studio dialog - ), - ] else + ) + else RectangleButton( width: double.infinity, child: const Text('Install Studio'), diff --git a/lib/meta/views/tabs/sections/home/version_tiles/vsc_version.dart b/lib/meta/views/tabs/sections/home/version_tiles/vsc_version.dart index 2a8e4584..3998e551 100644 --- a/lib/meta/views/tabs/sections/home/version_tiles/vsc_version.dart +++ b/lib/meta/views/tabs/sections/home/version_tiles/vsc_version.dart @@ -37,6 +37,7 @@ Future _check(List data) async { _port.send([ _result.version?.toString(), ]); + return; } class HomeVSCVersionTile extends StatefulWidget { @@ -59,8 +60,9 @@ class _HomeVSCVersionStateTile extends State { Future _load() async { while (mounted) { Directory _logPath = await getApplicationSupportDirectory(); - await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) - .timeout(const Duration(minutes: 1), onTimeout: () async { + Isolate _i = + await Isolate.spawn(_check, [_port.sendPort, _logPath.path]) + .timeout(const Duration(minutes: 1), onTimeout: () async { await logger.file(LogTypeTag.error, 'VS Code version check timeout'); setState(() => _error = true); @@ -69,6 +71,7 @@ class _HomeVSCVersionStateTile extends State { if (mounted && !_listening) { _port.listen((dynamic data) { + _i.kill(); setState(() => _listening = true); if (mounted) { setState(() { @@ -134,37 +137,48 @@ class _HomeVSCVersionStateTile extends State { ], ), VSeparators.normal(), - if (_doneLoading && _version != null || - !_doneLoading) ...[ - HoverMessageWithIconAction( - message: SharedPref() - .pref - .containsKey(SPConst.lastVSCodeUpdateCheck) - ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastVSCodeUpdateCheck) ?? DateTime.now().toString()))}' - : 'Never checked for new updates before', - icon: const Icon(Icons.refresh_rounded, - color: kGreenColor, size: 15), - onPressed: () {}, - // TODO: Show update vsc dialog - ), - VSeparators.normal(), - HoverMessageWithIconAction( - message: SharedPref() - .pref - .containsKey(SPConst.lastVSCodeUpdate) - ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastVSCodeUpdate) ?? DateTime.now().toString()))}' - : 'Never updated before', - icon: const Icon(Icons.check_rounded, - color: kGreenColor, size: 15), + IgnorePointer( + ignoring: _version == null && _doneLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _version == null && _doneLoading ? 0.2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastVSCodeUpdateCheck) + ? 'Checked for new updates ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastVSCodeUpdateCheck) ?? '...'))}' + : 'Never checked for new updates before', + icon: const Icon(Icons.refresh_rounded, + color: kGreenColor, size: 15), + onPressed: () {}, + // TODO: Show update vsc dialog + ), + VSeparators.normal(), + HoverMessageWithIconAction( + message: SharedPref() + .pref + .containsKey(SPConst.lastVSCodeUpdate) + ? 'Last updated ${getTimeAgo(DateTime.parse(SharedPref().pref.getString(SPConst.lastVSCodeUpdate) ?? '...'))}' + : 'Never updated before', + icon: const Icon(Icons.check_rounded, + color: kGreenColor, size: 15), + ), + ], + ), ), - VSeparators.normal(), + ), + VSeparators.normal(), + if (_version != null || !_doneLoading) RectangleButton( child: const Text('Check Updates'), width: double.infinity, onPressed: () {}, // TODO: Show update vsc dialog - ), - ] else + ) + else RectangleButton( width: double.infinity, child: const Text('Install VS Code'), diff --git a/lib/meta/views/tabs/sections/projects/dialogs/delete_projects.dart b/lib/meta/views/tabs/sections/projects/dialogs/delete_projects.dart index 405c24b8..8b2bd621 100644 --- a/lib/meta/views/tabs/sections/projects/dialogs/delete_projects.dart +++ b/lib/meta/views/tabs/sections/projects/dialogs/delete_projects.dart @@ -69,7 +69,6 @@ class _DeleteProjectDialogState extends State { 'This project is NOT a git repository. Please be aware that after you delete this project, you will not be able to recover it.'), VSeparators.normal(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/meta/views/tabs/sections/projects/elements/project_tile.dart b/lib/meta/views/tabs/sections/projects/elements/project_tile.dart index f15207eb..5337f7a3 100644 --- a/lib/meta/views/tabs/sections/projects/elements/project_tile.dart +++ b/lib/meta/views/tabs/sections/projects/elements/project_tile.dart @@ -5,26 +5,21 @@ import 'package:flutter/material.dart'; import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/components/widgets/buttons/rectangle_button.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; +import 'package:fluttermatic/core/models/projects.model.dart'; import 'package:fluttermatic/meta/views/dialogs/open_project.dart'; import 'package:fluttermatic/meta/views/tabs/sections/projects/dialogs/project_options.dart'; +import 'package:fluttermatic/meta/views/tabs/sections/projects/models/projects.services.dart'; import 'package:fluttermatic/meta/views/tabs/sections/projects/projects.dart'; import 'package:fluttermatic/meta/views/workflows/views/existing_workflows.dart'; -// 📦 Package imports: - - class ProjectInfoTile extends StatefulWidget { - final String name; - final String? description; - final DateTime modDate; - final String path; + final ProjectObject project; + final Function() onPinChanged; const ProjectInfoTile({ Key? key, - required this.name, - required this.description, - required this.modDate, - required this.path, + required this.project, + required this.onPinChanged, }) : super(key: key); @override @@ -37,6 +32,7 @@ class _ProjectInfoTileState extends State { @override Widget build(BuildContext context) { return MouseRegion( + key: ValueKey(widget.project.path), onEnter: (_) => setState(() => _isHovering = true), onExit: (_) => setState(() => _isHovering = false), child: RoundContainer( @@ -48,38 +44,62 @@ class _ProjectInfoTileState extends State { children: [ Expanded( child: Text( - widget.name, + widget.project.name, style: const TextStyle(fontSize: 18), maxLines: 1, overflow: TextOverflow.fade, softWrap: false, ), ), - if (_isHovering) - Padding( - padding: const EdgeInsets.only(left: 5), + if (_isHovering) ...[ + HSeparators.small(), + Tooltip( + waitDuration: const Duration(seconds: 1), + message: widget.project.pinned ? 'Unpin' : 'Pin', child: RectangleButton( padding: EdgeInsets.zero, - child: const Icon(Icons.more_vert, size: 14), + hoverColor: Colors.transparent, color: Colors.transparent, - onPressed: () { - showDialog( - context: context, - builder: (_) => - ProjectOptionsDialog(path: widget.path), - ); + child: Icon( + widget.project.pinned + ? Icons.push_pin_rounded + : Icons.push_pin_outlined, + color: widget.project.pinned ? kYellowColor : null, + size: 14), + onPressed: () async { + await ProjectServicesModel.updateProjectPinStatus( + widget.project.path, !widget.project.pinned); + + widget.onPinChanged(); }, radius: BorderRadius.circular(2), width: 22, height: 22, ), ), + HSeparators.xSmall(), + RectangleButton( + padding: EdgeInsets.zero, + child: const Icon(Icons.more_vert, size: 14), + color: Colors.transparent, + onPressed: () { + showDialog( + context: context, + builder: (_) => + ProjectOptionsDialog(path: widget.project.path), + ); + }, + radius: BorderRadius.circular(2), + width: 22, + height: 22, + ), + ], ], ), VSeparators.normal(), Expanded( child: Text( - widget.description ?? 'No project description found.', + widget.project.description ?? 'No project description found.', style: const TextStyle(color: Colors.grey), overflow: TextOverflow.ellipsis, maxLines: 3, @@ -90,9 +110,9 @@ class _ProjectInfoTileState extends State { padding: const EdgeInsets.only(top: 10), child: Tooltip( waitDuration: const Duration(milliseconds: 500), - message: widget.path, + message: widget.project.path, child: Text( - widget.path, + widget.project.path, maxLines: 1, overflow: TextOverflow.fade, softWrap: false, @@ -102,7 +122,7 @@ class _ProjectInfoTileState extends State { ), VSeparators.small(), Text( - 'Modified date: ${toMonth(widget.modDate.month)} ${widget.modDate.day}, ${widget.modDate.year}', + 'Modified date: ${toMonth(widget.project.modDate.month)} ${widget.project.modDate.day}, ${widget.project.modDate.year}', maxLines: 2, overflow: TextOverflow.fade, softWrap: false, @@ -117,7 +137,8 @@ class _ProjectInfoTileState extends State { onPressed: () { showDialog( context: context, - builder: (_) => OpenProjectInEditor(path: widget.path), + builder: (_) => + OpenProjectInEditor(path: widget.project.path), ); }, ), @@ -133,8 +154,8 @@ class _ProjectInfoTileState extends State { onPressed: () { showDialog( context: context, - builder: (_) => - ShowExistingWorkflows(pubspecPath: widget.path), + builder: (_) => ShowExistingWorkflows( + pubspecPath: widget.project.path), ); }, ), diff --git a/lib/meta/views/tabs/sections/projects/models/projects.services.dart b/lib/meta/views/tabs/sections/projects/models/projects.services.dart index b6f7c4a9..d35bbd11 100644 --- a/lib/meta/views/tabs/sections/projects/models/projects.services.dart +++ b/lib/meta/views/tabs/sections/projects/models/projects.services.dart @@ -3,6 +3,9 @@ import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; +// 📦 Package imports: +import 'package:path_provider/path_provider.dart'; + // 🌎 Project imports: import 'package:fluttermatic/core/models/projects.model.dart'; import 'package:fluttermatic/core/services/logs.dart'; @@ -56,6 +59,55 @@ class ProjectServicesModel { await _file.writeAsString(jsonEncode(_newCache.toJson())); } + /// Will update the cache for the projects locally and set the new pinned + /// status for the provided project path. + static Future updateProjectPinStatus(String path, bool isPinned) async { + try { + Directory _dir = await getApplicationSupportDirectory(); + + // Gets the existing cache so that we can alter it with the new pinned + // status. + List _cache = + await ProjectSearchUtils.getProjectsFromCache(_dir.path); + + // Projects cache structure: + // [ + // { + // "name": "Project 1", + // ... + // }, + // { + // "name": "Project 1", + // ... + // } + // ] + + List> _newCache = >[]; + + // Will find the project that matches the provided path and update the + // pinned status. + for (ProjectObject project in _cache) { + ProjectObject _newProject = ProjectObject( + name: project.name, + modDate: project.modDate, + path: project.path, + description: project.description, + pinned: project.path == path ? isPinned : project.pinned, + ); + + _newCache.add(_newProject.toJson()); + } + + // Now we will write the new cache to the file. + await File(ProjectSearchUtils.getProjectCachePath(_dir.path)) + .writeAsString(jsonEncode(_newCache)); + } catch (_, s) { + await logger.file(LogTypeTag.error, + 'Failed to update the pinned status for the project: $path :$_', + stackTraces: s); + } + } + /// If we have cache, we will use it to improve performance. After we send to /// the port listener, we will then fetch again to update the cache in the /// background. @@ -81,6 +133,12 @@ class ProjectServicesModel { List _projectsCache = await ProjectSearchUtils.getProjectsFromCache(_supportDir); + ProjectIsolateFetchResult _result = ProjectIsolateFetchResult( + projects: _projectsCache.where((ProjectObject e) => !e.pinned).toList(), + pinnedProjects: + _projectsCache.where((ProjectObject e) => e.pinned).toList(), + ); + ProjectCacheResult? _cache = await getProjectCache(_supportDir); // Check to see if we need to refetch again because of time interval or cache @@ -115,7 +173,7 @@ class ProjectServicesModel { logDir: Directory(_supportDir)); // Don't kill isolate. Will refetch with cache. - _port.send([_projectsCache, false, true]); + _port.send([_result, false, true]); List _projectsRefetch = await ProjectSearchUtils.getProjectsFromPath( @@ -134,25 +192,34 @@ class ProjectServicesModel { ), ); + ProjectIsolateFetchResult _refetchResult = ProjectIsolateFetchResult( + projects: + _projectsRefetch.where((ProjectObject e) => !e.pinned).toList(), + pinnedProjects: + _projectsRefetch.where((ProjectObject e) => e.pinned).toList(), + ); + // Kill isolate. Cache is now updated. - _port.send([_projectsRefetch, true, false]); + _port.send([_refetchResult, true, false]); + return; } else { await logger.file(LogTypeTag.info, 'Fetching projects from cache. Cache still valid.', logDir: Directory(_supportDir)); // Kill isolate. Cache is still valid. - _port.send([_projectsCache, true, false]); + _port.send([_result, true, false]); + return; } } else { // Kill isolate. - _port.send([_projectsCache, true, false]); + _port.send([_result, true, false]); + return; } - - return; } else { await logger.file( LogTypeTag.info, 'Fetching projects initially. No cache found.', logDir: Directory(_supportDir)); + List _projectsPaths = await ProjectSearchUtils.getProjectsFromPath( cache: await getProjectCache(_supportDir) ?? @@ -165,12 +232,29 @@ class ProjectServicesModel { supportDir: _supportDir, ); - _port.send([_projectsPaths, true, false]); + ProjectIsolateFetchResult _result = ProjectIsolateFetchResult( + projects: _projectsPaths.where((ProjectObject e) => !e.pinned).toList(), + pinnedProjects: + _projectsPaths.where((ProjectObject e) => e.pinned).toList(), + ); + + // Kill isolate + _port.send([_result, true, false]); return; } } } +class ProjectIsolateFetchResult { + final List pinnedProjects; + final List projects; + + const ProjectIsolateFetchResult({ + required this.pinnedProjects, + required this.projects, + }); +} + class ProjectCacheResult { final String? projectsPath; final int? refreshIntervals; diff --git a/lib/meta/views/tabs/sections/projects/projects.dart b/lib/meta/views/tabs/sections/projects/projects.dart index 4c8be166..87470574 100644 --- a/lib/meta/views/tabs/sections/projects/projects.dart +++ b/lib/meta/views/tabs/sections/projects/projects.dart @@ -1,4 +1,5 @@ // 🎯 Dart imports: +import 'dart:io'; import 'dart:isolate'; // 🐦 Flutter imports: @@ -18,9 +19,9 @@ import 'package:fluttermatic/components/widgets/ui/snackbar_tile.dart'; import 'package:fluttermatic/components/widgets/ui/spinner.dart'; import 'package:fluttermatic/core/models/projects.model.dart'; import 'package:fluttermatic/core/services/logs.dart'; +import 'package:fluttermatic/meta/utils/extract_pubspec.dart'; import 'package:fluttermatic/meta/utils/shared_pref.dart'; import 'package:fluttermatic/meta/views/tabs/components/horizontal_axis.dart'; -import 'package:fluttermatic/meta/views/tabs/home.dart'; import 'package:fluttermatic/meta/views/tabs/sections/projects/elements/project_tile.dart'; import 'package:fluttermatic/meta/views/tabs/sections/projects/models/projects.services.dart'; @@ -38,7 +39,9 @@ class _HomeProjectsSectionState extends State { bool _loadProjectsCalled = false; // Data - final List _projects = []; + final List _dartProjects = []; + final List _pinnedProjects = []; + final List _flutterProjects = []; final ReceivePort _loadProjectsPort = ReceivePort('FIND_PROJECTS_ISOLATE_PORT'); @@ -59,7 +62,7 @@ class _HomeProjectsSectionState extends State { supportDir: (await getApplicationSupportDirectory()).path, ); - Isolate _isolate = await Isolate.spawn( + Isolate _i = await Isolate.spawn( ProjectServicesModel.getProjectsIsolate, [ _loadProjectsPort.sendPort, @@ -85,16 +88,52 @@ class _HomeProjectsSectionState extends State { if (message is List) { setState(() { _projectsLoading = false; - _projects.clear(); - _projects.addAll(message.first); - if (message[2] == true) { - _reloadingFromCache = true; - } else { - _reloadingFromCache = false; + + // Clear the existing list of projects before adding the newly + // loaded ones. + _pinnedProjects.clear(); + _flutterProjects.clear(); + _dartProjects.clear(); + + // Adds the pinned projects + _pinnedProjects.addAll( + (message.first as ProjectIsolateFetchResult) + .pinnedProjects); + + // Will sort the Dart and Flutter projects. + List _dart = []; + List _flutter = []; + + // Sort + for (ProjectObject project + in (message.first as ProjectIsolateFetchResult).projects) { + try { + PubspecInfo _pubspec = extractPubspec( + lines: File(project.path + '\\pubspec.yaml') + .readAsLinesSync(), + path: project.path + '\\pubspec.yaml'); + + if (_pubspec.isFlutterProject) { + _flutter.add(project); + } else { + _dart.add(project); + } + } catch (_, s) { + logger.file(LogTypeTag.warning, + 'Failed to sort in project tabs projects: $_', + stackTraces: s); + } } + + // Add + _dartProjects.addAll(_dart); + _flutterProjects.addAll(_flutter); + + _reloadingFromCache = message[2] == true; }); + if (message[1] == true) { - _isolate.kill(); + _i.kill(); } } }); @@ -131,16 +170,7 @@ class _HomeProjectsSectionState extends State { width: 40, height: 40, child: const Icon(Icons.refresh_rounded, size: 20), - onPressed: () { - Navigator.pushReplacement( - context, - PageRouteBuilder( - pageBuilder: (_, __, ___) => - const HomeScreen(tab: HomeTab.projects), - transitionDuration: Duration.zero, - ), - ); - }, + onPressed: () => _loadProjects(true), ); } @@ -160,6 +190,7 @@ class _HomeProjectsSectionState extends State { @override Widget build(BuildContext context) { return Stack( + fit: StackFit.expand, children: [ if (!SharedPref().pref.containsKey(SPConst.projectsPath)) Center( @@ -191,14 +222,7 @@ class _HomeProjectsSectionState extends State { goToPage: SettingsPage.projects, ), ); - await Navigator.pushReplacement( - context, - PageRouteBuilder>( - pageBuilder: (_, __, ___) => - const HomeScreen(tab: HomeTab.projects), - transitionDuration: Duration.zero, - ), - ); + await _loadProjects(true); }, ), HSeparators.small(), @@ -211,7 +235,9 @@ class _HomeProjectsSectionState extends State { ) else if (_projectsLoading) const Center(child: Spinner(thickness: 2)) - else if (_projects.isEmpty) + else if (_pinnedProjects.isEmpty && + _flutterProjects.isEmpty && + _dartProjects.isEmpty) Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -219,7 +245,7 @@ class _HomeProjectsSectionState extends State { const Icon(Icons.info_outline_rounded, size: 40), VSeparators.large(), const Text( - 'No projects found. Please check the path you have added.', + 'No projects found. Please check the path you\nhave added.', textAlign: TextAlign.center, ), VSeparators.large(), @@ -237,14 +263,7 @@ class _HomeProjectsSectionState extends State { goToPage: SettingsPage.projects, ), ); - await Navigator.pushReplacement( - context, - PageRouteBuilder>( - pageBuilder: (_, __, ___) => - const HomeScreen(tab: HomeTab.projects), - transitionDuration: Duration.zero, - ), - ); + await _loadProjects(true); }, ), HSeparators.small(), @@ -258,25 +277,157 @@ class _HomeProjectsSectionState extends State { SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(15), - child: HorizontalAxisView( - title: 'Projects', - isVertical: true, - action: SquareButton( - size: 20, - tooltip: 'Reload', - color: Colors.transparent, - hoverColor: Colors.transparent, - icon: const Icon(Icons.refresh_rounded, size: 15), - onPressed: () => _loadProjects(true), - ), - content: _projects.map((ProjectObject e) { - return ProjectInfoTile( - name: e.name, - description: e.description, - modDate: e.modDate, - path: e.path, - ); - }).toList(), + child: Column( + children: [ + if (_pinnedProjects.isNotEmpty) + HorizontalAxisView( + title: 'Pinned Projects', + isVertical: true, + canCollapse: true, + isCollapsedInitially: SharedPref() + .pref + .getBool(SPConst.pinnedProjectsCollapsed) ?? + false, + onCollapse: (bool isCollapsed) { + SharedPref().pref.setBool( + SPConst.pinnedProjectsCollapsed, isCollapsed); + }, + action: SquareButton( + size: 20, + tooltip: 'Reload', + color: Colors.transparent, + hoverColor: Colors.transparent, + icon: const Icon(Icons.refresh_rounded, size: 15), + onPressed: () => _loadProjects(true), + ), + content: _pinnedProjects.map((ProjectObject e) { + return ProjectInfoTile( + project: e, + onPinChanged: () { + // Get the information about this project whether + // it is Flutter or Dart so we can add it to the + // correct list. + PubspecInfo _pubspec = extractPubspec( + lines: File(e.path + '\\pubspec.yaml') + .readAsLinesSync(), + path: e.path + '\\pubspec.yaml'); + + // Remove it from the pinned list and add it to + // the Flutter list. + ProjectObject _project = ProjectObject( + name: e.name, + modDate: e.modDate, + path: e.path, + description: e.description, + pinned: false, + ); + + setState(() { + _pinnedProjects.remove(e); + + if (_pubspec.isFlutterProject) { + _flutterProjects.add(_project); + } else { + _dartProjects.add(_project); + } + }); + }, + ); + }).toList(), + ), + if (_flutterProjects.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 15), + child: HorizontalAxisView( + title: 'Flutter Projects', + isVertical: true, + canCollapse: true, + isCollapsedInitially: SharedPref() + .pref + .getBool(SPConst.flutterProjectsCollapsed) ?? + false, + onCollapse: (bool isCollapsed) { + SharedPref().pref.setBool( + SPConst.flutterProjectsCollapsed, isCollapsed); + }, + action: SquareButton( + size: 20, + tooltip: 'Reload', + color: Colors.transparent, + hoverColor: Colors.transparent, + icon: const Icon(Icons.refresh_rounded, size: 15), + onPressed: () => _loadProjects(true), + ), + content: _flutterProjects.map((ProjectObject e) { + return ProjectInfoTile( + project: e, + onPinChanged: () { + // Remove it from the flutter projects list and + // add it to the pinned list. + ProjectObject _project = ProjectObject( + name: e.name, + modDate: e.modDate, + path: e.path, + description: e.description, + pinned: true, + ); + + setState(() { + _flutterProjects.remove(e); + _pinnedProjects.add(_project); + }); + }, + ); + }).toList(), + ), + ), + if (_dartProjects.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 15), + child: HorizontalAxisView( + title: 'Dart Projects', + canCollapse: true, + isVertical: true, + isCollapsedInitially: SharedPref() + .pref + .getBool(SPConst.dartProjectsCollapsed) ?? + false, + onCollapse: (bool isCollapsed) { + SharedPref().pref.setBool( + SPConst.dartProjectsCollapsed, isCollapsed); + }, + action: SquareButton( + size: 20, + tooltip: 'Reload', + color: Colors.transparent, + hoverColor: Colors.transparent, + icon: const Icon(Icons.refresh_rounded, size: 15), + onPressed: () => _loadProjects(true), + ), + content: _dartProjects.map((ProjectObject e) { + return ProjectInfoTile( + project: e, + onPinChanged: () { + // Remove it from the flutter projects list and + // add it to the pinned list. + ProjectObject _project = ProjectObject( + name: e.name, + modDate: e.modDate, + path: e.path, + description: e.description, + pinned: true, + ); + + setState(() { + _dartProjects.remove(e); + _pinnedProjects.add(_project); + }); + }, + ); + }).toList(), + ), + ), + ], ), ), ), diff --git a/lib/meta/views/tabs/sections/pub/elements/pub_tile.dart b/lib/meta/views/tabs/sections/pub/elements/pub_tile.dart index 97ade5e7..e7aac4c1 100644 --- a/lib/meta/views/tabs/sections/pub/elements/pub_tile.dart +++ b/lib/meta/views/tabs/sections/pub/elements/pub_tile.dart @@ -231,7 +231,10 @@ class _PubPkgTileState extends State { ), HSeparators.xSmall(), Text( - (widget.data?.metrics?.score.maxPoints ?? 0) + NumberFormat.compact() + .format((widget.data?.metrics?.score + .popularityScore ?? + 0) * 100) .toString() + '%', style: TextStyle( diff --git a/lib/meta/views/tabs/sections/pub/models/package_dialog.dart b/lib/meta/views/tabs/sections/pub/models/package_dialog.dart index 0ed56599..db4ba94d 100644 --- a/lib/meta/views/tabs/sections/pub/models/package_dialog.dart +++ b/lib/meta/views/tabs/sections/pub/models/package_dialog.dart @@ -57,7 +57,7 @@ class _PubPackageDialogState extends State { .packageOptions(widget.pkgInfo.name) .then((PackageOptions value) => setState(() => _pkgOptions = value)); - Isolate _isolate = + Isolate _i = await Isolate.spawn(PkgViewData.getPkgReadMeIsolate, [ _readMePort.sendPort, widget.pkgInfo.name, @@ -82,6 +82,7 @@ class _PubPackageDialogState extends State { ); _readMePort.listen((dynamic _readMe) { + _i.kill(); if (mounted) { setState(() { if (_readMe is List) { @@ -92,7 +93,6 @@ class _PubPackageDialogState extends State { _shouldDisplay = true; }); } - _isolate.kill(); }); } catch (_, s) { await logger.file(LogTypeTag.error, @@ -139,400 +139,392 @@ class _PubPackageDialogState extends State { type: InformationType.warning, ), VSeparators.normal(), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ConstrainedBox( - constraints: BoxConstraints( - minWidth: _size.width > 1000 ? 700 : 450, - maxWidth: _size.width > 1000 ? 700 : 450, - maxHeight: 450, - ), - child: Builder( - builder: (BuildContext context) { - try { - if (!_hasReadme) { - return informationWidget( - 'Couldn\'t find a README.md file for this package. Check it out in pub.dev', - type: InformationType.warning, - ); - } else { - if (!_shouldDisplay) { - return Shimmer.fromColors( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 15, - runSpacing: 15, - crossAxisAlignment: WrapCrossAlignment.start, - runAlignment: WrapAlignment.start, - alignment: WrapAlignment.start, - children: const [ - RoundContainer( - child: SizedBox.shrink(), - width: 150, - height: 30, - radius: 50, - ), - RoundContainer( - child: SizedBox.shrink(), - width: 100, - height: 30, - radius: 50, - ), - RoundContainer( - child: SizedBox.shrink(), - width: 120, - height: 30, - radius: 50, - ), - RoundContainer( - child: SizedBox.shrink(), - width: 80, - height: 30, - radius: 50, - ), - ], - ), - VSeparators.normal(), - const Expanded(child: Center(child: Spinner())), - ], - ), + IgnorePointer( + ignoring: !_shouldDisplay, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minWidth: _size.width > 1000 ? 700 : 450, + maxWidth: _size.width > 1000 ? 700 : 450, + maxHeight: 450, + ), + child: Builder( + builder: (BuildContext context) { + try { + if (!_hasReadme) { + return informationWidget( + 'Couldn\'t find a README.md file for this package. Check it out in pub.dev', + type: InformationType.warning, ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( + } else { + if (!_shouldDisplay) { + return Shimmer.fromColors( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), - child: ColorFiltered( - colorFilter: ColorFilter.mode( - Theme.of(context).isDarkTheme - ? (widget.pkgInfo.metrics! - .scorecard.derivedTags - .contains('is:null-safe') - ? kGreenColor - : kYellowColor) - : (widget.pkgInfo.metrics! - .scorecard.derivedTags - .contains('is:null-safe') - ? kGreenColor - : Colors.redAccent), - BlendMode.srcATop), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - widget.pkgInfo.metrics!.scorecard - .derivedTags - .contains('is:null-safe') - ? Icons.done_all_rounded - : Icons - .do_not_disturb_alt_rounded, - size: 13, - ), - HSeparators.xSmall(), - Text(widget.pkgInfo.metrics!.scorecard - .derivedTags - .contains('is:null-safe') - ? 'Null safe' - : 'Not null safe'), - ], + Wrap( + spacing: 15, + runSpacing: 15, + crossAxisAlignment: + WrapCrossAlignment.start, + runAlignment: WrapAlignment.start, + alignment: WrapAlignment.start, + children: const [ + RoundContainer( + child: SizedBox.shrink(), + width: 150, + height: 30, + radius: 50, ), - ), - radius: 50, - ), - HSeparators.xSmall(), - ...[ - 'platform:android', - 'platform:ios', - 'platform:windows', - 'platform:linux', - 'platform:macos', - 'platform:web', - ].map( - (String e) { - if (!widget.pkgInfo.metrics!.scorecard - .derivedTags - .contains(e)) { - return const SizedBox.shrink(); - } - return Padding( - padding: - const EdgeInsets.only(right: 5), - child: RoundContainer( - color: - Colors.blueGrey.withOpacity(0.2), - child: Text( - e.substring(e.indexOf(':') + 1)), - ), - ); - }, + RoundContainer( + child: SizedBox.shrink(), + width: 100, + height: 30, + radius: 50, + ), + RoundContainer( + child: SizedBox.shrink(), + width: 120, + height: 30, + radius: 50, + ), + RoundContainer( + child: SizedBox.shrink(), + width: 80, + height: 30, + radius: 50, + ), + ], ), + VSeparators.normal(), + const Expanded( + child: Center(child: Spinner())), ], ), - ), - VSeparators.xSmall(), - Expanded( - child: SingleChildScrollView( - child: ListView.builder( - shrinkWrap: true, - itemCount: _data!.length, - itemBuilder: (_, int i) { - if (_data![i].startsWith('code:')) { - return MarkdownBlock( - data: _data![i].substring(5)); - } else { - return html.Html( - data: _data![i].substring(5), - onImageError: (_, __) async { - await logger.file(LogTypeTag.error, - 'Failed to load README.md image for package ${widget.pkgInfo}', - stackTraces: __); - }, - onMathError: (_, __, ___) { - return const Text( - 'Failed to load due to math error.', - style: TextStyle( - color: AppTheme.errorColor), - ); - }, - onLinkTap: - (String? url, _, __, ___) async { - if (url != null) { - bool _canLaunch = - await canLaunch(url); - if (_canLaunch) { - await launch(url); - } - } - }, - ); - } - }, - ), - ), - ), - ], - ); - } - } catch (_, s) { - logger.file(LogTypeTag.error, - 'Failed to load README.md for package ${widget.pkgInfo.name}', - stackTraces: s); - return informationWidget( - 'We found a README.md for this package. However, we are not able to display if for you at the moment.', - type: InformationType.warning, - ); - } - }, - ), - ), - HSeparators.normal(), - Expanded( - child: Builder( - builder: (BuildContext context) { - return Shimmer.fromColors( - enabled: !_shouldDisplay, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Row( + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: RoundContainer( - color: Colors.blueGrey[ - Theme.of(context).isDarkTheme - ? 800 - : 100], - child: Column( - children: [ - Text(widget - .pkgInfo.metrics!.score.grantedPoints - .toString()), - VSeparators.small(), - const Text('Pub Points'), - ], - ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + RoundContainer( + child: ColorFiltered( + colorFilter: ColorFilter.mode( + Theme.of(context).isDarkTheme + ? (widget.pkgInfo.metrics! + .scorecard.derivedTags + .contains( + 'is:null-safe') + ? kGreenColor + : kYellowColor) + : (widget.pkgInfo.metrics! + .scorecard.derivedTags + .contains( + 'is:null-safe') + ? kGreenColor + : Colors.redAccent), + BlendMode.srcATop), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + widget.pkgInfo.metrics!.scorecard + .derivedTags + .contains('is:null-safe') + ? Icons.done_all_rounded + : Icons + .do_not_disturb_alt_rounded, + size: 13, + ), + HSeparators.xSmall(), + Text(widget.pkgInfo.metrics! + .scorecard.derivedTags + .contains('is:null-safe') + ? 'Null safe' + : 'Not null safe'), + ], + ), + ), + radius: 50, + ), + HSeparators.xSmall(), + ...[ + 'platform:android', + 'platform:ios', + 'platform:windows', + 'platform:linux', + 'platform:macos', + 'platform:web', + ].map( + (String e) { + if (!widget.pkgInfo.metrics!.scorecard + .derivedTags + .contains(e)) { + return const SizedBox.shrink(); + } + return Padding( + padding: + const EdgeInsets.only(right: 5), + child: RoundContainer( + child: Text(e + .substring(e.indexOf(':') + 1)), + ), + ); + }, + ), + ], ), ), - HSeparators.small(), + VSeparators.xSmall(), Expanded( - child: RoundContainer( - color: Colors.blueGrey[ - Theme.of(context).isDarkTheme - ? 800 - : 100], - child: Column( - children: [ - Text(NumberFormat.percentPattern().format( - widget.pkgInfo.metrics!.score - .popularityScore)), - VSeparators.small(), - const Text('Popularity'), - ], + child: SingleChildScrollView( + child: ListView.builder( + shrinkWrap: true, + itemCount: _data!.length, + itemBuilder: (_, int i) { + if (_data![i].startsWith('code:')) { + return MarkdownBlock( + data: _data![i].substring(5)); + } else { + return html.Html( + data: _data![i].substring(5), + onImageError: (_, __) async { + await logger.file(LogTypeTag.error, + 'Failed to load README.md image for package ${widget.pkgInfo}', + stackTraces: __); + }, + onMathError: (_, __, ___) { + return const Text( + 'Failed to load due to math error.', + style: TextStyle( + color: AppTheme.errorColor), + ); + }, + onLinkTap: + (String? url, _, __, ___) async { + if (url != null) { + bool _canLaunch = + await canLaunch(url); + if (_canLaunch) { + await launch(url); + } + } + }, + ); + } + }, ), ), ), ], - ), - VSeparators.normal(), - RoundContainer( - width: double.infinity, - color: Colors.blueGrey[ - Theme.of(context).isDarkTheme ? 800 : 100], - child: Row( + ); + } + } catch (_, s) { + logger.file(LogTypeTag.error, + 'Failed to load README.md for package ${widget.pkgInfo.name}', + stackTraces: s); + return informationWidget( + 'We found a README.md for this package. However, we are not able to display if for you at the moment.', + type: InformationType.warning, + ); + } + }, + ), + ), + HSeparators.normal(), + Expanded( + child: Builder( + builder: (BuildContext context) { + return Shimmer.fromColors( + enabled: !_shouldDisplay, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( children: [ Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text(NumberFormat.compact().format(widget - .pkgInfo.metrics!.score.likeCount)), - VSeparators.small(), - const Text('Package Likes'), - ], + child: RoundContainer( + child: Column( + children: [ + Text(widget.pkgInfo.metrics!.score + .grantedPoints + .toString()), + VSeparators.small(), + const Text('Pub Points'), + ], + ), ), ), HSeparators.small(), - // Shows a like button for the user to like the - // package. - IconButton( - onPressed: () async { - ScaffoldMessenger.of(context) - .clearSnackBars(); - // Show snackbar to ask the user to sign - // in to like the package. - ScaffoldMessenger.of(context).showSnackBar( - snackBarTile( - context, - 'Please sign in to like and view all of your package inventory.', - type: SnackBarType.warning, - ), - ); - }, - icon: Icon( - Icons.thumb_up_outlined, - color: Theme.of(context).isDarkTheme - ? Colors.white - : Colors.black, + Expanded( + child: RoundContainer( + child: Column( + children: [ + Text(NumberFormat.percentPattern() + .format(widget.pkgInfo.metrics! + .score.popularityScore)), + VSeparators.small(), + const Text('Popularity'), + ], + ), ), ), ], ), - ), - VSeparators.normal(), - RoundContainer( - width: double.infinity, - color: Colors.blueGrey[ - Theme.of(context).isDarkTheme ? 800 : 100], - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.pkgInfo.info.description), - VSeparators.small(), - InkWell( - onTap: () { - Clipboard.setData(ClipboardData( - text: widget.pkgInfo.name + - ': ^${widget.pkgInfo.info.version}')); - ScaffoldMessenger.of(context) - .clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - snackBarTile( - context, - 'Dependency has been copied to your clipboard.', - type: SnackBarType.done, - ), - ); - }, - child: Row( - children: [ - Expanded( - child: Text( - 'Version ' + - widget.pkgInfo.info.version, - style: const TextStyle( - fontWeight: FontWeight.bold), + VSeparators.normal(), + RoundContainer( + width: double.infinity, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text(NumberFormat.compact().format( + widget.pkgInfo.metrics!.score + .likeCount)), + VSeparators.small(), + const Text('Package Likes'), + ], + ), + ), + HSeparators.small(), + // Shows a like button for the user to like the + // package. + IconButton( + onPressed: () async { + ScaffoldMessenger.of(context) + .clearSnackBars(); + // Show snackbar to ask the user to sign + // in to like the package. + ScaffoldMessenger.of(context) + .showSnackBar( + snackBarTile( + context, + 'Please sign in to like and view all of your package inventory.', + type: SnackBarType.warning, ), - ), - HSeparators.small(), - const Icon(Icons.copy_rounded, size: 15), - ], + ); + }, + icon: Icon( + Icons.thumb_up_outlined, + color: Theme.of(context).isDarkTheme + ? Colors.white + : Colors.black, + ), ), - ), - ], + ], + ), ), - ), - VSeparators.normal(), - RoundContainer( - width: double.infinity, - color: Colors.blueGrey[ - Theme.of(context).isDarkTheme ? 800 : 100], - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - widget.pkgInfo.publisher.publisherId ?? - 'Unknown Publisher', + VSeparators.normal(), + RoundContainer( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.pkgInfo.info.description), + VSeparators.small(), + InkWell( + onTap: () { + Clipboard.setData(ClipboardData( + text: widget.pkgInfo.name + + ': ^${widget.pkgInfo.info.version}')); + ScaffoldMessenger.of(context) + .clearSnackBars(); + ScaffoldMessenger.of(context) + .showSnackBar( + snackBarTile( + context, + 'Dependency has been copied to your clipboard.', + type: SnackBarType.done, + ), + ); + }, + child: Row( + children: [ + Expanded( + child: Text( + 'Version ' + + widget.pkgInfo.info.version, + style: const TextStyle( + fontWeight: FontWeight.bold), + ), + ), + HSeparators.small(), + const Icon(Icons.copy_rounded, + size: 15), + ], + ), ), - ), - HSeparators.small(), - // Shows verified icon if the publisher is verified. - // TODO: Check if the publisher is verified first. - if (1 == 2) - const Tooltip( - message: 'Verified Publisher', - child: Icon(Icons.verified_rounded, - size: 18, color: kGreenColor), + ], + ), + ), + VSeparators.normal(), + RoundContainer( + width: double.infinity, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + widget.pkgInfo.publisher.publisherId ?? + 'Unknown Publisher', + ), ), - ], + HSeparators.small(), + // Shows verified icon if the publisher is verified. + // TODO: Check if the publisher is verified first. + if (1 == 2) + const Tooltip( + message: 'Verified Publisher', + child: Icon(Icons.verified_rounded, + size: 18, color: kGreenColor), + ), + ], + ), ), - ), - VSeparators.normal(), - RectangleButton( - color: Colors.blueGrey[ - Theme.of(context).isDarkTheme ? 800 : 100], - width: double.infinity, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'Open in Pub.dev', - style: TextStyle( + VSeparators.normal(), + RectangleButton( + width: double.infinity, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Open in Pub.dev', + style: TextStyle( + color: Theme.of(context).isDarkTheme + ? Colors.white + : Colors.black, + ), + ), + HSeparators.small(), + Icon( + Icons.open_in_new_rounded, + size: 15, color: Theme.of(context).isDarkTheme ? Colors.white : Colors.black, ), - ), - HSeparators.small(), - Icon( - Icons.open_in_new_rounded, - size: 15, - color: Theme.of(context).isDarkTheme - ? Colors.white - : Colors.black, - ), - ], + ], + ), + onPressed: () => launch(widget.pkgInfo.info.url), ), - onPressed: () => launch(widget.pkgInfo.info.url), - ), - ], - ), - ); - }, + ], + ), + ); + }, + ), ), - ), - ], + ], + ), ), ], ), diff --git a/lib/meta/views/tabs/sections/pub/models/pkg_data.dart b/lib/meta/views/tabs/sections/pub/models/pkg_data.dart index e87e02c5..f56f54a3 100644 --- a/lib/meta/views/tabs/sections/pub/models/pkg_data.dart +++ b/lib/meta/views/tabs/sections/pub/models/pkg_data.dart @@ -5,7 +5,6 @@ import 'dart:isolate'; // 📦 Package imports: import 'package:http/http.dart' as http; -import 'package:path_provider/path_provider.dart'; import 'package:pub_api_client/pub_api_client.dart'; // 🌎 Project imports: @@ -34,15 +33,22 @@ class PkgViewData { } static PkgViewData fromJson(Map json) { - return PkgViewData( - name: json['name'] as String, - info: PubPackage.fromJson(json['info'] as Map), - metrics: json['metrics'] != null - ? PackageMetrics.fromJson(json['metrics'] as Map) - : null, - publisher: - PackagePublisher.fromJson(json['publisher'] as Map), - ); + try { + return PkgViewData( + name: json['name'] as String, + info: PubPackage.fromJson(json['info'] as Map), + metrics: json['metrics'] != null + ? PackageMetrics.fromJson(json['metrics'] as Map) + : null, + publisher: PackagePublisher.fromJson( + json['publisher'] as Map), + ); + } catch (_, s) { + logger.file( + LogTypeTag.error, 'Failed to parse json package: $_, json: $json', + stackTraces: s); + rethrow; + } } /// Returns the package's README by crawling the package's homepage. @@ -142,11 +148,13 @@ class PkgViewData { } _port.send(_readMe); + return; } catch (_, s) { await logger.file( LogTypeTag.error, 'Failed to fetch README for package: $_pkgName', stackTraces: s); _port.send(false); + return; } } @@ -165,10 +173,12 @@ class PkgViewData { /// Once we get the JSON, we will store it the first time and then use it to /// filter the results as the user types. /// This is done to avoid making too many requests to the pub API. - static Future getInitialPackages() async { + static Future getPackagesIsolate(List data) async { + SendPort _port = data[0]; + String _path = data[1]; + List _pubPackages = []; - Directory _dir = await getApplicationSupportDirectory(); - File _cache = File(_dir.path + '\\cache\\pub_cache.map.json'); + File _cache = File(_path + '\\cache\\pub_cache.map.json'); try { if (await _cache.exists()) { @@ -177,52 +187,56 @@ class PkgViewData { DateTime _time = DateTime.parse(_cacheFile['timestamp']); - // Make sure it hasn't been more than a day since the last time we updated + await logger.file(LogTypeTag.info, + 'Fetching pub packages from cache after ${_time.difference(DateTime.now()).inMinutes.abs()} minute(s) since stored.', + logDir: Directory(_path)); + + List _pendingPkg = + (_cacheFile['packages'] as List).toList(); + + _pendingPkg = _pendingPkg.length > _loadCount + ? _pendingPkg.sublist(0, _loadCount) + : _pendingPkg; + + for (dynamic pkg in _pendingPkg) { + File _pkg = File(_path + '\\cache\\packages\\$pkg.json'); + + if (await _pkg.exists()) { + String _pkgInfo = await _pkg.readAsString(); + _pubPackages.add(PkgViewData.fromJson(jsonDecode(_pkgInfo))); + } else { + await logger.file( + LogTypeTag.error, + 'Failed to find package: $pkg in cache, even though declared in cache map.', + logDir: Directory(_path), + ); + } + } + + // Make sure it hasn't been more than an hour since the last time we updated // the cache. if (_time.difference(DateTime.now()).inMinutes.abs() < _cacheTimeout) { + GetPkgResponseModel _response = GetPkgResponseModel( + response: GetPkgResponse.done, packages: _pubPackages); + + // Kill isolate + _port.send([_response, true, false]); + return; + } else { + GetPkgResponseModel _response = GetPkgResponseModel( + response: GetPkgResponse.pending, packages: _pubPackages); + + // Don't kill isolate. Will refetch with cache. + _port.send([_response, false, true]); + await logger.file( LogTypeTag.info, - 'Fetching pub packages from cache after ' - '${_time.difference(DateTime.now()).inMinutes.abs()} minute(s) since stored.', + 'Refetching pub packages cache -- cache exists but is stale/expired.', + logDir: Directory(_path), ); - List _pendingPkg = - (_cacheFile['packages'] as List).toList(); - - _pendingPkg = _pendingPkg.length > _loadCount - ? _pendingPkg.sublist(0, _loadCount) - : _pendingPkg; - - for (dynamic pkg in _pendingPkg) { - File _pkg = File(_dir.path + '\\cache\\packages\\$pkg.json'); - - if (await _pkg.exists()) { - String _pkgInfo = await _pkg.readAsString(); - _pubPackages.add(PkgViewData.fromJson(jsonDecode(_pkgInfo))); - } else { - await logger.file( - LogTypeTag.error, - 'Failed to find package: $pkg in cache, even though declared in cache map.', - ); - } - } - - return GetPkgResponseModel( - response: GetPkgResponse.done, packages: _pubPackages); - } else { - await logger.file(LogTypeTag.info, - 'Refetching pub packages cache -- cache exists but is stale/expired.'); } } - if (_pubPackages.isNotEmpty) { - return GetPkgResponseModel( - response: GetPkgResponse.done, packages: _pubPackages); - } - - // http.Response _result = await http - // .get(Uri.parse('https://pub.dev/api/package-name-completion-data')) - // .onError((_, __) => http.Response('', 300)); - SearchResults _search1 = await PubClient().search('', page: 1); SearchResults _search2 = await PubClient().search('', page: 2); @@ -253,13 +267,13 @@ class PkgViewData { ); // Create the directory for projects if it doesn't exist. - if (!Directory(_dir.path + '\\cache\\packages').existsSync()) { - Directory(_dir.path + '\\cache\\packages').createSync(recursive: true); + if (!Directory(_path + '\\cache\\packages').existsSync()) { + Directory(_path + '\\cache\\packages').createSync(recursive: true); } // Save it locally as cache for later use. for (PkgViewData e in _pubPackages) { - File _pkg = File(_dir.path + '\\cache\\packages\\${e.name}.json'); + File _pkg = File(_path + '\\cache\\packages\\${e.name}.json'); if (await _pkg.exists()) { await _pkg.delete(); @@ -268,15 +282,25 @@ class PkgViewData { await _pkg.writeAsString(jsonEncode(e.toJson())); } await logger.file(LogTypeTag.info, - 'Added packages ${_pubPackages.map((PkgViewData e) => e.name + ', ')} to cache.'); + 'Added packages ${_pubPackages.map((PkgViewData e) => e.name + ', ')} to cache.', + logDir: Directory(_path)); - return GetPkgResponseModel( + GetPkgResponseModel _response = GetPkgResponseModel( response: GetPkgResponse.done, packages: _pubPackages); + + // Kill isolate + _port.send([_response, true, false]); + return; } catch (_, s) { await logger.file(LogTypeTag.error, 'Failed to fetch pub packages.', - stackTraces: s); - return GetPkgResponseModel( + stackTraces: s, logDir: Directory(_path)); + + GetPkgResponseModel _response = GetPkgResponseModel( response: GetPkgResponse.error, packages: _pubPackages); + + // Kill isolate + _port.send([_response, true, false]); + return; } } } @@ -294,5 +318,6 @@ class GetPkgResponseModel { enum GetPkgResponse { done, error, + pending, network, } diff --git a/lib/meta/views/tabs/sections/pub/pub.dart b/lib/meta/views/tabs/sections/pub/pub.dart index 65d0b1c0..a8d0b1ab 100644 --- a/lib/meta/views/tabs/sections/pub/pub.dart +++ b/lib/meta/views/tabs/sections/pub/pub.dart @@ -1,14 +1,20 @@ +// 🎯 Dart imports: +import 'dart:isolate'; + // 🐦 Flutter imports: import 'package:flutter/material.dart'; // 📦 Package imports: import 'package:flutter_svg/flutter_svg.dart'; +import 'package:path_provider/path_provider.dart'; // 🌎 Project imports: import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/components/widgets/buttons/rectangle_button.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; import 'package:fluttermatic/components/widgets/ui/snackbar_tile.dart'; +import 'package:fluttermatic/components/widgets/ui/spinner.dart'; +import 'package:fluttermatic/core/services/logs.dart'; import 'package:fluttermatic/meta/utils/app_theme.dart'; import 'package:fluttermatic/meta/views/tabs/components/horizontal_axis.dart'; import 'package:fluttermatic/meta/views/tabs/sections/pub/elements/pub_tile.dart'; @@ -22,10 +28,16 @@ class HomePubSection extends StatefulWidget { } class _HomePubSectionState extends State { - final List _pubPackages = []; - + // Utils bool _errorPage = false; bool _loadingPackages = true; + bool _loadPubCalled = false; + bool _reloadingFromCache = false; + + // Data + final List _pubPackages = []; + final ReceivePort _loadPubPackagesPort = + ReceivePort('GET_PUB_PACKAGES_ISOLATE_PORT'); // Will get the JSON from this URL: https://pub.dev/api/package-name-completion-data // This JSON will contain the list of all pub packages that are available. @@ -58,36 +70,75 @@ class _HomePubSectionState extends State { } }); - GetPkgResponseModel _result = await PkgViewData.getInitialPackages(); - - if (mounted) { - switch (_result.response) { - case GetPkgResponse.done: - setState(() => _pubPackages.addAll(_result.packages)); - break; - case GetPkgResponse.error: - setState(() { - _errorPage = true; - _pubPackages.clear(); - }); - break; - case GetPkgResponse.network: - setState(() { - _errorPage = false; - _pubPackages.clear(); - }); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - snackBarTile( - context, - 'There appears to be a problem with the network. Please check your connection try again.', - type: SnackBarType.error, - ), - ); - break; - } + // We want to get the pub cache first to show in the meantime as we are + // making a request to the pub API to fetch the latest data. + Isolate _i = await Isolate.spawn( + PkgViewData.getPackagesIsolate, + [ + _loadPubPackagesPort.sendPort, + (await getApplicationSupportDirectory()).path, + ], + ).timeout(const Duration(minutes: 2)).onError((_, StackTrace s) async { + await logger.file(LogTypeTag.error, 'Failed to get packages: $_', + stackTraces: s); + + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + snackBarTile(context, 'Couldn\'t get the pub packages.', + type: SnackBarType.error), + ); + + return Isolate.current; + }); - setState(() => _loadingPackages = false); + if (!_loadPubCalled) { + _loadPubPackagesPort.listen((dynamic message) { + if (mounted) { + setState(() => _loadPubCalled = true); + if (message is List && mounted) { + setState(() { + _loadingPackages = false; + switch ((message.first as GetPkgResponseModel).response) { + case GetPkgResponse.done: + _pubPackages.clear(); + _pubPackages + .addAll((message.first as GetPkgResponseModel).packages); + break; + case GetPkgResponse.error: + setState(() { + _errorPage = true; + _pubPackages.clear(); + }); + break; + case GetPkgResponse.pending: + _pubPackages.clear(); + _pubPackages + .addAll((message.first as GetPkgResponseModel).packages); + _reloadingFromCache = true; + break; + case GetPkgResponse.network: + _errorPage = false; + _pubPackages.clear(); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + snackBarTile( + context, + 'There appears to be a problem with the network. Please check your connection try again.', + type: SnackBarType.error, + ), + ); + break; + } + _reloadingFromCache = message[2] == true; + }); + + // No more expected responses, we will kill the isolate. + if (message[1] == true) { + _i.kill(); + } + } + } + }); } } @@ -104,93 +155,113 @@ class _HomePubSectionState extends State { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Stack( children: [ - if (_errorPage) - Expanded( - child: Center( - child: SizedBox( - width: 300, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset(Assets.error), - VSeparators.normal(), - const Text( - 'Something went wrong. Maybe check your internet connection and try again.', - textAlign: TextAlign.center, - ), - VSeparators.large(), - RectangleButton( - child: const Text('Retry'), - onPressed: _getInitialPackages, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_errorPage) + Expanded( + child: Center( + child: SizedBox( + width: 300, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(Assets.error), + VSeparators.normal(), + const Text( + 'Something went wrong. Maybe check your internet connection and try again.', + textAlign: TextAlign.center, + ), + VSeparators.large(), + RectangleButton( + child: const Text('Retry'), + onPressed: _getInitialPackages, + ), + ], ), - ], - ), - ), - ), - ) - else if (_loadingPackages) - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(15), - child: HorizontalAxisView( - title: 'Favorites & Popular Packages', - isVertical: true, - // Creates a empty list of packages that will be filled later. - content: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - .map((int e) => const PubPkgTile(data: null)) - .toList(), + ), ), - ), - ), - ) - else - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - HorizontalAxisView( + ) + else if (_loadingPackages) + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(15), + child: HorizontalAxisView( title: 'Favorites & Popular Packages', isVertical: true, - content: _pubPackages - .map((PkgViewData e) => PubPkgTile(data: e)) + // Creates a empty list of packages that will be filled later. + content: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + .map((int e) => const PubPkgTile(data: null)) .toList(), ), - VSeparators.large(), - RoundContainer( - borderWith: 2, - padding: const EdgeInsets.all(20), - borderColor: Colors.blueGrey.withOpacity(0.2), - width: double.infinity, - child: Row( - children: [ - SvgPicture.asset( - Assets.package, - height: 25, - color: Theme.of(context).isDarkTheme - ? Colors.white - : Colors.black, - ), - HSeparators.small(), - const Expanded( - child: Text( - 'Try searching for the package that you are looking for.', - style: TextStyle(fontSize: 20), - ), + ), + ), + ) + else + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HorizontalAxisView( + title: 'Favorites & Popular Packages', + isVertical: true, + content: _pubPackages + .map((PkgViewData e) => PubPkgTile(data: e)) + .toList(), + ), + VSeparators.large(), + RoundContainer( + borderWith: 2, + padding: const EdgeInsets.all(20), + borderColor: Colors.blueGrey.withOpacity(0.2), + width: double.infinity, + child: Row( + children: [ + SvgPicture.asset( + Assets.package, + height: 25, + color: Theme.of(context).isDarkTheme + ? Colors.white + : Colors.black, + ), + HSeparators.small(), + const Expanded( + child: Text( + 'Try searching for the package that you are looking for.', + style: TextStyle(fontSize: 20), + ), + ), + ], ), - ], - ), + ), + VSeparators.large(), + ], ), - VSeparators.large(), - ], + ), ), ), + ], + ), + if (_reloadingFromCache) + Positioned( + bottom: 20, + right: 20, + child: Tooltip( + message: 'Searching for new pub packages...', + child: RoundContainer( + borderWith: 2, + borderColor: Colors.blueGrey.withOpacity(0.5), + child: const Spinner(thickness: 2), + height: 40, + width: 40, + radius: 60, + ), ), ), ], diff --git a/lib/meta/views/tabs/sections/workflows/elements/tile.dart b/lib/meta/views/tabs/sections/workflows/elements/tile.dart index 100d2d20..3ca21758 100644 --- a/lib/meta/views/tabs/sections/workflows/elements/tile.dart +++ b/lib/meta/views/tabs/sections/workflows/elements/tile.dart @@ -12,6 +12,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/components/widgets/buttons/rectangle_button.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; +import 'package:fluttermatic/meta/views/workflows/actions.dart'; import 'package:fluttermatic/meta/views/workflows/models/workflow.dart'; import 'package:fluttermatic/meta/views/workflows/runner/runner.dart'; import 'package:fluttermatic/meta/views/workflows/startup.dart'; @@ -85,11 +86,13 @@ class _WorkflowInfoTileState extends State { ], ), VSeparators.normal(), - Text( - widget.workflow.description, - style: const TextStyle(color: Colors.grey), - overflow: TextOverflow.ellipsis, - maxLines: 3, + Expanded( + child: Text( + widget.workflow.description, + style: const TextStyle(color: Colors.grey), + overflow: TextOverflow.ellipsis, + maxLines: 3, + ), ), VSeparators.normal(), Row( @@ -107,59 +110,134 @@ class _WorkflowInfoTileState extends State { ], ), VSeparators.normal(), + if (widget.workflow.workflowActions.length == 1) + Row( + children: [ + const RoundContainer( + height: 10, + width: 10, + radius: 10, + padding: EdgeInsets.zero, + color: kGreenColor, + child: SizedBox.shrink(), + ), + HSeparators.small(), + Text( + workflowActionModels + .firstWhere((WorkflowActionModel element) { + return element.id == + widget.workflow.workflowActions.first; + }).name, + style: const TextStyle(color: Colors.grey), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ) + // Will show the first and last action if there are more + // than one + else + Row( + children: [ + const RoundContainer( + height: 10, + width: 10, + radius: 10, + padding: EdgeInsets.zero, + color: kGreenColor, + child: SizedBox.shrink(), + ), + HSeparators.small(), + Text( + workflowActionModels + .firstWhere((WorkflowActionModel element) { + return element.id == + widget.workflow.workflowActions.first; + }).name, + style: const TextStyle(color: Colors.grey), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + HSeparators.xSmall(), + if (widget.workflow.workflowActions.length == 2) + const Text('...', + style: TextStyle(color: Colors.grey)) + else + const RoundContainer( + height: 2, + width: 10, + color: kGreenColor, + child: SizedBox.shrink(), + ), + HSeparators.xSmall(), + Text( + workflowActionModels + .firstWhere((WorkflowActionModel element) { + return element.id == + widget.workflow.workflowActions.last; + }).name, + style: const TextStyle(color: Colors.grey), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), ], ), ), - HSeparators.normal(), - Row( - children: [ - if (!widget.workflow.isSaved && _isHovering) - Tooltip( - message: 'This workflow has not completed setup yet', - child: SvgPicture.asset(Assets.warn, height: 20), - ), - const Spacer(), - RectangleButton( - width: 40, - height: 40, - child: const Icon(Icons.edit_rounded, size: 20), - onPressed: () async { - Map _workflow = - jsonDecode(await File(widget.path).readAsString()); - - await showDialog( - context: context, - builder: (_) => StartUpWorkflow( - pubspecPath: (widget.path.split('\\') - ..removeLast() - ..removeLast()) - .join('\\') + - '\\pubspec.yaml', - editWorkflowTemplate: - WorkflowTemplate.fromJson(_workflow), + if (_isHovering || !widget.workflow.isSaved) + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + children: [ + if (!widget.workflow.isSaved) + Tooltip( + message: 'This workflow has not completed setup yet', + child: SvgPicture.asset(Assets.warn, height: 20), ), - ); - }, - ), - if (widget.workflow.isSaved) - Padding( - padding: const EdgeInsets.only(left: 10), - child: RectangleButton( + const Spacer(), + RectangleButton( width: 40, height: 40, - child: const Icon(Icons.play_arrow_rounded, - color: kGreenColor, size: 22), - onPressed: () { - showDialog( + child: const Icon(Icons.edit_rounded, size: 20), + onPressed: () async { + Map _workflow = + jsonDecode(await File(widget.path).readAsString()); + + await showDialog( context: context, - builder: (_) => - WorkflowRunnerDialog(workflowPath: widget.path), + builder: (_) => StartUpWorkflow( + pubspecPath: (widget.path.split('\\') + ..removeLast() + ..removeLast()) + .join('\\') + + '\\pubspec.yaml', + editWorkflowTemplate: + WorkflowTemplate.fromJson(_workflow), + ), ); }, ), - ), - ], - ) + if (widget.workflow.isSaved) + Padding( + padding: const EdgeInsets.only(left: 10), + child: RectangleButton( + width: 40, + height: 40, + child: const Icon(Icons.play_arrow_rounded, + color: kGreenColor, size: 22), + onPressed: () { + showDialog( + context: context, + builder: (_) => WorkflowRunnerDialog( + workflowPath: widget.path), + ); + }, + ), + ), + ], + ), + ) ], ), ), diff --git a/lib/meta/views/tabs/sections/workflows/models/workflows.services.dart b/lib/meta/views/tabs/sections/workflows/models/workflows.services.dart index 56249d92..d07ffba8 100644 --- a/lib/meta/views/tabs/sections/workflows/models/workflows.services.dart +++ b/lib/meta/views/tabs/sections/workflows/models/workflows.services.dart @@ -56,7 +56,7 @@ class WorkflowServicesModel { _isExpiredCache = false; } - if (_isExpiredCache) { + if (_isExpiredCache || _force) { if (_force) { await logger.file(LogTypeTag.info, 'Fetching workflows from cache. Cache expired. Force refetch.', @@ -87,19 +87,20 @@ class WorkflowServicesModel { // Kill isolate. Cache is now updated. _port.send([_workflowsRefetch, true, false]); + return; } else { await logger.file(LogTypeTag.info, 'Fetching workflows from cache. Cache still valid.', logDir: Directory(_supportDir)); // Kill isolate. Cache is still valid. _port.send([_workflowsCache, true, false]); + return; } } else { // Kill isolate. _port.send([_workflowsCache, true, false]); + return; } - - return; } else { await logger.file( LogTypeTag.info, 'Fetching workflows initially. No cache found.', @@ -116,6 +117,7 @@ class WorkflowServicesModel { supportDir: _supportDir, ); + // Kill isolate _port.send([_projectsPaths, true, false]); return; } diff --git a/lib/meta/views/tabs/sections/workflows/workflow.dart b/lib/meta/views/tabs/sections/workflows/workflow.dart index 9a30978a..39f16320 100644 --- a/lib/meta/views/tabs/sections/workflows/workflow.dart +++ b/lib/meta/views/tabs/sections/workflows/workflow.dart @@ -16,12 +16,10 @@ import 'package:fluttermatic/components/widgets/buttons/square_button.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; import 'package:fluttermatic/components/widgets/ui/snackbar_tile.dart'; import 'package:fluttermatic/components/widgets/ui/spinner.dart'; -import 'package:fluttermatic/components/widgets/ui/stage_tile.dart'; import 'package:fluttermatic/core/services/logs.dart'; import 'package:fluttermatic/meta/utils/bin/utils/workflow.search.dart'; import 'package:fluttermatic/meta/utils/shared_pref.dart'; import 'package:fluttermatic/meta/views/tabs/components/horizontal_axis.dart'; -import 'package:fluttermatic/meta/views/tabs/home.dart'; import 'package:fluttermatic/meta/views/tabs/sections/projects/models/projects.services.dart'; import 'package:fluttermatic/meta/views/tabs/sections/workflows/elements/tile.dart'; import 'package:fluttermatic/meta/views/tabs/sections/workflows/models/workflows.services.dart'; @@ -63,7 +61,7 @@ class _HomeWorkflowSectionsState extends State { supportDir: (await getApplicationSupportDirectory()).path, ); - Isolate _isolate = await Isolate.spawn( + Isolate _i = await Isolate.spawn( WorkflowServicesModel.getWorkflowsIsolate, [ _loadWorkflowsPort.sendPort, @@ -91,14 +89,12 @@ class _HomeWorkflowSectionsState extends State { _workflowsLoading = false; _workflows.clear(); _workflows.addAll(message.first); - if (message[2] == true) { - _reloadingFromCache = true; - } else { - _reloadingFromCache = false; - } + _reloadingFromCache = message[2] == true; }); + + // No more expected responses, so will kill the isolate if (message[1] == true) { - _isolate.kill(); + _i.kill(); } } }); @@ -137,24 +133,20 @@ class _HomeWorkflowSectionsState extends State { width: 40, height: 40, child: const Icon(Icons.refresh_rounded, size: 20), - onPressed: () { - Navigator.pushReplacement( - context, - PageRouteBuilder( - pageBuilder: (_, __, ___) => - const HomeScreen(tab: HomeTab.workflow), - transitionDuration: Duration.zero, - ), - ); - }, + onPressed: () => _loadWorkflows(true), ), HSeparators.small(), RectangleButton( width: 40, height: 40, child: const Icon(Icons.add_rounded, size: 20), - onPressed: () => showDialog( - context: context, builder: (_) => const StartUpWorkflow()), + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const StartUpWorkflow(), + ); + await _loadWorkflows(true); + }, ), ], ); @@ -206,14 +198,7 @@ class _HomeWorkflowSectionsState extends State { builder: (_) => const SettingDialog( goToPage: SettingsPage.projects), ); - await Navigator.pushReplacement( - context, - PageRouteBuilder>( - pageBuilder: (_, __, ___) => - const HomeScreen(tab: HomeTab.workflow), - transitionDuration: Duration.zero, - ), - ); + await _loadWorkflows(true); }, ), HSeparators.small(), @@ -252,14 +237,7 @@ class _HomeWorkflowSectionsState extends State { goToPage: SettingsPage.projects, ), ); - await Navigator.pushReplacement( - context, - PageRouteBuilder>( - pageBuilder: (_, __, ___) => - const HomeScreen(tab: HomeTab.workflow), - transitionDuration: Duration.zero, - ), - ); + await _loadWorkflows(true); }, ), HSeparators.small(), @@ -295,8 +273,23 @@ class _HomeWorkflowSectionsState extends State { canCollapse: true, action: Row( children: [ - const StageTile(stageType: StageType.alpha), - HSeparators.normal(), + SquareButton( + size: 20, + tooltip: 'Add Workflow', + color: Colors.transparent, + hoverColor: Colors.transparent, + icon: const Icon(Icons.add_rounded, size: 15), + onPressed: () async { + await showDialog( + context: context, + builder: (_) => StartUpWorkflow( + pubspecPath: _workflows[i].path), + ); + + await _loadWorkflows(true); + }, + ), + HSeparators.small(), SquareButton( size: 20, tooltip: 'Reload', diff --git a/lib/meta/views/workflows/action_settings/deploy_web.dart b/lib/meta/views/workflows/action_settings/deploy_web.dart index 220df607..90877a5a 100644 --- a/lib/meta/views/workflows/action_settings/deploy_web.dart +++ b/lib/meta/views/workflows/action_settings/deploy_web.dart @@ -52,7 +52,6 @@ class _DeployWebWorkflowActionStateConfig if (widget.isValidated) RoundContainer( width: double.infinity, - color: Colors.blueGrey.withOpacity(0.2), child: Column( children: [ SvgPicture.asset(Assets.done, height: 20), @@ -336,7 +335,6 @@ class __VerifyColumnBarState extends State<_VerifyColumnBar> { @override Widget build(BuildContext context) { return RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ SvgPicture.asset(Assets.done, height: 20), @@ -357,21 +355,18 @@ class __VerifyColumnBarState extends State<_VerifyColumnBar> { duration: const Duration(milliseconds: 100), child: _isVerified ? SquareButton( - color: Colors.blueGrey.withOpacity(0.2), icon: const Icon(Icons.edit_rounded, color: kYellowColor), onPressed: widget.onEdit, ) : Row( children: [ SquareButton( - color: Colors.blueGrey.withOpacity(0.2), icon: const Icon(Icons.close_rounded, color: AppTheme.errorColor), onPressed: widget.onEdit, ), HSeparators.small(), SquareButton( - color: Colors.blueGrey.withOpacity(0.2), icon: const Icon(Icons.check_rounded, color: kGreenColor), onPressed: () async { diff --git a/lib/meta/views/workflows/components/build_mode_selector.dart b/lib/meta/views/workflows/components/build_mode_selector.dart index ad495760..c0580bc7 100644 --- a/lib/meta/views/workflows/components/build_mode_selector.dart +++ b/lib/meta/views/workflows/components/build_mode_selector.dart @@ -77,7 +77,6 @@ Widget selectBuildTypeTile( required bool isSelected, }) { return RectangleButton( - color: Colors.blueGrey.withOpacity(0.2), disableColor: Colors.blueGrey.withOpacity(0.2), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), width: double.infinity, diff --git a/lib/meta/views/workflows/runner/status/error.dart b/lib/meta/views/workflows/runner/status/error.dart index 22abd9f8..22f82a4b 100644 --- a/lib/meta/views/workflows/runner/status/error.dart +++ b/lib/meta/views/workflows/runner/status/error.dart @@ -33,7 +33,6 @@ class WorkflowError extends StatelessWidget { RoundContainer( width: 500, height: 230, - color: Colors.blueGrey.withOpacity(0.2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/meta/views/workflows/runner/status/startup.dart b/lib/meta/views/workflows/runner/status/startup.dart index 17e88a97..a93ebde1 100644 --- a/lib/meta/views/workflows/runner/status/startup.dart +++ b/lib/meta/views/workflows/runner/status/startup.dart @@ -27,7 +27,6 @@ class WorkflowStartUp extends StatelessWidget { children: [ RoundContainer( width: 500, - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Expanded( diff --git a/lib/meta/views/workflows/runner/status/success.dart b/lib/meta/views/workflows/runner/status/success.dart index 1b97d638..33011fdc 100644 --- a/lib/meta/views/workflows/runner/status/success.dart +++ b/lib/meta/views/workflows/runner/status/success.dart @@ -45,7 +45,6 @@ class WorkflowSuccess extends StatelessWidget { VSeparators.normal(), RoundContainer( width: 500, - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Expanded( diff --git a/lib/meta/views/workflows/sections/actions.dart b/lib/meta/views/workflows/sections/actions.dart index a42f554f..cca77a0d 100644 --- a/lib/meta/views/workflows/sections/actions.dart +++ b/lib/meta/views/workflows/sections/actions.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; // 📦 Package imports: import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/src/provider.dart'; // 🌎 Project imports: import 'package:fluttermatic/app/constants/constants.dart'; @@ -11,7 +12,7 @@ import 'package:fluttermatic/components/widgets/buttons/square_button.dart'; import 'package:fluttermatic/components/widgets/ui/info_widget.dart'; import 'package:fluttermatic/components/widgets/ui/information_widget.dart'; import 'package:fluttermatic/components/widgets/ui/round_container.dart'; -import 'package:fluttermatic/components/widgets/ui/snackbar_tile.dart'; +import 'package:fluttermatic/core/notifiers/theme.notifier.dart'; import 'package:fluttermatic/meta/utils/app_theme.dart'; import 'package:fluttermatic/meta/utils/extract_pubspec.dart'; import 'package:fluttermatic/meta/views/workflows/actions.dart'; @@ -59,7 +60,6 @@ class _SetProjectWorkflowActionsState extends State { children: [ RoundContainer( width: double.infinity, - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Expanded( @@ -106,10 +106,10 @@ class _SetProjectWorkflowActionsState extends State { type: InformationType.green, ), ), - VSeparators.normal(), + VSeparators.small(), infoWidget(context, 'You can select more than one workflow to run. We will try to show you analysis and details about the result of these workflows when you run them. You will be able to change the order of this workflow in the upcoming steps.'), - VSeparators.normal(), + VSeparators.small(), DragTarget( builder: (_, List candidateItems, List rejectedItems) { @@ -121,7 +121,6 @@ class _SetProjectWorkflowActionsState extends State { ); } else if (_addedWorkflows.isEmpty) { return RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), borderColor: kGreenColor.withOpacity(0.8), child: Row( children: [ @@ -137,7 +136,6 @@ class _SetProjectWorkflowActionsState extends State { } else { return RoundContainer( width: double.infinity, - color: Colors.blueGrey.withOpacity(0.2), child: Wrap( spacing: 10, runSpacing: 10, @@ -149,6 +147,10 @@ class _SetProjectWorkflowActionsState extends State { ].map( (WorkflowActionModel? e) { return RoundContainer( + color: Colors.blueGrey.withOpacity( + context.read().isDarkTheme + ? 0.2 + : 0.1), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -158,23 +160,9 @@ class _SetProjectWorkflowActionsState extends State { tooltip: 'Remove', icon: const Icon(Icons.close, size: 10), size: 20, - color: Colors.blueGrey.withOpacity(0.2), onPressed: () { setState(() => _addedWorkflows.remove(e)); widget.onActionsUpdate(_addedWorkflows); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - snackBarTile( - context, - '${e?.name ?? '"UNKNOWN"'} workflow action has been removed.', - type: SnackBarType.warning, - action: snackBarAction( - text: 'Undo', - onPressed: () => setState( - () => _addedWorkflows.add(e!)), - ), - ), - ); }, ), ], @@ -195,15 +183,6 @@ class _SetProjectWorkflowActionsState extends State { onAccept: (WorkflowActionModel val) { setState(() => _addedWorkflows.add(val)); widget.onActionsUpdate(_addedWorkflows); - - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - snackBarTile( - context, - '${val.name} workflow action has been added.', - type: SnackBarType.done, - ), - ); }, ), if (_addedWorkflows.length != workflowActionModels.length) @@ -246,19 +225,14 @@ class _SetProjectWorkflowActionsState extends State { onTap: () { setState(() => _addedWorkflows.add(e)); widget.onActionsUpdate(_addedWorkflows); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - snackBarTile( - context, - '${e.name} workflow action has been added.', - type: SnackBarType.done, - ), - ); }, child: RoundContainer( borderColor: _addedWorkflows.contains(e) ? kGreenColor - : AppTheme.darkBackgroundColor, + : Colors.blueGrey.withOpacity( + context.read().isDarkTheme + ? 0.2 + : 0.1), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -276,7 +250,6 @@ class _SetProjectWorkflowActionsState extends State { borderColor: _addedWorkflows.contains(e) ? AppTheme.errorColor : Colors.transparent, - color: Colors.blueGrey, child: Text(e.name), ), ), diff --git a/lib/meta/views/workflows/sections/configure_actions.dart b/lib/meta/views/workflows/sections/configure_actions.dart index 034695b0..f3950721 100644 --- a/lib/meta/views/workflows/sections/configure_actions.dart +++ b/lib/meta/views/workflows/sections/configure_actions.dart @@ -8,7 +8,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:fluttermatic/app/constants/constants.dart'; import 'package:fluttermatic/app/constants/enum.dart'; import 'package:fluttermatic/components/widgets/buttons/rectangle_button.dart'; -import 'package:fluttermatic/components/widgets/ui/round_container.dart'; import 'package:fluttermatic/meta/views/workflows/action_settings/build_android.dart'; import 'package:fluttermatic/meta/views/workflows/action_settings/build_ios.dart'; import 'package:fluttermatic/meta/views/workflows/action_settings/build_linux.dart'; @@ -137,27 +136,24 @@ class _SetProjectWorkflowActionsConfigurationState children: [ if (!_isBuildActionSelected.contains(true)) Center( - child: RoundContainer( - color: Colors.blueGrey.withOpacity(0.1), - child: Column( - children: [ - SvgPicture.asset(Assets.done, height: 30), - VSeparators.normal(), - const Text( - 'Nothing to configure', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + child: Column( + children: [ + SvgPicture.asset(Assets.done, height: 30), + VSeparators.normal(), + const Text( + 'Nothing to configure', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + VSeparators.normal(), + const SizedBox( + width: 400, + child: Text( + 'You have no additional options to configure your workflow\nactions, you can move on.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey), ), - VSeparators.normal(), - const SizedBox( - width: 400, - child: Text( - 'You have no additional options to configure your workflow actions, you can move on.', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 14, color: Colors.grey), - ), - ), - ], - ), + ), + ], ), ), if (_isDeployWeb) diff --git a/lib/meta/views/workflows/sections/info.dart b/lib/meta/views/workflows/sections/info.dart index c8d64403..acbc31a2 100644 --- a/lib/meta/views/workflows/sections/info.dart +++ b/lib/meta/views/workflows/sections/info.dart @@ -350,6 +350,9 @@ class _SetProjectWorkflowInfoState extends State { TextInputFormatter.withFunction( (TextEditingValue oldValue, TextEditingValue newValue) { + if (newValue.text.isEmpty) { + return newValue; + } // Only allow a-z, A-Z, 0-9, and spaces. if (RegExp(r'^[a-zA-Z0-9 _]+$') .hasMatch(newValue.text)) { @@ -407,7 +410,7 @@ class _SetProjectWorkflowInfoState extends State { TextInputFormatter.withFunction( (TextEditingValue oldValue, TextEditingValue newValue) { - if (newValue.text.length < 100) { + if (newValue.text.length < 300) { if (_descriptionTooLong) { setState(() => _descriptionTooLong = false); } @@ -428,7 +431,7 @@ class _SetProjectWorkflowInfoState extends State { padding: const EdgeInsets.symmetric(horizontal: 10), child: Tooltip( message: - 'Description length must be less than 100 characters.', + 'Description length must be less than 300 characters.', child: SvgPicture.asset(Assets.error, height: 20), ), ), diff --git a/lib/meta/views/workflows/sections/reorder_actions.dart b/lib/meta/views/workflows/sections/reorder_actions.dart index 5bdf8a1c..5af56a17 100644 --- a/lib/meta/views/workflows/sections/reorder_actions.dart +++ b/lib/meta/views/workflows/sections/reorder_actions.dart @@ -56,52 +56,56 @@ class _SetProjectWorkflowActionsOrderState type: InformationType.green, ), VSeparators.normal(), - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 350), - child: ReorderableListView.builder( - shrinkWrap: true, - itemCount: _workflowActions.length, - itemBuilder: (_, int i) { - return RoundContainer( - radius: 0, - width: double.infinity, - key: ValueKey(_workflowActions[i].id), - child: Row( - children: [ - Text( - (i + 1).toString() + ' - ', - style: const TextStyle( - fontSize: 15, fontWeight: FontWeight.bold), - ), - HSeparators.small(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(_workflowActions[i].name), - VSeparators.xSmall(), - Text( - _workflowActions[i].description, - style: const TextStyle(color: Colors.grey), - ), - ], - ), - ], - ), - ); - }, - onReorder: (int oldIndex, int newIndex) { - // Reorder the list with the new index. Then after it will call on - // update to update. - setState(() { - if (newIndex > oldIndex) { - newIndex -= 1; - } - WorkflowActionModel item = _workflowActions.removeAt(oldIndex); - _workflowActions.insert(newIndex, item); - }); - - widget.onReorder(_workflowActions); - }, + ClipRRect( + borderRadius: BorderRadius.circular(5), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 350), + child: ReorderableListView.builder( + shrinkWrap: true, + itemCount: _workflowActions.length, + itemBuilder: (_, int i) { + return RoundContainer( + radius: 0, + width: double.infinity, + key: ValueKey(_workflowActions[i].id), + child: Row( + children: [ + Text( + (i + 1).toString() + ' - ', + style: const TextStyle( + fontSize: 15, fontWeight: FontWeight.bold), + ), + HSeparators.small(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_workflowActions[i].name), + VSeparators.xSmall(), + Text( + _workflowActions[i].description, + style: const TextStyle(color: Colors.grey), + ), + ], + ), + ], + ), + ); + }, + onReorder: (int oldIndex, int newIndex) { + // Reorder the list with the new index. Then after it will call on + // update to update. + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + WorkflowActionModel item = + _workflowActions.removeAt(oldIndex); + _workflowActions.insert(newIndex, item); + }); + + widget.onReorder(_workflowActions); + }, + ), ), ), VSeparators.normal(), @@ -117,7 +121,7 @@ class _SetProjectWorkflowActionsOrderState width: 20, height: 20, radius: 50, - color: Colors.blueGrey.withOpacity(0.5), + color: Colors.blueGrey.withOpacity(0.2), padding: EdgeInsets.zero, child: Center( child: Text( @@ -125,7 +129,7 @@ class _SetProjectWorkflowActionsOrderState .length .toString(), style: const TextStyle( - fontSize: 12, fontWeight: FontWeight.bold)), + fontSize: 11, fontWeight: FontWeight.bold)), ), ), HSeparators.small(), @@ -135,32 +139,8 @@ class _SetProjectWorkflowActionsOrderState onPressed: () { showDialog( context: context, - builder: (_) => DialogTemplate( - child: Column( - children: [ - const DialogHeader(title: 'Suggestions'), - ..._suggestions(widget.workflowActions) - .map( - (String e) => Padding( - padding: const EdgeInsets.only(bottom: 10), - child: RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), - width: double.infinity, - child: Text(e), - ), - ), - ) - .toList(), - RectangleButton( - width: double.infinity, - child: const Text('OK'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ), - ), + builder: (_) => _ShowSuggestionsDialog( + workflowActions: _workflowActions), ); }, ), @@ -189,7 +169,14 @@ class _SetProjectWorkflowActionsOrderState Expanded( child: RectangleButton( child: const Text('View Suggestions'), - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + Navigator.of(context).pop(); + showDialog( + context: context, + builder: (_) => _ShowSuggestionsDialog( + workflowActions: _workflowActions), + ); + }, ), ), HSeparators.normal(), @@ -220,6 +207,41 @@ class _SetProjectWorkflowActionsOrderState } } +class _ShowSuggestionsDialog extends StatelessWidget { + final List workflowActions; + const _ShowSuggestionsDialog({ + Key? key, + required this.workflowActions, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DialogTemplate( + child: Column( + children: [ + const DialogHeader(title: 'Suggestions'), + ..._suggestions(workflowActions) + .map( + (String e) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: RoundContainer( + width: double.infinity, + child: Text(e), + ), + ), + ) + .toList(), + RectangleButton( + width: double.infinity, + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + } +} + List _suggestions(List workflowActions) { List _suggestions = []; @@ -339,9 +361,8 @@ List _suggestions(List workflowActions) { // If we are testing code after performing macOS build. if (_containsBuildMacOS && _containsTestCode) { - int _buildMacOSIndex = workflowActions.indexWhere( - (WorkflowActionModel e) => - e.id == WorkflowActionsIds.buildProjectForMacOS); + int _buildMacOSIndex = workflowActions.indexWhere((WorkflowActionModel e) => + e.id == WorkflowActionsIds.buildProjectForMacOS); int _testCodeIndex = workflowActions.indexWhere( (WorkflowActionModel e) => e.id == WorkflowActionsIds.runProjectTests); @@ -355,9 +376,8 @@ List _suggestions(List workflowActions) { // If we are testing code after performing Linux build. if (_containsBuildLinux && _containsTestCode) { - int _buildLinuxIndex = workflowActions.indexWhere( - (WorkflowActionModel e) => - e.id == WorkflowActionsIds.buildProjectForLinux); + int _buildLinuxIndex = workflowActions.indexWhere((WorkflowActionModel e) => + e.id == WorkflowActionsIds.buildProjectForLinux); int _testCodeIndex = workflowActions.indexWhere( (WorkflowActionModel e) => e.id == WorkflowActionsIds.runProjectTests); @@ -441,9 +461,8 @@ List _suggestions(List workflowActions) { (WorkflowActionModel e) => e.id == WorkflowActionsIds.analyzeDartProject); - int _buildMacOSIndex = workflowActions.indexWhere( - (WorkflowActionModel e) => - e.id == WorkflowActionsIds.buildProjectForMacOS); + int _buildMacOSIndex = workflowActions.indexWhere((WorkflowActionModel e) => + e.id == WorkflowActionsIds.buildProjectForMacOS); if (_analyzeCodeIndex > _buildMacOSIndex) { _suggestions.add( @@ -458,9 +477,8 @@ List _suggestions(List workflowActions) { (WorkflowActionModel e) => e.id == WorkflowActionsIds.analyzeDartProject); - int _buildLinuxIndex = workflowActions.indexWhere( - (WorkflowActionModel e) => - e.id == WorkflowActionsIds.buildProjectForLinux); + int _buildLinuxIndex = workflowActions.indexWhere((WorkflowActionModel e) => + e.id == WorkflowActionsIds.buildProjectForLinux); if (_analyzeCodeIndex > _buildLinuxIndex) { _suggestions.add( diff --git a/lib/meta/views/workflows/startup.dart b/lib/meta/views/workflows/startup.dart index 5791ca79..ff41af33 100644 --- a/lib/meta/views/workflows/startup.dart +++ b/lib/meta/views/workflows/startup.dart @@ -126,8 +126,7 @@ class _StartUpWorkflowState extends State { setState(() => _isSavingLocally = true); - Isolate _isolate = - await Isolate.spawn(_saveWorkflowWithIsolate, [ + Isolate _i = await Isolate.spawn(_saveWorkflowWithIsolate, [ _saveLocallyPort.sendPort, _projectPath, _pendingChanges, @@ -144,6 +143,7 @@ class _StartUpWorkflowState extends State { if (!_syncStreamListening) { setState(() => _syncStreamListening = true); _saveLocallyPort.listen((dynamic message) { + _i.kill(); if (message is Map) { setState(() { _lastSavedContent = message; @@ -157,7 +157,6 @@ class _StartUpWorkflowState extends State { _saveLocalError = true; }); } - _isolate.kill(); }); } @@ -725,10 +724,12 @@ Future _saveWorkflowWithIsolate(List data) async { .timeout(const Duration(seconds: 3)); _port.send(_data); + return; } catch (_, s) { await logger.file(LogTypeTag.error, 'Couldn\'t sync workflow settings.', stackTraces: s); _port.send([]); + return; } } diff --git a/lib/meta/views/workflows/views/confirm_delete.dart b/lib/meta/views/workflows/views/confirm_delete.dart index d0f03496..653a62c6 100644 --- a/lib/meta/views/workflows/views/confirm_delete.dart +++ b/lib/meta/views/workflows/views/confirm_delete.dart @@ -53,7 +53,6 @@ class _ConfirmWorkflowDeleteState extends State { ), VSeparators.normal(), RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Expanded( diff --git a/lib/meta/views/workflows/views/existing_workflows.dart b/lib/meta/views/workflows/views/existing_workflows.dart index 45dcb8f6..9b718d2c 100644 --- a/lib/meta/views/workflows/views/existing_workflows.dart +++ b/lib/meta/views/workflows/views/existing_workflows.dart @@ -242,16 +242,16 @@ class __WorkflowTileState extends State<_WorkflowTile> { onEnter: (_) => setState(() => _isHovering = true), onExit: (_) => setState(() => _isHovering = false), child: RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), padding: EdgeInsets.zero, child: Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.all(10), - child: Text(widget.template.name), + child: Text(widget.template.name, maxLines: 1), ), ), + HSeparators.normal(), if (!widget.template.isSaved) Padding( padding: const EdgeInsets.symmetric(horizontal: 5), @@ -261,7 +261,6 @@ class __WorkflowTileState extends State<_WorkflowTile> { child: SvgPicture.asset(Assets.warn, height: 20), ), ), - HSeparators.normal(), if (_isHovering) Padding( padding: const EdgeInsets.symmetric(horizontal: 5), diff --git a/lib/meta/views/workflows/views/log_history.dart b/lib/meta/views/workflows/views/log_history.dart index 04f70006..3f2bd38e 100644 --- a/lib/meta/views/workflows/views/log_history.dart +++ b/lib/meta/views/workflows/views/log_history.dart @@ -222,7 +222,7 @@ class _ShowWorkflowLogHistoryState extends State { ), ], ) - ] else + ] else ...[ ConstrainedBox( constraints: const BoxConstraints(maxHeight: 400), child: ListView.builder( @@ -252,6 +252,13 @@ class _ShowWorkflowLogHistoryState extends State { }, ), ), + VSeparators.normal(), + RectangleButton( + width: double.infinity, + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], ], ), ); @@ -289,7 +296,6 @@ class __LogTileState extends State<_LogTile> { onEnter: (_) => setState(() => _isHovering = true), onExit: (_) => setState(() => _isHovering = false), child: RoundContainer( - color: Colors.blueGrey.withOpacity(0.2), child: Row( children: [ Expanded( @@ -320,14 +326,14 @@ class __LogTileState extends State<_LogTile> { size: 30, tooltip: 'Delete', icon: const Icon(Icons.delete_forever, - color: kRedColor, size: 20), + color: kRedColor, size: 18), onPressed: widget.onDelete, ), HSeparators.small(), SquareButton( size: 30, tooltip: 'Open Log', - icon: const Icon(Icons.preview_rounded, size: 20), + icon: const Icon(Icons.preview_rounded, size: 18), onPressed: () { showDialog( context: context, diff --git a/lib/meta/views/workflows/views/workflow_options.dart b/lib/meta/views/workflows/views/workflow_options.dart index e0d63a5d..a3d75aa0 100644 --- a/lib/meta/views/workflows/views/workflow_options.dart +++ b/lib/meta/views/workflows/views/workflow_options.dart @@ -11,6 +11,7 @@ import 'package:fluttermatic/components/dialog_templates/dialog_header.dart'; import 'package:fluttermatic/components/widgets/buttons/rectangle_button.dart'; import 'package:fluttermatic/components/widgets/ui/coming_soon.dart'; import 'package:fluttermatic/components/widgets/ui/dialog_template.dart'; +import 'package:fluttermatic/components/widgets/ui/spinner.dart'; import 'package:fluttermatic/components/widgets/ui/stage_tile.dart'; import 'package:fluttermatic/meta/utils/app_theme.dart'; import 'package:fluttermatic/meta/views/workflows/models/workflow.dart'; @@ -19,7 +20,7 @@ import 'package:fluttermatic/meta/views/workflows/startup.dart'; import 'package:fluttermatic/meta/views/workflows/views/confirm_delete.dart'; import 'package:fluttermatic/meta/views/workflows/views/log_history.dart'; -class ShowWorkflowTileOptions extends StatelessWidget { +class ShowWorkflowTileOptions extends StatefulWidget { final String workflowPath; final Function() onDelete; @@ -29,6 +30,30 @@ class ShowWorkflowTileOptions extends StatelessWidget { required this.onDelete, }) : super(key: key); + @override + State createState() => + _ShowWorkflowTileOptionsState(); +} + +class _ShowWorkflowTileOptionsState extends State { + bool _loading = true; + + Future _loadWorkflow() async { + if (!await File(widget.workflowPath).exists()) { + widget.onDelete(); + await Future.delayed(const Duration(milliseconds: 300)); + Navigator.pop(context); + } else { + setState(() => _loading = false); + } + } + + @override + void initState() { + _loadWorkflow(); + super.initState(); + } + @override Widget build(BuildContext context) { return DialogTemplate( @@ -38,151 +63,155 @@ class ShowWorkflowTileOptions extends StatelessWidget { title: 'Options', leading: StageTile(stageType: StageType.alpha), ), - Row( - children: [ - Expanded( - child: RectangleButton( - height: 100, - child: Column( - children: [ - // const Expanded( - // child: Icon(Icons.preview_rounded, size: 25)), - // VSeparators.small(), - const Expanded(child: Center(child: Text('Preview'))), - VSeparators.small(), - const ComingSoonTile(), - ], + if (_loading) ...[ + const Padding(padding: EdgeInsets.all(30), child: Spinner()), + ] else ...[ + Row( + children: [ + Expanded( + child: RectangleButton( + height: 100, + child: Column( + children: [ + // const Expanded( + // child: Icon(Icons.preview_rounded, size: 25)), + // VSeparators.small(), + const Expanded(child: Center(child: Text('Preview'))), + VSeparators.small(), + const ComingSoonTile(), + ], + ), + // TODO: Open preview + onPressed: () {}, ), - // TODO: Open preview - onPressed: () {}, ), - ), - HSeparators.normal(), - Expanded( - child: RectangleButton( - height: 100, - child: Column( - children: [ - const Expanded( - child: - Center(child: Icon(Icons.edit_rounded, size: 25)), - ), - VSeparators.small(), - const Text('Edit'), - ], - ), - onPressed: () async { - Map _workflow = - jsonDecode(await File(workflowPath).readAsString()); + HSeparators.normal(), + Expanded( + child: RectangleButton( + height: 100, + child: Column( + children: [ + const Expanded( + child: + Center(child: Icon(Icons.edit_rounded, size: 25)), + ), + VSeparators.small(), + const Text('Edit'), + ], + ), + onPressed: () async { + Map _workflow = jsonDecode( + await File(widget.workflowPath).readAsString()); - await showDialog( - context: context, - builder: (_) => StartUpWorkflow( - pubspecPath: (workflowPath.split('\\') - ..removeLast() - ..removeLast()) - .join('\\') + - '\\pubspec.yaml', - editWorkflowTemplate: - WorkflowTemplate.fromJson(_workflow), - ), - ); - }, - ), - ), - HSeparators.normal(), - Expanded( - child: RectangleButton( - height: 100, - child: Column( - children: [ - const Expanded(child: Icon(Icons.history, size: 25)), - VSeparators.small(), - const Text('View Logs'), - ], + await showDialog( + context: context, + builder: (_) => StartUpWorkflow( + pubspecPath: (widget.workflowPath.split('\\') + ..removeLast() + ..removeLast()) + .join('\\') + + '\\pubspec.yaml', + editWorkflowTemplate: + WorkflowTemplate.fromJson(_workflow), + ), + ); + }, ), - onPressed: () { - Navigator.pop(context); - showDialog( - context: context, - builder: (_) => - ShowWorkflowLogHistory(workflowPath: workflowPath), - ); - }, ), - ), - ], - ), - VSeparators.normal(), - Row( - children: [ - Expanded( - child: Tooltip( - message: !WorkflowTemplate.fromJson( - jsonDecode(File(workflowPath).readAsStringSync())) - .isSaved - ? 'This workflow is not saved yet. You can edit it, but you will need to save it before you can run it.' - : '', + HSeparators.normal(), + Expanded( child: RectangleButton( height: 100, - disable: !WorkflowTemplate.fromJson( - jsonDecode(File(workflowPath).readAsStringSync())) - .isSaved, child: Column( children: [ - const Expanded( - child: Icon(Icons.play_arrow_rounded, - color: kGreenColor, size: 28), - ), + const Expanded(child: Icon(Icons.history, size: 25)), VSeparators.small(), - const Text('Run'), + const Text('View Logs'), ], ), onPressed: () { Navigator.pop(context); showDialog( context: context, - builder: (_) => - WorkflowRunnerDialog(workflowPath: workflowPath), + builder: (_) => ShowWorkflowLogHistory( + workflowPath: widget.workflowPath), ); }, ), ), - ), - HSeparators.normal(), - Expanded( - child: RectangleButton( - height: 100, - child: Column( - children: [ - const Expanded( - child: Icon(Icons.delete_forever, - color: AppTheme.errorColor, size: 28), + ], + ), + VSeparators.normal(), + Row( + children: [ + Expanded( + child: Tooltip( + message: !WorkflowTemplate.fromJson(jsonDecode( + File(widget.workflowPath).readAsStringSync())) + .isSaved + ? 'This workflow is not saved yet. You can edit it, but you will need to save it before you can run it.' + : '', + child: RectangleButton( + height: 100, + disable: !WorkflowTemplate.fromJson(jsonDecode( + File(widget.workflowPath).readAsStringSync())) + .isSaved, + child: Column( + children: [ + const Expanded( + child: Icon(Icons.play_arrow_rounded, + color: kGreenColor, size: 28), + ), + VSeparators.small(), + const Text('Run'), + ], ), - VSeparators.small(), - const Text('Delete'), - ], + onPressed: () { + Navigator.pop(context); + showDialog( + context: context, + builder: (_) => WorkflowRunnerDialog( + workflowPath: widget.workflowPath), + ); + }, + ), ), - onPressed: () { - Navigator.pop(context); - showDialog( - context: context, - builder: (_) => ConfirmWorkflowDelete( - path: workflowPath, - template: WorkflowTemplate.fromJson( - jsonDecode(File(workflowPath).readAsStringSync())), - onClose: (bool d) { - if (d) { - return onDelete(); - } - }, - ), - ); - }, ), - ), - ], - ), + HSeparators.normal(), + Expanded( + child: RectangleButton( + height: 100, + child: Column( + children: [ + const Expanded( + child: Icon(Icons.delete_forever, + color: AppTheme.errorColor, size: 28), + ), + VSeparators.small(), + const Text('Delete'), + ], + ), + onPressed: () { + Navigator.pop(context); + showDialog( + context: context, + builder: (_) => ConfirmWorkflowDelete( + path: widget.workflowPath, + template: WorkflowTemplate.fromJson(jsonDecode( + File(widget.workflowPath).readAsStringSync())), + onClose: (bool d) { + if (d) { + return widget.onDelete(); + } + }, + ), + ); + }, + ), + ), + ], + ), + ], ], ), ); diff --git a/scripts/bin/script.dart b/scripts/bin/script.dart index 9ac289ae..59c26abc 100644 --- a/scripts/bin/script.dart +++ b/scripts/bin/script.dart @@ -29,7 +29,7 @@ Future main(List args) async { // Build the app in release mode. await buildAppWithMode(BuildMode.release); - exit(0); + return; } if (args.length > 1) { @@ -40,12 +40,12 @@ Future main(List args) async { if (args.first == '--help' || args.first == '-h') { print(infoPen(helpMessage)); - exit(0); + return; } if (args.first == '--version' || args.first == '-v') { print(infoPen(versionMessage)); - exit(0); + return; } if (args.first.startsWith('--mode') || args.first.startsWith('-m')) { @@ -62,21 +62,21 @@ Future main(List args) async { // Build the app in debug mode. await buildAppWithMode(BuildMode.debug); - exit(0); + return; case 'profile': print(greenPen('Building app in Profile mode...')); // Build the app in profile mode. await buildAppWithMode(BuildMode.profile); - exit(0); + return; case 'release': print(greenPen('Building app in Release mode...')); // Build the app in release mode. await buildAppWithMode(BuildMode.release); - exit(0); + return; default: print(errorPen('Invalid build mode provided.')); exit(1);