diff --git a/packages/pasteboard/.metadata b/packages/pasteboard/.metadata index 0dbdc524..6d779205 100644 --- a/packages/pasteboard/.metadata +++ b/packages/pasteboard/.metadata @@ -1,10 +1,42 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 15b872266458f3299b6586565024a64530460e73 - channel: master + revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + channel: stable project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + - platform: ios + create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + - platform: linux + create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + - platform: macos + create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + - platform: web + create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + - platform: windows + create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/pasteboard/README.md b/packages/pasteboard/README.md index c6ed661e..5505c9c5 100644 --- a/packages/pasteboard/README.md +++ b/packages/pasteboard/README.md @@ -10,6 +10,7 @@ A flutter plugin which could read image,files from clipboard and write files to | Linux | ✅ | | macOS | ✅ | | iOS | ✅ | +| Web | ✅ | ## Getting Started diff --git a/packages/pasteboard/analysis_options.yaml b/packages/pasteboard/analysis_options.yaml index 92a34527..ccf5e3b9 100644 --- a/packages/pasteboard/analysis_options.yaml +++ b/packages/pasteboard/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:very_good_analysis/analysis_options.yaml +include: package:flutter_lints/flutter.yaml analyzer: errors: @@ -7,46 +7,34 @@ analyzer: lines_longer_than_80_chars: ignore avoid_function_literals_in_foreach_calls: ignore control_flow_in_finally: true - exclude: - - 'bin/cache/**' - # the following two are relative to the stocks example and the flutter package respectively - # see https://github.com/dart-lang/sdk/issues/28463 - - 'lib/i18n/messages_*.dart' - - 'lib/src/http/**' - # custom - - 'lib/generated/**' - - '**.g.dart' - # test - - 'test/**' linter: rules: - - prefer_const_constructors_in_immutables - - prefer_relative_imports - # - require_trailing_commas - - prefer_final_locals - - avoid_void_async - - always_put_required_named_parameters_first - - unnecessary_await_in_return - - prefer_expression_function_bodies - - avoid_field_initializers_in_const_classes - - file_names - - unnecessary_parenthesis - - prefer_void_to_null - - avoid_bool_literals_in_conditional_expressions - - avoid_returning_null_for_void - - prefer_function_declarations_over_variables - - empty_statements - - prefer_is_not_operator - - cast_nullable_to_non_nullable - - type_annotate_public_apis - - prefer_const_literals_to_create_immutables - - use_named_constants - - use_string_buffers - - unnecessary_raw_strings - - unnecessary_null_checks - - parameter_assignments - - prefer_const_declarations - - sort_unnamed_constructors_first - - use_setters_to_change_properties - - curly_braces_in_flow_control_structures + prefer_const_constructors_in_immutables: true + prefer_relative_imports: true + prefer_final_locals: true + avoid_void_async: true + always_put_required_named_parameters_first: true + unnecessary_await_in_return: true + prefer_expression_function_bodies: true + avoid_field_initializers_in_const_classes: true + file_names: true + unnecessary_parenthesis: true + prefer_void_to_null: true + avoid_bool_literals_in_conditional_expressions: true + avoid_returning_null_for_void: true + prefer_function_declarations_over_variables: true + empty_statements: true + prefer_is_not_operator: true + cast_nullable_to_non_nullable: true + type_annotate_public_apis: true + prefer_const_literals_to_create_immutables: true + use_named_constants: true + use_string_buffers: true + unnecessary_raw_strings: true + unnecessary_null_checks: true + parameter_assignments: true + prefer_const_declarations: true + sort_unnamed_constructors_first: true + use_setters_to_change_properties: true + curly_braces_in_flow_control_structures: true diff --git a/packages/pasteboard/example/lib/main.dart b/packages/pasteboard/example/lib/main.dart index 6f35c5dd..36e61828 100644 --- a/packages/pasteboard/example/lib/main.dart +++ b/packages/pasteboard/example/lib/main.dart @@ -46,13 +46,24 @@ class _MyAppState extends State { controller: textController, maxLines: 10, ), - MaterialButton( - onPressed: () async { - final lines = - const LineSplitter().convert(textController.text); - await Pasteboard.writeFiles(lines); - }, - child: const Text('copy'), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: () async { + final lines = + const LineSplitter().convert(textController.text); + await Pasteboard.writeFiles(lines); + }, + child: const Text('copy as files'), + ), + TextButton( + onPressed: () { + Pasteboard.writeText(textController.text); + }, + child: const Text('copy as text'), + ), + ], ), MaterialButton( onPressed: () async { diff --git a/packages/pasteboard/example/linux/flutter/generated_plugins.cmake b/packages/pasteboard/example/linux/flutter/generated_plugins.cmake index 1829bc31..c242e0ad 100644 --- a/packages/pasteboard/example/linux/flutter/generated_plugins.cmake +++ b/packages/pasteboard/example/linux/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST pasteboard ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/pasteboard/example/pubspec.lock b/packages/pasteboard/example/pubspec.lock index e803db61..514719c5 100644 --- a/packages/pasteboard/example/pubspec.lock +++ b/packages/pasteboard/example/pubspec.lock @@ -42,14 +42,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -67,6 +67,18 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" lints: dependency: transitive description: @@ -87,7 +99,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -108,7 +120,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -120,7 +132,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -155,21 +167,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.20.0" diff --git a/packages/pasteboard/example/web/favicon.png b/packages/pasteboard/example/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/packages/pasteboard/example/web/favicon.png differ diff --git a/packages/pasteboard/example/web/icons/Icon-192.png b/packages/pasteboard/example/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/packages/pasteboard/example/web/icons/Icon-192.png differ diff --git a/packages/pasteboard/example/web/icons/Icon-512.png b/packages/pasteboard/example/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/packages/pasteboard/example/web/icons/Icon-512.png differ diff --git a/packages/pasteboard/example/web/icons/Icon-maskable-192.png b/packages/pasteboard/example/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/packages/pasteboard/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/pasteboard/example/web/icons/Icon-maskable-512.png b/packages/pasteboard/example/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/packages/pasteboard/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/pasteboard/example/web/index.html b/packages/pasteboard/example/web/index.html new file mode 100644 index 00000000..394487c7 --- /dev/null +++ b/packages/pasteboard/example/web/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + pasteboard_example + + + + + + + + + + diff --git a/packages/pasteboard/example/web/manifest.json b/packages/pasteboard/example/web/manifest.json new file mode 100644 index 00000000..8a0cca41 --- /dev/null +++ b/packages/pasteboard/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "pasteboard_example", + "short_name": "pasteboard_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Demonstrates how to use the pasteboard plugin.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/pasteboard/example/windows/flutter/generated_plugins.cmake b/packages/pasteboard/example/windows/flutter/generated_plugins.cmake index b0870296..64141601 100644 --- a/packages/pasteboard/example/windows/flutter/generated_plugins.cmake +++ b/packages/pasteboard/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST pasteboard ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/pasteboard/lib/pasteboard.dart b/packages/pasteboard/lib/pasteboard.dart index 290d1fbb..da6f25ef 100644 --- a/packages/pasteboard/lib/pasteboard.dart +++ b/packages/pasteboard/lib/pasteboard.dart @@ -1,72 +1,42 @@ import 'dart:async'; -import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; +import 'src/pasteboard_platform_web.dart' + if (dart.library.io) 'src/pasteboard_platform_io.dart'; class Pasteboard { - static const MethodChannel _channel = MethodChannel('pasteboard'); - /// Returns the image data of the pasteboard. - static Future get image async { - final image = await _channel.invokeMethod('image'); - - if (image == null) { - return null; - } - if (Platform.isMacOS || Platform.isLinux || Platform.isIOS) { - return image as Uint8List; - } else if (Platform.isWindows) { - final file = File(image as String); - final bytes = await file.readAsBytes(); - await file.delete(); - return bytes; - } - return null; - } + /// + /// available on iOS and desktop. + static Future get image => pasteboard.image; /// only available on Windows /// Get "HTML format" from system pasteboard. - /// HTML format: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767917(v=vs.85) - /// - static Future get html async { - if (Platform.isWindows) { - return await _channel.invokeMethod('html') as String?; - } - return null; - } + /// HTML format: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767917(v=vs.85) + static Future get html => pasteboard.html; /// only available on iOS /// /// set image data to system pasteboard. - static Future writeImage(Uint8List? image) async { - if (image == null) { - return; - } - if (Platform.isIOS) { - await _channel.invokeMethod('writeImage', image); - } - } + static Future writeImage(Uint8List? image) => + pasteboard.writeImage(image); /// Only available on desktop platforms. /// /// Get files from system pasteboard. - static Future> files() async { - final files = await _channel.invokeMethod('files'); - return files?.cast() ?? const []; - } + static Future> files() => pasteboard.files(); /// Only available on desktop platforms. /// /// Set files to system pasteboard. - static Future writeFiles(List files) async { - try { - await _channel.invokeMethod('writeFiles', files); - return true; - } catch (e) { - debugPrint('$e'); - return false; - } - } + static Future writeFiles(List files) => + pasteboard.writeFiles(files); + + /// Available on all platforms. + /// Get text from system pasteboard. + static Future get text => pasteboard.text; + + /// Available on all platforms. + /// Set text to system pasteboard. + static void writeText(String value) => pasteboard.writeText(value); } diff --git a/packages/pasteboard/lib/src/pasteboard_platform.dart b/packages/pasteboard/lib/src/pasteboard_platform.dart new file mode 100644 index 00000000..e597903a --- /dev/null +++ b/packages/pasteboard/lib/src/pasteboard_platform.dart @@ -0,0 +1,17 @@ +import 'dart:typed_data'; + +abstract class PasteboardPlatform { + Future get image; + + Future get html; + + Future writeImage(Uint8List? image); + + Future> files(); + + Future writeFiles(List files); + + Future get text; + + void writeText(String value); +} diff --git a/packages/pasteboard/lib/src/pasteboard_platform_io.dart b/packages/pasteboard/lib/src/pasteboard_platform_io.dart new file mode 100644 index 00000000..dcf02d1f --- /dev/null +++ b/packages/pasteboard/lib/src/pasteboard_platform_io.dart @@ -0,0 +1,79 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'pasteboard_platform.dart'; + +const PasteboardPlatform pasteboard = PasteboardPlatformIO(); + +class PasteboardPlatformIO implements PasteboardPlatform { + const PasteboardPlatformIO(); + + static const MethodChannel _channel = MethodChannel('pasteboard'); + + @override + Future> files() async { + final files = await _channel.invokeMethod('files'); + return files?.cast() ?? const []; + } + + @override + Future get html async { + if (Platform.isWindows) { + return await _channel.invokeMethod('html') as String?; + } + return null; + } + + @override + Future get image async { + final image = await _channel.invokeMethod('image'); + + if (image == null) { + return null; + } + if (Platform.isMacOS || Platform.isLinux || Platform.isIOS) { + return image as Uint8List; + } else if (Platform.isWindows) { + final file = File(image as String); + final bytes = await file.readAsBytes(); + await file.delete(); + return bytes; + } + return null; + } + + @override + Future writeFiles(List files) async { + try { + await _channel.invokeMethod('writeFiles', files); + return true; + } catch (error, stacktrace) { + debugPrint('$error\n$stacktrace'); + return false; + } + } + + @override + Future writeImage(Uint8List? image) async { + if (image == null) { + return; + } + if (Platform.isIOS) { + await _channel.invokeMethod('writeImage', image); + } + } + + @override + Future get text async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + return data?.text; + } + + @override + void writeText(String value) { + Clipboard.setData(ClipboardData(text: value)); + } +} diff --git a/packages/pasteboard/lib/src/pasteboard_platform_web.dart b/packages/pasteboard/lib/src/pasteboard_platform_web.dart new file mode 100644 index 00000000..b44fa7ea --- /dev/null +++ b/packages/pasteboard/lib/src/pasteboard_platform_web.dart @@ -0,0 +1,92 @@ +// ignore: avoid_web_libraries_in_flutter +import 'dart:html'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; + +import 'pasteboard_platform.dart'; + +const PasteboardPlatform pasteboard = PasteboardPlatformWeb(); + +class PasteboardPlatformWeb implements PasteboardPlatform { + const PasteboardPlatformWeb(); + + @override + Future> files() async => const []; + + @override + Future get html async => null; + + @override + Future get image async => null; + + @override + Future writeFiles(List files) async => false; + + @override + Future writeImage(Uint8List? image) async {} + + @override + Future get text async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + return data?.text; + } + + @override + void writeText(String value) { + final fakeElement = _createCopyFakeElement(value)..select(); + document.body?.append(fakeElement); + _select(fakeElement); + _copyCommand(); + fakeElement.remove(); + } +} + +void _select(TextAreaElement element) { + final isReadOnly = element.hasAttribute('readonly'); + + if (!isReadOnly) { + element.setAttribute('readonly', ''); + } + + element + ..select() + ..setSelectionRange(0, element.value?.length ?? 0); + + if (!isReadOnly) { + element.removeAttribute('readonly'); + } +} + +/// https://github.com/zenorocha/clipboard.js/blob/master/src/common/create-fake-element.js +TextAreaElement _createCopyFakeElement(String value) { + final isRtl = document.documentElement?.getAttribute('dir') == 'rtl'; + final fakeElement = TextAreaElement() + // Prevent zooming on iOS + ..style.fontSize = '12pt' + // Reset box model + ..style.border = '0' + ..style.padding = '0' + // Move element out of screen horizontally + ..style.position = 'absolute' + ..style.setProperty(isRtl ? 'right' : 'left', '-9999px'); + + // Move element to the same position vertically + final yPosition = + window.pageYOffset | (document.documentElement?.scrollTop ?? 0); + fakeElement + ..style.top = '${yPosition}px' + ..setAttribute('readonly', '') + ..value = value; + return fakeElement; +} + +bool _copyCommand() { + try { + return document.execCommand('copy'); + } catch (error, stack) { + window.alert('$error, $stack'); + // ignore + return false; + } +} diff --git a/packages/pasteboard/pubspec.yaml b/packages/pasteboard/pubspec.yaml index ee35ff3b..00b24cc9 100644 --- a/packages/pasteboard/pubspec.yaml +++ b/packages/pasteboard/pubspec.yaml @@ -10,11 +10,13 @@ environment: dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter dev_dependencies: flutter_test: sdk: flutter - very_good_analysis: ^2.1.2 + flutter_lints: '^2.0.0' # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/pasteboard/test/pasteboard_test.dart b/packages/pasteboard/test/pasteboard_test.dart deleted file mode 100644 index 4f1a93f5..00000000 --- a/packages/pasteboard/test/pasteboard_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pasteboard/pasteboard.dart'; - -void main() { - const MethodChannel channel = MethodChannel('pasteboard'); - - TestWidgetsFlutterBinding.ensureInitialized(); - - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; - }); - }); - - tearDown(() { - channel.setMockMethodCallHandler(null); - }); - - test('getPlatformVersion', () async { - expect(await Pasteboard.platformVersion, '42'); - }); -}