Skip to content

Commit

Permalink
feat: support saving images, improving docs, and adding tests (#9)
Browse files Browse the repository at this point in the history
* feat: support saving an image to the gallery, improve docs, add tests, and other improvements

* chore(example): format AndroidManifest.xml using Android Studio

* chore(docs): minor changes in doc comments of QuillNativeBridge

* chore!: improve support for unit testing without workarounds (BREAKING CHANGE)

* chore: update min versions of platform implementation packages in quill_native_bridge
  • Loading branch information
EchoEllet authored Dec 1, 2024
1 parent 4f0e089 commit f588942
Show file tree
Hide file tree
Showing 107 changed files with 7,354 additions and 438 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# 🪶 Quill Native Bridge
# 🪶 Flutter quill_native_bridge plugin

An internal plugin for [`flutter_quill`](https://pub.dev/packages/flutter_quill) package to access platform-specific APIs.
An internal Flutter plugin for [`flutter_quill`](https://pub.dev/packages/flutter_quill) package to access platform-specific APIs,
built following the [federated plugin architecture](https://docs.google.com/document/d/1LD7QjmzJZLCopUrFAAE98wOUQpjmguyGTN2wd_89Srs/).
A detailed explanation of the federated plugin concept can be found in the [Flutter documentation](https://docs.flutter.dev/packages-and-plugins/developing-packages#federated-plugins).

> [!NOTE]
>
> **Internal Use Only**: Exclusively for `flutter_quill`. Breaking changes may occur.
This means the project is separated into the following packages:

For more details, refer to [quill_native_bridge](./quill_native_bridge/README.md).
1. [`quill_native_bridge`](https://pub.dev/packages/quill_native_bridge): The app-facing package that clients depend on to use the plugin. This package specifies the API used by the Flutter app.
2. [`quill_native_bridge_platform_interface`](https://pub.dev/packages/quill_native_bridge_platform_interface): The package that declares an interface that any platform package must implement to support the app-facing package.
3. The platform packages: One or more packages that contain the platform-specific implementation code. The app-facing package calls into these packages—they aren't included into an app, unless they contain platform-specific functionality:
* [`quill_native_bridge_android`](https://pub.dev/packages/quill_native_bridge_android)
* [`quill_native_bridge_ios`](https://pub.dev/packages/quill_native_bridge_ios)
* [`quill_native_bridge_macos`](https://pub.dev/packages/quill_native_bridge_macos)
* [`quill_native_bridge_linux`](https://pub.dev/packages/quill_native_bridge_linux)
* [`quill_native_bridge_windows`](https://pub.dev/packages/quill_native_bridge_windows)
* [`quill_native_bridge_web`](https://pub.dev/packages/quill_native_bridge_web)

For more details, refer to [quill_native_bridge README](./quill_native_bridge/README.md).
12 changes: 12 additions & 0 deletions quill_native_bridge/.pubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
readme_assets/
example/assets/

example/android/
example/ios/
example/macos/
example/web/
example/windows/
example/linux/
example/test_driver/
example/integration_test/
test/
10 changes: 10 additions & 0 deletions quill_native_bridge/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to this project will be documented in this file.

## 11.0.0

- Improves `README.md`. Adds more details to `README.md`.
- Updates the section `Platform configuration` to `Setup` in `README.md`.
- Improves doc comments.
- Adds unit tests for `QuillNativeBridge`.
- Adds support for saving images.
- Updates Java compatibility version to 11. Related [flutter#156111](https://github.com/flutter/flutter/issues/156111).
- **BREAKING CHANGE**: Converted all static methods in `QuillNativeBridge` to instance methods to improve unit testing and extensibility.

## 10.7.11

- Adds pub topics to package metadata.
Expand Down
267 changes: 254 additions & 13 deletions quill_native_bridge/README.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion quill_native_bridge/example/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# 🪶 Quill Native Bridge Example

Demonstrates the usage of [`quill_native_bridge`](https://pub.dev/packages/quill_native_bridge) plugin.
Demonstrates the usage of [`quill_native_bridge`](https://pub.dev/packages/quill_native_bridge) plugin.

Refer to [example_test.dart](./test/example_test.dart) for a minimal example of mocking `QuillNativeBridge` for tests.
6 changes: 3 additions & 3 deletions quill_native_bridge/example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ android {
ndkVersion = flutter.ndkVersion

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
jvmTarget = JavaVersion.VERSION_11
}

defaultConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- START: Required for saving images to the gallery on Android 9 (API 28) and earlier -->
<!-- More details: https://pub.dev/packages/quill_native_bridge#-setup -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- END: Required for saving images to the gallery on Android 9 (API 28) and earlier -->

<application
android:label="quill_native_bridge_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="quill_native_bridge_example">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
Expand All @@ -31,15 +38,18 @@
android:name="flutterEmbedding"
android:value="2" />

<!-- START: Required for copying images to the system clipboard -->
<!-- More details: https://pub.dev/packages/quill_native_bridge#-setup -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- END: Required for copying images to the system clipboard -->
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
Expand All @@ -48,8 +58,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
4 changes: 2 additions & 2 deletions quill_native_bridge/example/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pluginManagement {

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id "com.android.application" version "8.7.2" apply false
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
}

include ":app"
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added quill_native_bridge/example/assets/loading.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'package:flutter/services.dart' as services
show Clipboard, ClipboardData;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image_compare/image_compare.dart';
import 'package:integration_test/integration_test.dart';
import 'package:quill_native_bridge/quill_native_bridge.dart';
import 'package:quill_native_bridge_example/assets.dart';
import 'package:quill_native_bridge_example/main.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand All @@ -15,8 +16,8 @@ void main() {
() async {
Future<void> verifyImageCopiedToClipboard(String assetPath) async {
final imageBytes = await loadAssetFile(assetPath);
await QuillNativeBridge.copyImageToClipboard(imageBytes);
final clipboardImageBytes = await QuillNativeBridge.getClipboardImage();
await quillNativeBridge.copyImageToClipboard(imageBytes);
final clipboardImageBytes = await quillNativeBridge.getClipboardImage();
final pixelMismatchPercentage =
await compareImages(src1: imageBytes, src2: clipboardImageBytes);
expect(pixelMismatchPercentage, 0);
Expand All @@ -34,10 +35,10 @@ void main() {
final imageBytes = await loadAssetFile(kFlutterQuillAssetImage);
final imageBytes2 = await loadAssetFile(kQuillJsRichTextEditor);

await QuillNativeBridge.copyImageToClipboard(imageBytes);
await QuillNativeBridge.copyImageToClipboard(imageBytes2);
await quillNativeBridge.copyImageToClipboard(imageBytes);
await quillNativeBridge.copyImageToClipboard(imageBytes2);

final clipboardImageBytes = await QuillNativeBridge.getClipboardImage();
final clipboardImageBytes = await quillNativeBridge.getClipboardImage();
final pixelMismatchPercentage =
await compareImages(src1: imageBytes, src2: clipboardImageBytes);
expect(pixelMismatchPercentage, isNot(0));
Expand All @@ -53,8 +54,8 @@ void main() {
test('copying HTML to the clipboard should make it accessible', () async {
const htmlToCopy =
'<div class="container"><h1>Test Document</h1><p>This is a <strong>sample</strong> paragraph with <a href="https://example.com">a link</a> and some <span style="color:red;">red text</span>.</p><ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul><footer>Footer content here</footer></div>';
await QuillNativeBridge.copyHtmlToClipboard(htmlToCopy);
final clipboardHtml = await QuillNativeBridge.getClipboardHtml();
await quillNativeBridge.copyHtmlToClipboard(htmlToCopy);
final clipboardHtml = await quillNativeBridge.getClipboardHtml();
expect(htmlToCopy, clipboardHtml);
});

Expand All @@ -63,10 +64,10 @@ void main() {
const html1 = '<pre style="font-family: monospace;">HTML</pre>';
const html2 = '<div style="border: 1px solid;">HTML Div</div>';

await QuillNativeBridge.copyHtmlToClipboard(html1);
await QuillNativeBridge.copyHtmlToClipboard(html2);
await quillNativeBridge.copyHtmlToClipboard(html1);
await quillNativeBridge.copyHtmlToClipboard(html2);

final clipboardHtml = await QuillNativeBridge.getClipboardHtml();
final clipboardHtml = await quillNativeBridge.getClipboardHtml();
expect(clipboardHtml, isNot(html1));
expect(clipboardHtml, html2);
});
Expand All @@ -80,44 +81,43 @@ void main() {

// Copy HTML to clipboard before copying an image

await QuillNativeBridge.copyHtmlToClipboard(html);
await quillNativeBridge.copyHtmlToClipboard(html);

expect(
await QuillNativeBridge.getClipboardHtml(),
await quillNativeBridge.getClipboardHtml(),
html,
);

// Image clipboard item
final imageBytes = await loadAssetFile(kFlutterQuillAssetImage);
await QuillNativeBridge.copyImageToClipboard(imageBytes);
await quillNativeBridge.copyImageToClipboard(imageBytes);

expect(
await QuillNativeBridge.getClipboardHtml(),
await quillNativeBridge.getClipboardHtml(),
null,
);

// Copy HTML to clipboard before copying plain text

await QuillNativeBridge.copyHtmlToClipboard(html);
await quillNativeBridge.copyHtmlToClipboard(html);

expect(
await QuillNativeBridge.getClipboardHtml(),
await quillNativeBridge.getClipboardHtml(),
html,
);

// Plain text clipboard item
const plainTextExample = 'Flutter Quill';
services.Clipboard.setData(
const services.ClipboardData(text: plainTextExample),
Clipboard.setData(
const ClipboardData(text: plainTextExample),
);
expect(
(await services.Clipboard.getData(services.Clipboard.kTextPlain))
?.text,
(await Clipboard.getData(Clipboard.kTextPlain))?.text,
plainTextExample,
);

expect(
await QuillNativeBridge.getClipboardHtml(),
await quillNativeBridge.getClipboardHtml(),
null,
);
},
Expand All @@ -130,8 +130,8 @@ void main() {
() async {
const exampleHtml = '<div style="border: 1px solid;">HTML Div</div>';

await QuillNativeBridge.copyHtmlToClipboard(exampleHtml);
final clipboardHtml = await QuillNativeBridge.getClipboardHtml();
await quillNativeBridge.copyHtmlToClipboard(exampleHtml);
final clipboardHtml = await quillNativeBridge.getClipboardHtml();

if (clipboardHtml == null) {
fail(
Expand All @@ -150,4 +150,31 @@ void main() {
},
);
});

group(
'saveImageToGallery',
() {
test('throws an error if image bytes are invalid', () async {
if (!(await quillNativeBridge
.isSupported(QuillNativeBridgeFeature.saveImageToGallery))) {
markTestSkipped(
'The platform $defaultTargetPlatform does not apply to save images to the gallery feature');
return;
}
await expectLater(
quillNativeBridge.saveImageToGallery(Uint8List.fromList([1, 0, 1]),
options: const GalleryImageSaveOptions(
name: 'ExampleImageName',
fileExtension: 'png',
albumName: null,
)),
throwsA(isA<PlatformException>().having(
(e) => e.code,
'code',
equals('INVALID_IMAGE'),
)),
);
});
},
);
}
12 changes: 12 additions & 0 deletions quill_native_bridge/example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,17 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>

<!-- START: Add-only permission: Required on iOS 14 and later if the album name is not specified -->
<!-- More details: https://pub.dev/packages/quill_native_bridge#-setup -->
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Used to demonstrate quill_native_bridge plugin</string>
<!-- END: Add-only permission: Required on iOS 14 and later if the album name is not specified -->

<!-- START: Read-write permission: Always required on iOS 13 and earlier, and also required on iOS 14 and later if the album name is specified -->
<!-- More details: https://pub.dev/packages/quill_native_bridge#-setup -->
<key>NSPhotoLibraryUsageDescription</key>
<string>Used to demonstrate quill_native_bridge plugin on iOS 13 and earlier or if the album was specified</string>
<!-- END: Read-write permission: Always required on iOS 13 and earlier, and also required on iOS 14 and later if the album name is specified -->
</dict>
</plist>
Loading

0 comments on commit f588942

Please sign in to comment.