diff --git a/experimental/linting_tool/lib/app.dart b/experimental/linting_tool/lib/app.dart index 7b31b4a5e..8937e70f8 100644 --- a/experimental/linting_tool/lib/app.dart +++ b/experimental/linting_tool/lib/app.dart @@ -11,6 +11,8 @@ import 'package:linting_tool/routes.dart' as routes; import 'package:provider/provider.dart'; import 'package:http/http.dart' as http; +final client = http.Client(); + class LintingTool extends StatefulWidget { const LintingTool({Key? key}) : super(key: key); @@ -26,10 +28,10 @@ class _LintingToolState extends State { return MultiProvider( providers: [ ChangeNotifierProvider( - create: (context) => RuleStore(http.Client()), + create: (context) => RuleStore(client), ), ChangeNotifierProvider( - create: (context) => ProfilesStore(), + create: (context) => ProfilesStore(client), ), ], child: MaterialApp( diff --git a/experimental/linting_tool/lib/model/profiles_store.dart b/experimental/linting_tool/lib/model/profiles_store.dart index fd8d48dd2..7d6155b0b 100644 --- a/experimental/linting_tool/lib/model/profiles_store.dart +++ b/experimental/linting_tool/lib/model/profiles_store.dart @@ -2,16 +2,26 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:json2yaml/json2yaml.dart'; import 'package:linting_tool/model/profile.dart'; import 'package:linting_tool/model/rule.dart'; import 'package:linting_tool/repository/hive_service.dart'; +import 'package:linting_tool/repository/repository.dart'; +import 'package:http/http.dart' as http; +import 'package:file_selector/file_selector.dart' as file_selector; +import 'package:yaml/yaml.dart'; const _boxName = 'rules_profile'; class ProfilesStore extends ChangeNotifier { - ProfilesStore() { + late final Repository repository; + ProfilesStore(http.Client httpClient) { + repository = Repository(httpClient); fetchSavedProfiles(); } bool _isLoading = true; @@ -66,4 +76,84 @@ class ProfilesStore extends ChangeNotifier { await fetchSavedProfiles(); }); } + + Future exportProfileFile( + RulesProfile profile, { + RulesStyle rulesStyle = RulesStyle.booleanMap, + }) async { + _isLoading = true; + notifyListeners(); + + var resultSaved = false; + + try { + var templateFileData = await repository.getTemplateFile(); + + String newYamlFile = + _prepareYamlFile(profile, templateFileData, rulesStyle); + + resultSaved = await _saveFileToDisk(newYamlFile); + } on SocketException catch (e) { + log(e.toString()); + _error = 'Check internet connection.'; + resultSaved = false; + } on Exception catch (e) { + log(e.toString()); + } + + _isLoading = false; + notifyListeners(); + + return resultSaved; + } + + Future _saveFileToDisk(String newYamlFile) async { + const name = 'analysis_options.yaml'; + + /// Get file path using file picker. + var savePath = await file_selector.getSavePath( + suggestedName: name, + ); + + final data = Uint8List.fromList(newYamlFile.codeUnits); + final file = file_selector.XFile.fromData(data, name: name); + + /// Save file to disk if path was provided. + if (savePath != null) { + file.saveTo(savePath); + return true; + } + + var errorMessage = 'File path not found.'; + _error = errorMessage; + throw Exception(errorMessage); + } + + String _prepareYamlFile( + RulesProfile profile, YamlMap templateFile, RulesStyle rulesStyle) { + var rules = profile.rules.map((e) => e.name).toList(); + + var rulesData = + json.decode(json.encode(templateFile)) as Map; + + /// Add rules to existing template according to formatting style. + if (rulesStyle == RulesStyle.booleanMap) { + var rulesMap = Map.fromEntries( + rules.map( + (e) => MapEntry(e, true), + ), + ); + rulesData.update('linter', (dynamic value) => {'rules': rulesMap}); + } else { + rulesData.update('linter', (dynamic value) => {'rules': rules}); + } + + return json2yaml(rulesData, yamlStyle: YamlStyle.pubspecYaml); + } +} + +/// Formatting style for rules. +enum RulesStyle { + list, + booleanMap, } diff --git a/experimental/linting_tool/lib/pages/saved_lints_page.dart b/experimental/linting_tool/lib/pages/saved_lints_page.dart index 6c94b0a93..0adb50fa0 100644 --- a/experimental/linting_tool/lib/pages/saved_lints_page.dart +++ b/experimental/linting_tool/lib/pages/saved_lints_page.dart @@ -45,7 +45,7 @@ class SavedLintsPage extends StatelessWidget { ), itemCount: profilesStore.savedProfiles.length, cacheExtent: 5, - itemBuilder: (context, index) { + itemBuilder: (itemBuilderContext, index) { var profile = profilesStore.savedProfiles[index]; return ListTile( title: Text( @@ -74,10 +74,21 @@ class SavedLintsPage extends StatelessWidget { ), PopupMenuButton( icon: const Icon(Icons.more_vert), - onSelected: (value) { + onSelected: (value) async { switch (value) { case 'Export file': - // TODO(abd99): Implement exporting files. + // TODO(abd99): Add option to select formatting style. + + var saved = await profilesStore + .exportProfileFile(profile); + + if (!saved) { + _showSnackBar( + context, + profilesStore.error ?? 'Failed to save file.', + ); + } + break; case 'Delete': profilesStore.deleteProfile(profile); @@ -123,4 +134,12 @@ class SavedLintsPage extends StatelessWidget { }, ); } + + void _showSnackBar(BuildContext context, String data) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(data), + ), + ); + } } diff --git a/experimental/linting_tool/lib/repository/api_provider.dart b/experimental/linting_tool/lib/repository/api_provider.dart index 8cead9f11..dc48391f8 100644 --- a/experimental/linting_tool/lib/repository/api_provider.dart +++ b/experimental/linting_tool/lib/repository/api_provider.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:linting_tool/model/rule.dart'; +import 'package:yaml/yaml.dart'; class APIProvider { final _baseURL = 'https://dart-lang.github.io/linter'; @@ -12,7 +13,7 @@ class APIProvider { APIProvider(this.httpClient); Future> getRulesList() async { - http.Response response = + final response = await httpClient.get(Uri.parse('$_baseURL//lints/machine/rules.json')); if (response.statusCode == 200) { @@ -26,4 +27,14 @@ class APIProvider { throw Exception('Failed to load rules'); } } + + Future getTemplateFile() async { + final response = await httpClient.get(Uri.parse( + 'https://raw.githubusercontent.com/flutter/flutter/master/packages/flutter_tools/templates/app_shared/analysis_options.yaml.tmpl')); + if (response.statusCode == 200) { + return loadYaml(response.body) as YamlMap; + } else { + throw Exception('Failed to load template file'); + } + } } diff --git a/experimental/linting_tool/lib/repository/repository.dart b/experimental/linting_tool/lib/repository/repository.dart index 4e17d4984..9f5b8ff7c 100644 --- a/experimental/linting_tool/lib/repository/repository.dart +++ b/experimental/linting_tool/lib/repository/repository.dart @@ -5,6 +5,7 @@ import 'package:linting_tool/model/rule.dart'; import 'package:linting_tool/repository/api_provider.dart'; import 'package:http/http.dart' as http; +import 'package:yaml/yaml.dart'; class Repository { late final APIProvider _apiProvider; @@ -14,4 +15,6 @@ class Repository { } Future> getRulesList() => _apiProvider.getRulesList(); + + Future getTemplateFile() => _apiProvider.getTemplateFile(); } diff --git a/experimental/linting_tool/linux/flutter/generated_plugin_registrant.cc b/experimental/linting_tool/linux/flutter/generated_plugin_registrant.cc index d38195aa0..db1fd636a 100644 --- a/experimental/linting_tool/linux/flutter/generated_plugin_registrant.cc +++ b/experimental/linting_tool/linux/flutter/generated_plugin_registrant.cc @@ -4,6 +4,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); } diff --git a/experimental/linting_tool/linux/flutter/generated_plugins.cmake b/experimental/linting_tool/linux/flutter/generated_plugins.cmake index 51436ae8c..68205a330 100644 --- a/experimental/linting_tool/linux/flutter/generated_plugins.cmake +++ b/experimental/linting_tool/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift b/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift index 0d56f519c..b700dcdae 100644 --- a/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import file_selector_macos import path_provider_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/experimental/linting_tool/macos/Podfile.lock b/experimental/linting_tool/macos/Podfile.lock index 328ddd1ae..ba70f1766 100644 --- a/experimental/linting_tool/macos/Podfile.lock +++ b/experimental/linting_tool/macos/Podfile.lock @@ -1,19 +1,25 @@ PODS: + - file_selector_macos (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) - path_provider_macos (0.0.1): - FlutterMacOS DEPENDENCIES: + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) EXTERNAL SOURCES: + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos FlutterMacOS: :path: Flutter/ephemeral path_provider_macos: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos SPEC CHECKSUMS: + file_selector_macos: ff6dc948d4ddd34e8602a1f60b7d0b4cc6051a47 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b diff --git a/experimental/linting_tool/macos/Runner/DebugProfile.entitlements b/experimental/linting_tool/macos/Runner/DebugProfile.entitlements index 3ba6c1266..9158f3eb1 100644 --- a/experimental/linting_tool/macos/Runner/DebugProfile.entitlements +++ b/experimental/linting_tool/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,7 @@ com.apple.security.network.server + com.apple.security.files.user-selected.read-write + diff --git a/experimental/linting_tool/macos/Runner/Release.entitlements b/experimental/linting_tool/macos/Runner/Release.entitlements index 7a2230dc3..c1a22b8a0 100644 --- a/experimental/linting_tool/macos/Runner/Release.entitlements +++ b/experimental/linting_tool/macos/Runner/Release.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.files.user-selected.read-write + diff --git a/experimental/linting_tool/pubspec.lock b/experimental/linting_tool/pubspec.lock index 2d0de87d7..c47530063 100644 --- a/experimental/linting_tool/pubspec.lock +++ b/experimental/linting_tool/pubspec.lock @@ -155,6 +155,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+4" crypto: dependency: transitive description: @@ -204,6 +211,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + file_selector: + dependency: "direct main" + description: + name: file_selector + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.2" + file_selector_linux: + dependency: "direct main" + description: + name: file_selector_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+1" + file_selector_macos: + dependency: "direct main" + description: + name: file_selector_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+1" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + file_selector_web: + dependency: "direct main" + description: + name: file_selector_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.1+1" + file_selector_windows: + dependency: "direct main" + description: + name: file_selector_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+1" fixnum: dependency: transitive description: @@ -235,6 +284,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -319,6 +373,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" + json2yaml: + dependency: "direct main" + description: + name: json2yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" json_annotation: dependency: "direct main" description: @@ -626,7 +687,7 @@ packages: source: hosted version: "0.2.0" yaml: - dependency: transitive + dependency: "direct main" description: name: yaml url: "https://pub.dartlang.org" @@ -634,4 +695,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.13.0 <3.0.0" - flutter: ">=1.20.0" + flutter: ">=2.0.0" diff --git a/experimental/linting_tool/pubspec.yaml b/experimental/linting_tool/pubspec.yaml index f789abede..915c66eed 100644 --- a/experimental/linting_tool/pubspec.yaml +++ b/experimental/linting_tool/pubspec.yaml @@ -12,14 +12,21 @@ dependencies: adaptive_breakpoints: ^0.0.4 cupertino_icons: ^1.0.2 equatable: ^2.0.3 + file_selector: ^0.8.2 + file_selector_linux: ^0.0.2+1 + file_selector_macos: ^0.0.4+1 + file_selector_web: ^0.8.1+1 + file_selector_windows: ^0.0.2+1 flutter_markdown: ^0.6.2 google_fonts: ^2.1.0 hive: ^2.0.4 hive_flutter: ^1.1.0 http: ^0.13.3 + json2yaml: ^2.0.0 json_annotation: ^4.0.1 mockito: ^5.0.13 provider: ^5.0.0 + yaml: ^3.1.0 dev_dependencies: flutter_test: diff --git a/experimental/linting_tool/test/widget_test.dart b/experimental/linting_tool/test/widget_test.dart index 4b4e1c879..48c64818c 100644 --- a/experimental/linting_tool/test/widget_test.dart +++ b/experimental/linting_tool/test/widget_test.dart @@ -38,7 +38,7 @@ class _TestApp extends StatelessWidget { create: (context) => RuleStore(_mockClient), ), ChangeNotifierProvider( - create: (context) => ProfilesStore(), + create: (context) => ProfilesStore(_mockClient), ), ], child: MaterialApp( diff --git a/experimental/linting_tool/windows/flutter/generated_plugin_registrant.cc b/experimental/linting_tool/windows/flutter/generated_plugin_registrant.cc index 4bfa0f3a3..0cb14e795 100644 --- a/experimental/linting_tool/windows/flutter/generated_plugin_registrant.cc +++ b/experimental/linting_tool/windows/flutter/generated_plugin_registrant.cc @@ -4,6 +4,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorPlugin")); } diff --git a/experimental/linting_tool/windows/flutter/generated_plugins.cmake b/experimental/linting_tool/windows/flutter/generated_plugins.cmake index 4d10c2518..63eda9b7b 100644 --- a/experimental/linting_tool/windows/flutter/generated_plugins.cmake +++ b/experimental/linting_tool/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows ) set(PLUGIN_BUNDLED_LIBRARIES)