diff --git a/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift b/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift
index d906c508d9..dc3b203eab 100644
--- a/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,12 +5,16 @@
import FlutterMacOS
import Foundation
+import flowy_editor
import flowy_sdk
import path_provider_macos
+import url_launcher_macos
import window_size
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ FlowyEditorPlugin.register(with: registry.registrar(forPlugin: "FlowyEditorPlugin"))
FlowySdkPlugin.register(with: registry.registrar(forPlugin: "FlowySdkPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin"))
}
diff --git a/app_flowy/macos/Podfile.lock b/app_flowy/macos/Podfile.lock
index 2260813685..599cd8de01 100644
--- a/app_flowy/macos/Podfile.lock
+++ b/app_flowy/macos/Podfile.lock
@@ -1,34 +1,46 @@
PODS:
+ - flowy_editor (0.0.1):
+ - FlutterMacOS
- flowy_sdk (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- path_provider_macos (0.0.1):
- FlutterMacOS
+ - url_launcher_macos (0.0.1):
+ - FlutterMacOS
- window_size (0.0.2):
- FlutterMacOS
DEPENDENCIES:
+ - flowy_editor (from `Flutter/ephemeral/.symlinks/plugins/flowy_editor/macos`)
- flowy_sdk (from `Flutter/ephemeral/.symlinks/plugins/flowy_sdk/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
+ - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
EXTERNAL SOURCES:
+ flowy_editor:
+ :path: Flutter/ephemeral/.symlinks/plugins/flowy_editor/macos
flowy_sdk:
:path: Flutter/ephemeral/.symlinks/plugins/flowy_sdk/macos
FlutterMacOS:
:path: Flutter/ephemeral
path_provider_macos:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
+ url_launcher_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
window_size:
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
SPEC CHECKSUMS:
+ flowy_editor: 26060a984848e6afac1f6a4455511f4114119d8d
flowy_sdk: c302ac0a22dea596db0df8073b9637b2bf2ff6fd
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b
+ url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
-COCOAPODS: 1.10.1
+COCOAPODS: 1.9.3
diff --git a/app_flowy/packages/flowy_editor/.gitignore b/app_flowy/packages/flowy_editor/.gitignore
new file mode 100644
index 0000000000..e9dc58d3d6
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+.dart_tool/
+
+.packages
+.pub/
+
+build/
diff --git a/app_flowy/packages/flowy_editor/.metadata b/app_flowy/packages/flowy_editor/.metadata
new file mode 100644
index 0000000000..90455796c5
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/.metadata
@@ -0,0 +1,10 @@
+# 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.
+
+version:
+ revision: b7d4806243a4e906bf061f79a0e314ba28111aa6
+ channel: dev
+
+project_type: plugin
diff --git a/app_flowy/packages/flowy_editor/.vscode/launch.json b/app_flowy/packages/flowy_editor/.vscode/launch.json
new file mode 100644
index 0000000000..ebad467a72
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/.vscode/launch.json
@@ -0,0 +1,19 @@
+{
+ // 使用 IntelliSense 了解相关属性。
+ // 悬停以查看现有属性的描述。
+ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "flowy_editor",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flowy_editor example",
+ "cwd": "example",
+ "request": "launch",
+ "type": "dart"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app_flowy/packages/flowy_editor/CHANGELOG.md b/app_flowy/packages/flowy_editor/CHANGELOG.md
new file mode 100644
index 0000000000..41cc7d8192
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.
diff --git a/app_flowy/packages/flowy_editor/LICENSE b/app_flowy/packages/flowy_editor/LICENSE
new file mode 100644
index 0000000000..ba75c69f7f
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/LICENSE
@@ -0,0 +1 @@
+TODO: Add your license here.
diff --git a/app_flowy/packages/flowy_editor/README.md b/app_flowy/packages/flowy_editor/README.md
new file mode 100644
index 0000000000..23c1259570
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/README.md
@@ -0,0 +1,14 @@
+# flowy_editor
+
+A new flutter plugin project.
+
+## Getting Started
+
+This project is a starting point for a Flutter
+[plug-in package](https://flutter.dev/developing-packages/),
+a specialized package that includes platform-specific implementation code for
+Android and/or iOS.
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.dev/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
\ No newline at end of file
diff --git a/app_flowy/packages/flowy_editor/analysis_options.yaml b/app_flowy/packages/flowy_editor/analysis_options.yaml
new file mode 100644
index 0000000000..358d9a7f1d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/analysis_options.yaml
@@ -0,0 +1,8 @@
+include: package:pedantic/analysis_options.yaml
+analyzer:
+ exclude:
+ - "**/*.g.dart"
+ - "**/*.freezed.dart"
+
+
+# https://dart.dev/guides/language/analysis-options
\ No newline at end of file
diff --git a/app_flowy/packages/flowy_editor/android/.gitignore b/app_flowy/packages/flowy_editor/android/.gitignore
new file mode 100644
index 0000000000..c6cbe562a4
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/android/.gitignore
@@ -0,0 +1,8 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
diff --git a/app_flowy/packages/flowy_editor/android/build.gradle b/app_flowy/packages/flowy_editor/android/build.gradle
new file mode 100644
index 0000000000..8ca243c8b7
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/android/build.gradle
@@ -0,0 +1,40 @@
+group 'com.plugin.flowy_editor'
+version '1.0-SNAPSHOT'
+
+buildscript {
+ ext.kotlin_version = '1.3.50'
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+rootProject.allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion 30
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+ defaultConfig {
+ minSdkVersion 16
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/app_flowy/packages/flowy_editor/android/gradle.properties b/app_flowy/packages/flowy_editor/android/gradle.properties
new file mode 100644
index 0000000000..94adc3a3f9
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/app_flowy/packages/flowy_editor/android/gradle/wrapper/gradle-wrapper.properties b/app_flowy/packages/flowy_editor/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..3c9d0852bf
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/app_flowy/packages/flowy_editor/android/settings.gradle b/app_flowy/packages/flowy_editor/android/settings.gradle
new file mode 100644
index 0000000000..d4d969b6de
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'flowy_editor'
diff --git a/app_flowy/packages/flowy_editor/android/src/main/AndroidManifest.xml b/app_flowy/packages/flowy_editor/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..620513d130
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/app_flowy/packages/flowy_editor/android/src/main/kotlin/com/plugin/flowy_editor/FlowyEditorPlugin.kt b/app_flowy/packages/flowy_editor/android/src/main/kotlin/com/plugin/flowy_editor/FlowyEditorPlugin.kt
new file mode 100644
index 0000000000..eb18984fb4
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/android/src/main/kotlin/com/plugin/flowy_editor/FlowyEditorPlugin.kt
@@ -0,0 +1,36 @@
+package com.plugin.flowy_editor
+
+import androidx.annotation.NonNull
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import io.flutter.plugin.common.MethodChannel.Result
+import io.flutter.plugin.common.PluginRegistry.Registrar
+
+/** FlowyEditorPlugin */
+class FlowyEditorPlugin: FlutterPlugin, MethodCallHandler {
+ /// The MethodChannel that will the communication between Flutter and native Android
+ ///
+ /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+ /// when the Flutter Engine is detached from the Activity
+ private lateinit var channel : MethodChannel
+
+ override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flowy_editor")
+ channel.setMethodCallHandler(this)
+ }
+
+ override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
+ if (call.method == "getPlatformVersion") {
+ result.success("Android ${android.os.Build.VERSION.RELEASE}")
+ } else {
+ result.notImplemented()
+ }
+ }
+
+ override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
+ channel.setMethodCallHandler(null)
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/.gitignore b/app_flowy/packages/flowy_editor/example/.gitignore
new file mode 100644
index 0000000000..0fa6b675c0
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/.gitignore
@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/app_flowy/packages/flowy_editor/example/.metadata b/app_flowy/packages/flowy_editor/example/.metadata
new file mode 100644
index 0000000000..7aa657dd7f
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/.metadata
@@ -0,0 +1,10 @@
+# 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.
+
+version:
+ revision: b7d4806243a4e906bf061f79a0e314ba28111aa6
+ channel: dev
+
+project_type: app
diff --git a/app_flowy/packages/flowy_editor/example/README.md b/app_flowy/packages/flowy_editor/example/README.md
new file mode 100644
index 0000000000..6a8d0dfd92
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/README.md
@@ -0,0 +1,16 @@
+# flowy_editor_example
+
+Demonstrates how to use the flowy_editor plugin.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.dev/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/app_flowy/packages/flowy_editor/example/android/.gitignore b/app_flowy/packages/flowy_editor/example/android/.gitignore
new file mode 100644
index 0000000000..0a741cb43d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/app_flowy/packages/flowy_editor/example/android/app/build.gradle b/app_flowy/packages/flowy_editor/example/android/app/build.gradle
new file mode 100644
index 0000000000..c463866b0c
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/build.gradle
@@ -0,0 +1,59 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 30
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.plugin.flowy_editor_example"
+ minSdkVersion 16
+ targetSdkVersion 30
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml b/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000000..17e44851a2
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml b/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1e79da5ec1
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/plugin/flowy_editor_example/MainActivity.kt b/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/plugin/flowy_editor_example/MainActivity.kt
new file mode 100644
index 0000000000..e875c46b05
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/plugin/flowy_editor_example/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.plugin.flowy_editor_example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000000..f74085f3f6
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000000..304732f884
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..db77bb4b7b
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..17987b79bb
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..09d4391482
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..d5f1c8d34e
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..4d6372eebd
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000000..449a9f9308
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..d74aa35c28
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml b/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000000..17e44851a2
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/android/build.gradle b/app_flowy/packages/flowy_editor/example/android/build.gradle
new file mode 100644
index 0000000000..c505a86352
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ ext.kotlin_version = '1.3.50'
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/app_flowy/packages/flowy_editor/example/android/gradle.properties b/app_flowy/packages/flowy_editor/example/android/gradle.properties
new file mode 100644
index 0000000000..94adc3a3f9
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties b/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..bc6a58afdd
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/app_flowy/packages/flowy_editor/example/android/settings.gradle b/app_flowy/packages/flowy_editor/example/android/settings.gradle
new file mode 100644
index 0000000000..44e62bcf06
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/app_flowy/packages/flowy_editor/example/assets/block_document.fdoc b/app_flowy/packages/flowy_editor/example/assets/block_document.fdoc
new file mode 100644
index 0000000000..1f0ad033ad
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/assets/block_document.fdoc
@@ -0,0 +1,33 @@
+[
+ {
+ "insert":"This is a checkbox"
+ },
+ {
+ "attributes":{
+ "list":"checked"
+ },
+ "insert":"\n"
+ },
+ {
+ "insert": {
+ "flutter_logo": ""
+ },
+ "attributes":{
+ "size": 200.0
+ }
+ },
+ {
+ "insert":"\n"
+ },
+ {
+ "insert": {
+ "test_block_type": "test_data"
+ },
+ "attributes":{
+
+ }
+ },
+ {
+ "insert":"\n"
+ }
+]
\ No newline at end of file
diff --git a/app_flowy/packages/flowy_editor/example/assets/long_document.fdoc b/app_flowy/packages/flowy_editor/example/assets/long_document.fdoc
new file mode 100644
index 0000000000..0c3be4df72
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/assets/long_document.fdoc
@@ -0,0 +1,95 @@
+[
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":1
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":2
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":3
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor\n"
+ },
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":1
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":2
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":3
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor\n"
+ },
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":1
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":2
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "heading":3
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Flowy Editor\n"
+ },
+ {
+ "insert":"\n"
+ }
+]
\ No newline at end of file
diff --git a/app_flowy/packages/flowy_editor/example/assets/plain_text_document.fdoc b/app_flowy/packages/flowy_editor/example/assets/plain_text_document.fdoc
new file mode 100644
index 0000000000..bfcccda58d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/assets/plain_text_document.fdoc
@@ -0,0 +1,125 @@
+[
+ {
+ "insert":"Flowy Editor"
+ },
+ {
+ "attributes":{
+ "header":1
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"This is the test document for flowy editor.\nTest the decoding Delta and renderiing attributed text.\nTest Content"
+ },
+ {
+ "attributes":{
+ "header":2
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"This is a plain text.\nThis is a quote text."
+ },
+ {
+ "attributes":{
+ "quote_block":true
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"This line contains a "
+ },
+ {
+ "attributes":{
+ "bold":true
+ },
+ "insert":"bold"
+ },
+ {
+ "insert":" word, also, "
+ },
+ {
+ "attributes":{
+ "italic":true
+ },
+ "insert":"italic"
+ },
+ {
+ "insert":" word here, and "
+ },
+ {
+ "attributes":{
+ "strikethrough":true
+ },
+ "insert":"strikethrough"
+ },
+ {
+ "insert": " and "
+ },
+ {
+ "attributes":{
+ "underline":true
+ },
+ "insert":"underline"
+ },
+ {
+ "insert":" etc...\n"
+ },
+ {
+ "attributes":{
+ "link":"appflowy.com"
+ },
+ "insert":"AppFlowy.com"
+ },
+ {
+ "insert":"\nEl"
+ },
+ {
+ "attributes":{
+ "bold":true
+ },
+ "insert": "eme"
+ },
+ {
+ "insert": "nt 1"
+ },
+ {
+ "attributes":{
+ "list":"bullet"
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Element 2"
+ },
+ {
+ "attributes":{
+ "list":"bullet"
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Choice 1"
+ },
+ {
+ "attributes":{
+ "list":"ordered"
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"Choice 2"
+ },
+ {
+ "attributes":{
+ "list":"ordered"
+ },
+ "insert":"\n"
+ },
+ {
+ "insert":"\nThis is the content of a checkbox"
+ },
+ {
+ "insert":"\n"
+ }
+]
\ No newline at end of file
diff --git a/app_flowy/packages/flowy_editor/example/ios/.gitignore b/app_flowy/packages/flowy_editor/example/ios/.gitignore
new file mode 100644
index 0000000000..e96ef602b8
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist b/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000000..9367d483e4
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 8.0
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig b/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000000..ec97fc6f30
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig b/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000000..c4855bfe20
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/app_flowy/packages/flowy_editor/example/ios/Podfile b/app_flowy/packages/flowy_editor/example/ios/Podfile
new file mode 100644
index 0000000000..1e8c3c90a5
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Podfile
@@ -0,0 +1,41 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/app_flowy/packages/flowy_editor/example/ios/Podfile.lock b/app_flowy/packages/flowy_editor/example/ios/Podfile.lock
new file mode 100644
index 0000000000..cfcdf38dfe
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Podfile.lock
@@ -0,0 +1,40 @@
+PODS:
+ - flowy_editor (0.0.1):
+ - Flutter
+ - Flutter (1.0.0)
+ - flutter_keyboard_visibility (0.0.1):
+ - Flutter
+ - path_provider (0.0.1):
+ - Flutter
+ - url_launcher (0.0.1):
+ - Flutter
+
+DEPENDENCIES:
+ - flowy_editor (from `.symlinks/plugins/flowy_editor/ios`)
+ - Flutter (from `Flutter`)
+ - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
+ - path_provider (from `.symlinks/plugins/path_provider/ios`)
+ - url_launcher (from `.symlinks/plugins/url_launcher/ios`)
+
+EXTERNAL SOURCES:
+ flowy_editor:
+ :path: ".symlinks/plugins/flowy_editor/ios"
+ Flutter:
+ :path: Flutter
+ flutter_keyboard_visibility:
+ :path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
+ path_provider:
+ :path: ".symlinks/plugins/path_provider/ios"
+ url_launcher:
+ :path: ".symlinks/plugins/url_launcher/ios"
+
+SPEC CHECKSUMS:
+ flowy_editor: bf8d58894ddb03453bd4d8521c57267ad638b837
+ Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
+ flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
+ path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
+ url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
+
+PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
+
+COCOAPODS: 1.9.3
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..14684c0039
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,546 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ E3DDC456A090874E3DDA4243 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AC220F65A26B2252F1DA215 /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 0ADA789FA5EE9F1E98B5777F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 207AE31B53F0717E0281FD87 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 3BAB9388A73799EC42C6C044 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 9AC220F65A26B2252F1DA215 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E3DDC456A090874E3DDA4243 /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 597674AF1FE94F975393FEB7 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 9AC220F65A26B2252F1DA215 /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ F25CAD61A400EA4A979759D7 /* Pods */,
+ 597674AF1FE94F975393FEB7 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ F25CAD61A400EA4A979759D7 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 3BAB9388A73799EC42C6C044 /* Pods-Runner.debug.xcconfig */,
+ 0ADA789FA5EE9F1E98B5777F /* Pods-Runner.release.xcconfig */,
+ 207AE31B53F0717E0281FD87 /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 5B014E49941065F7FD60552C /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 3083A35B476F73AE59140313 /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1020;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3083A35B476F73AE59140313 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/flowy_editor/flowy_editor.framework",
+ "${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework",
+ "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
+ "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flowy_editor.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 5B014E49941065F7FD60552C /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.plugin.flowyEditorExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.plugin.flowyEditorExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.plugin.flowyEditorExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..919434a625
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000000..f9b0d7c5ea
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000000..a28140cfdb
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..21a3cc14c7
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000000..f9b0d7c5ea
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift b/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000000..70693e4a8c
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..d36b1fab2d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000000..dc9ada4725
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000000..28c6bf0301
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000000..2ccbfd967d
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000000..f091b6b0bc
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000000..4cde12118d
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000000..d0ef06e7ed
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000000..dcdc2306c2
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000000..2ccbfd967d
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000000..c8f9ed8f5c
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000000..a6d6b8609d
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000000..a6d6b8609d
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000000..75b2d164a5
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000000..c4df70d39d
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000000..6a84f41e14
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000000..d0e1f58536
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000000..0bedcf2fd4
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000000..9da19eacad
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000000..9da19eacad
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000000..9da19eacad
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000000..89c2725b70
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000000..f2e259c7c9
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard b/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000000..f3c28516fb
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist b/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist
new file mode 100644
index 0000000000..b0292d8b37
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ flowy_editor_example
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h b/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000000..308a2a560b
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/app_flowy/packages/flowy_editor/example/lib/home_screen.dart b/app_flowy/packages/flowy_editor/example/lib/home_screen.dart
new file mode 100644
index 0000000000..3c9bf8e9f8
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/lib/home_screen.dart
@@ -0,0 +1,67 @@
+import 'widgets/home_drawer.dart';
+import 'widgets/editor_scaffold.dart';
+import 'package:flutter/material.dart';
+
+final flowyDocs = [
+ 'plain_text_document.fdoc',
+ 'block_document.fdoc',
+ 'long_document.fdoc',
+];
+
+class HomeScreen extends StatefulWidget {
+ @override
+ _HomeScreenState createState() => _HomeScreenState();
+}
+
+class _HomeScreenState extends State {
+ String filename;
+ Widget _editor;
+
+ @override
+ void initState() {
+ filename = flowyDocs[1];
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return HomeDrawer(
+ drawer: Container(
+ constraints: BoxConstraints(minWidth: 250, maxWidth: 250),
+ color: Colors.white,
+ child: ListView.separated(
+ itemBuilder: (context, index) {
+ return GestureDetector(
+ onTap: () => _selectDoc(index),
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ flowyDocs[index],
+ style: TextStyle(fontSize: 16.0, color: Colors.black54),
+ ),
+ ),
+ ),
+ );
+ },
+ itemCount: flowyDocs.length,
+ separatorBuilder: (context, index) => Divider(),
+ ),
+ ),
+ body: _editor ?? _homepageEditor(),
+ );
+ }
+
+ Widget _homepageEditor() {
+ return EditorScaffold(filename: filename);
+ }
+
+ void _selectDoc(int index) {
+ final filename = flowyDocs[index];
+ setState(() {
+ _editor = null;
+ this.filename = filename;
+ });
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/lib/main.dart b/app_flowy/packages/flowy_editor/example/lib/main.dart
new file mode 100644
index 0000000000..bb88e683af
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/lib/main.dart
@@ -0,0 +1,21 @@
+import 'home_screen.dart';
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(MyApp());
+}
+
+class MyApp extends StatefulWidget {
+ @override
+ _MyAppState createState() => _MyAppState();
+}
+
+class _MyAppState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ debugShowCheckedModeBanner: false,
+ home: HomeScreen(),
+ );
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/lib/widgets/editor_scaffold.dart b/app_flowy/packages/flowy_editor/example/lib/widgets/editor_scaffold.dart
new file mode 100644
index 0000000000..a88aa88d4d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/lib/widgets/editor_scaffold.dart
@@ -0,0 +1,130 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+class EditorScaffold extends StatefulWidget {
+ final String filename;
+
+ const EditorScaffold({
+ Key key,
+ @required this.filename,
+ }) : super(key: key);
+
+ @override
+ _EditorScaffoldState createState() => _EditorScaffoldState();
+}
+
+class _EditorScaffoldState extends State {
+ EditorController _controller;
+ String _filename;
+ final FocusNode _focusNode = FocusNode();
+
+ bool _loading = false;
+ String errorMsg;
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (_controller == null && !_loading) {
+ _load();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ Widget body;
+ if (_loading) {
+ body = Center(child: Text('Loading...'));
+ } else if (_filename != widget.filename) {
+ _load();
+ } else if (_controller == null) {
+ body = _zeroStateView();
+ } else {
+ final editor = FlowyEditor(
+ controller: _controller,
+ focusNode: _focusNode,
+ scrollable: true,
+ autoFocus: false,
+ expands: false,
+ padding: EdgeInsets.symmetric(horizontal: 8.0),
+ readOnly: false,
+ scrollBottomInset: 0,
+ scrollController: ScrollController(),
+ );
+ body = SafeArea(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Container(
+ child: editor,
+ )),
+ Container(
+ child: FlowyToolbar.basic(
+ controller: _controller,
+ onImageSelectCallback: _onImageSelection,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return Scaffold(
+ appBar: AppBar(
+ elevation: 0,
+ title: Text(widget.filename),
+ ),
+ body: body,
+ );
+ }
+
+ Future _load() async {
+ _filename = widget.filename;
+ try {
+ final docJson = await rootBundle.loadString('assets/${widget.filename}');
+ final doc = Document.fromJson(jsonDecode(docJson));
+ setState(() {
+ _controller = EditorController(
+ document: doc,
+ selection: const TextSelection.collapsed(offset: 0),
+ );
+ _loading = false;
+ });
+ } catch (error) {
+ setState(() {
+ _loading = false;
+ errorMsg = error.toString();
+ });
+ }
+ }
+
+ Widget _zeroStateView() {
+ return Container(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(Icons.error, color: Colors.red),
+ Text('Error: ${errorMsg ?? "unknow"}'),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Future _onImageSelection(File file) {
+ // TODO: Impl this
+ return Future.value('');
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/lib/widgets/home_drawer.dart b/app_flowy/packages/flowy_editor/example/lib/widgets/home_drawer.dart
new file mode 100644
index 0000000000..ac345d51d3
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/lib/widgets/home_drawer.dart
@@ -0,0 +1,49 @@
+import 'package:flutter/material.dart';
+
+class HomeDrawer extends StatelessWidget {
+ final Widget drawer;
+ final Widget body;
+
+ const HomeDrawer({
+ Key key,
+ this.drawer,
+ this.body,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ final appBar = AppBar(
+ title: Text('Flowy Editor Example'),
+ );
+ if (constraints.maxWidth < 800) {
+ return Scaffold(
+ appBar: appBar,
+ drawer: drawer,
+ body: body,
+ );
+ }
+ return Scaffold(
+ body: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Container(
+ constraints: BoxConstraints(minWidth: 250, maxWidth: 250),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ appBar,
+ Expanded(child: drawer),
+ ],
+ ),
+ ),
+ VerticalDivider(width: 1.0),
+ Expanded(child: body),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/macos/.gitignore b/app_flowy/packages/flowy_editor/example/macos/.gitignore
new file mode 100644
index 0000000000..d2fd377230
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/.gitignore
@@ -0,0 +1,6 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/xcuserdata/
diff --git a/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig b/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000000..4b81f9b2d2
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig b/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000000..5caa9d1579
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000000..047940f12f
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,16 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import flowy_editor
+import path_provider_macos
+import url_launcher_macos
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ FlowyEditorPlugin.register(with: registry.registrar(forPlugin: "FlowyEditorPlugin"))
+ PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+}
diff --git a/app_flowy/packages/flowy_editor/example/macos/Podfile b/app_flowy/packages/flowy_editor/example/macos/Podfile
new file mode 100644
index 0000000000..dade8dfad0
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Podfile
@@ -0,0 +1,40 @@
+platform :osx, '10.11'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_macos_build_settings(target)
+ end
+end
diff --git a/app_flowy/packages/flowy_editor/example/macos/Podfile.lock b/app_flowy/packages/flowy_editor/example/macos/Podfile.lock
new file mode 100644
index 0000000000..9428dcb655
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Podfile.lock
@@ -0,0 +1,34 @@
+PODS:
+ - flowy_editor (0.0.1):
+ - FlutterMacOS
+ - FlutterMacOS (1.0.0)
+ - path_provider_macos (0.0.1):
+ - FlutterMacOS
+ - url_launcher_macos (0.0.1):
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - flowy_editor (from `Flutter/ephemeral/.symlinks/plugins/flowy_editor/macos`)
+ - FlutterMacOS (from `Flutter/ephemeral`)
+ - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
+ - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
+
+EXTERNAL SOURCES:
+ flowy_editor:
+ :path: Flutter/ephemeral/.symlinks/plugins/flowy_editor/macos
+ FlutterMacOS:
+ :path: Flutter/ephemeral
+ path_provider_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
+ url_launcher_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
+
+SPEC CHECKSUMS:
+ flowy_editor: 26060a984848e6afac1f6a4455511f4114119d8d
+ FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+ path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b
+ url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
+
+PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
+
+COCOAPODS: 1.9.3
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj b/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..7dafd979d7
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,632 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 51;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 0C27A898F19849756B2961F2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C702E5916917E3066B271F03 /* Pods_Runner.framework */; };
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* flowy_editor_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flowy_editor_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 7C0A6189862111BCAA63B271 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 8F992DDDDDA933A327DC365B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+ C702E5916917E3066B271F03 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ E8F099538E88718D3391B642 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0C27A898F19849756B2961F2 /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ E817C7529977AC9EA3A99B00 /* Pods */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* flowy_editor_example.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "";
+ };
+ 33CEB47122A05771004F2AC0 /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+ );
+ path = Flutter;
+ sourceTree = "";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ C702E5916917E3066B271F03 /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ E817C7529977AC9EA3A99B00 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ E8F099538E88718D3391B642 /* Pods-Runner.debug.xcconfig */,
+ 8F992DDDDDA933A327DC365B /* Pods-Runner.release.xcconfig */,
+ 7C0A6189862111BCAA63B271 /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 0504CFEF98C55A917E5E1B29 /* [CP] Check Pods Manifest.lock */,
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ AD8696D5D39F3E86C935FCC3 /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* flowy_editor_example.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 0930;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 0504CFEF98C55A917E5E1B29 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+ };
+ 33CC111E2044C6BF0003C045 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ Flutter/ephemeral/FlutterInputs.xcfilelist,
+ );
+ inputPaths = (
+ Flutter/ephemeral/tripwire,
+ );
+ outputFileListPaths = (
+ Flutter/ephemeral/FlutterOutputs.xcfilelist,
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+ };
+ AD8696D5D39F3E86C935FCC3 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 33CC10E92044A3C60003C045 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+ targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 33CC10F52044A3C60003C045 /* Base */,
+ );
+ name = MainMenu.xib;
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000000..152410622c
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..21a3cc14c7
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift b/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000000..d53ef64377
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift
@@ -0,0 +1,9 @@
+import Cocoa
+import FlutterMacOS
+
+@NSApplicationMain
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..a2ec33f19f
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 0000000000..3c4935a7ca
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 0000000000..ed4cc16421
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 0000000000..483be61389
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 0000000000..bcbf36df2f
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 0000000000..9c0a652864
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 0000000000..e71a726136
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 0000000000..8a31fe2dd3
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Base.lproj/MainMenu.xib b/app_flowy/packages/flowy_editor/example/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000000..537341abf9
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig b/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 0000000000..b5323ca187
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = flowy_editor_example
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.plugin.flowyEditorExample
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2021 com.plugin. All rights reserved.
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig b/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 0000000000..36b0fd9464
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig b/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 0000000000..dff4f49561
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig b/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 0000000000..42bcbf4780
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements b/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 0000000000..dddb8a30c8
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.network.server
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist b/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist
new file mode 100644
index 0000000000..4789daa6a4
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ $(PRODUCT_COPYRIGHT)
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift b/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 0000000000..2722837ec9
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+ override func awakeFromNib() {
+ let flutterViewController = FlutterViewController.init()
+ let windowFrame = self.frame
+ self.contentViewController = flutterViewController
+ self.setFrame(windowFrame, display: true)
+
+ RegisterGeneratedPlugins(registry: flutterViewController)
+
+ super.awakeFromNib()
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements b/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements
new file mode 100644
index 0000000000..852fa1a472
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/pubspec.lock b/app_flowy/packages/flowy_editor/example/pubspec.lock
new file mode 100644
index 0000000000..e713df21de
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/pubspec.lock
@@ -0,0 +1,385 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ async:
+ dependency: transitive
+ description:
+ name: async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.6.1"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.15.0"
+ cupertino_icons:
+ dependency: "direct main"
+ description:
+ name: cupertino_icons
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.2"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ file:
+ dependency: "direct main"
+ description:
+ name: file
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.1.0"
+ flowy_editor:
+ dependency: "direct main"
+ description:
+ path: ".."
+ relative: true
+ source: path
+ version: "0.0.1"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_colorpicker:
+ dependency: transitive
+ description:
+ name: flutter_colorpicker
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.5.0"
+ flutter_keyboard_visibility:
+ dependency: transitive
+ description:
+ name: flutter_keyboard_visibility
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.0.1"
+ flutter_keyboard_visibility_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_keyboard_visibility_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ flutter_keyboard_visibility_web:
+ dependency: transitive
+ description:
+ name: flutter_keyboard_visibility_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ flutter_test:
+ dependency: "direct dev"
+ 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.3"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.10"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.0"
+ nested:
+ dependency: transitive
+ description:
+ name: nested
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.0"
+ path_provider:
+ dependency: "direct main"
+ description:
+ name: path_provider
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
+ path_provider_linux:
+ dependency: transitive
+ description:
+ name: path_provider_linux
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ path_provider_macos:
+ dependency: transitive
+ description:
+ name: path_provider_macos
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ path_provider_platform_interface:
+ dependency: transitive
+ description:
+ name: path_provider_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
+ path_provider_windows:
+ dependency: transitive
+ description:
+ name: path_provider_windows
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
+ pedantic:
+ dependency: transitive
+ description:
+ name: pedantic
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.11.1"
+ photo_view:
+ dependency: transitive
+ description:
+ path: "."
+ ref: "0.11.1-bugfix.1"
+ resolved-ref: "2b1915d8e798d887137397ec66511a14af30dadb"
+ url: "git@github.com:App-Flowy/photo_view.git"
+ source: git
+ version: "0.11.1-bugfix.1"
+ platform:
+ dependency: transitive
+ description:
+ name: platform
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.0"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ process:
+ dependency: transitive
+ description:
+ name: process
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.2.1"
+ provider:
+ dependency: "direct main"
+ description:
+ name: provider
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.0.0"
+ quiver:
+ dependency: transitive
+ description:
+ name: quiver
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ quiver_hashcode:
+ dependency: transitive
+ description:
+ name: quiver_hashcode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.1"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.10.0"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ string_validator:
+ dependency: transitive
+ description:
+ name: string_validator
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.0"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.0"
+ tuple:
+ dependency: transitive
+ description:
+ name: tuple
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.0"
+ url_launcher:
+ dependency: transitive
+ description:
+ name: url_launcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.0.3"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.5"
+ xdg_directories:
+ dependency: transitive
+ description:
+ name: xdg_directories
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.0"
+sdks:
+ dart: ">=2.12.0 <3.0.0"
+ flutter: ">=1.22.0"
diff --git a/app_flowy/packages/flowy_editor/example/pubspec.yaml b/app_flowy/packages/flowy_editor/example/pubspec.yaml
new file mode 100644
index 0000000000..9b82ac182d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/pubspec.yaml
@@ -0,0 +1,74 @@
+name: flowy_editor_example
+description: Demonstrates how to use the flowy_editor plugin.
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+environment:
+ sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+ file: ^6.1.0
+ path_provider: ^2.0.1
+ provider: ^5.0.0
+ flowy_editor:
+ # When depending on this package from a real application you should use:
+ # flowy_editor: ^x.y.z
+ # See https://dart.dev/tools/pub/dependencies#version-constraints
+ # The example app is bundled with the plugin so we use a path dependency on
+ # the parent directory to use the current plugin's version.
+ path: ../
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^1.0.2
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter.
+flutter:
+
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ assets:
+ - assets/
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware.
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/assets-and-images/#from-packages
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/app_flowy/packages/flowy_editor/example/web/favicon.png b/app_flowy/packages/flowy_editor/example/web/favicon.png
new file mode 100644
index 0000000000..8aaa46ac1a
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/web/favicon.png differ
diff --git a/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png b/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png
new file mode 100644
index 0000000000..b749bfef07
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png differ
diff --git a/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png b/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png
new file mode 100644
index 0000000000..88cfd48dff
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png differ
diff --git a/app_flowy/packages/flowy_editor/example/web/index.html b/app_flowy/packages/flowy_editor/example/web/index.html
new file mode 100644
index 0000000000..019f8278ba
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/web/index.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ flowy_editor_example
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/web/manifest.json b/app_flowy/packages/flowy_editor/example/web/manifest.json
new file mode 100644
index 0000000000..a4932b7c4d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/web/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "flowy_editor_example",
+ "short_name": "flowy_editor_example",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "Demonstrates how to use the flowy_editor 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"
+ }
+ ]
+}
diff --git a/app_flowy/packages/flowy_editor/example/windows/.gitignore b/app_flowy/packages/flowy_editor/example/windows/.gitignore
new file mode 100644
index 0000000000..d492d0d98c
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt b/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt
new file mode 100644
index 0000000000..0e3d031773
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt
@@ -0,0 +1,95 @@
+cmake_minimum_required(VERSION 3.15)
+project(flowy_editor_example LANGUAGES CXX)
+
+set(BINARY_NAME "flowy_editor_example")
+
+cmake_policy(SET CMP0063 NEW)
+
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Configure build options.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+ CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+ endif()
+endif()
+
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_17)
+ target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+ target_compile_options(${TARGET} PRIVATE /EHsc)
+ target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+ target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
+endfunction()
+
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+
+# Flutter library and tool build rules.
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build
+add_subdirectory("runner")
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ CONFIGURATIONS Profile;Release
+ COMPONENT Runtime)
diff --git a/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt b/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt
new file mode 100644
index 0000000000..b02c5485c9
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt
@@ -0,0 +1,103 @@
+cmake_minimum_required(VERSION 3.15)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "flutter_export.h"
+ "flutter_windows.h"
+ "flutter_messenger.h"
+ "flutter_plugin_registrar.h"
+ "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+ "core_implementations.cc"
+ "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+ "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+ "flutter_engine.cc"
+ "flutter_view_controller.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+ ${PHONY_OUTPUT}
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+ windows-x64 $
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc b/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000000..5de2cdb295
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+#include "generated_plugin_registrant.h"
+
+#include
+#include
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+ FlowyEditorPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FlowyEditorPlugin"));
+ UrlLauncherPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("UrlLauncherPlugin"));
+}
diff --git a/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h b/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000000..9846246b4d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,13 @@
+//
+// Generated file. Do not edit.
+//
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake b/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake
new file mode 100644
index 0000000000..133f87aeaa
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake
@@ -0,0 +1,17 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+ flowy_editor
+ url_launcher_windows
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt b/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt
new file mode 100644
index 0000000000..977e38b5d1
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt
@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.15)
+project(runner LANGUAGES CXX)
+
+add_executable(${BINARY_NAME} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "run_loop.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+apply_standard_settings(${BINARY_NAME})
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc b/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc
new file mode 100644
index 0000000000..f80ad64b0f
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#ifdef FLUTTER_BUILD_NUMBER
+#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
+#else
+#define VERSION_AS_NUMBER 1,0,0
+#endif
+
+#ifdef FLUTTER_BUILD_NAME
+#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "com.plugin" "\0"
+ VALUE "FileDescription", "Demonstrates how to use the flowy_editor plugin." "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "flowy_editor_example" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2021 com.plugin. All rights reserved." "\0"
+ VALUE "OriginalFilename", "flowy_editor_example.exe" "\0"
+ VALUE "ProductName", "flowy_editor_example" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp b/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp
new file mode 100644
index 0000000000..c422723045
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp
@@ -0,0 +1,64 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(RunLoop* run_loop,
+ const flutter::DartProject& project)
+ : run_loop_(run_loop), project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ run_loop_->RegisterFlutterInstance(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ run_loop_->UnregisterFlutterInstance(flutter_controller_->engine());
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opporutunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h b/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h
new file mode 100644
index 0000000000..b663ddd501
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h
@@ -0,0 +1,39 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "run_loop.h"
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow driven by the |run_loop|, hosting a
+ // Flutter view running |project|.
+ explicit FlutterWindow(RunLoop* run_loop,
+ const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The run loop driving events for this window.
+ RunLoop* run_loop_;
+
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp b/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp
new file mode 100644
index 0000000000..a5de5cb6ef
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp
@@ -0,0 +1,42 @@
+#include
+#include
+#include
+
+#include "flutter_window.h"
+#include "run_loop.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ RunLoop run_loop;
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(&run_loop, project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.CreateAndShow(L"flowy_editor_example", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ run_loop.Run();
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/resource.h b/app_flowy/packages/flowy_editor/example/windows/runner/resource.h
new file mode 100644
index 0000000000..66a65d1e4a
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/resources/app_icon.ico b/app_flowy/packages/flowy_editor/example/windows/runner/resources/app_icon.ico
new file mode 100644
index 0000000000..c04e20caf6
Binary files /dev/null and b/app_flowy/packages/flowy_editor/example/windows/runner/resources/app_icon.ico differ
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/run_loop.cpp b/app_flowy/packages/flowy_editor/example/windows/runner/run_loop.cpp
new file mode 100644
index 0000000000..2d6636ab6b
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/run_loop.cpp
@@ -0,0 +1,66 @@
+#include "run_loop.h"
+
+#include
+
+#include
+
+RunLoop::RunLoop() {}
+
+RunLoop::~RunLoop() {}
+
+void RunLoop::Run() {
+ bool keep_running = true;
+ TimePoint next_flutter_event_time = TimePoint::clock::now();
+ while (keep_running) {
+ std::chrono::nanoseconds wait_duration =
+ std::max(std::chrono::nanoseconds(0),
+ next_flutter_event_time - TimePoint::clock::now());
+ ::MsgWaitForMultipleObjects(
+ 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000),
+ QS_ALLINPUT);
+ bool processed_events = false;
+ MSG message;
+ // All pending Windows messages must be processed; MsgWaitForMultipleObjects
+ // won't return again for items left in the queue after PeekMessage.
+ while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) {
+ processed_events = true;
+ if (message.message == WM_QUIT) {
+ keep_running = false;
+ break;
+ }
+ ::TranslateMessage(&message);
+ ::DispatchMessage(&message);
+ // Allow Flutter to process messages each time a Windows message is
+ // processed, to prevent starvation.
+ next_flutter_event_time =
+ std::min(next_flutter_event_time, ProcessFlutterMessages());
+ }
+ // If the PeekMessage loop didn't run, process Flutter messages.
+ if (!processed_events) {
+ next_flutter_event_time =
+ std::min(next_flutter_event_time, ProcessFlutterMessages());
+ }
+ }
+}
+
+void RunLoop::RegisterFlutterInstance(
+ flutter::FlutterEngine* flutter_instance) {
+ flutter_instances_.insert(flutter_instance);
+}
+
+void RunLoop::UnregisterFlutterInstance(
+ flutter::FlutterEngine* flutter_instance) {
+ flutter_instances_.erase(flutter_instance);
+}
+
+RunLoop::TimePoint RunLoop::ProcessFlutterMessages() {
+ TimePoint next_event_time = TimePoint::max();
+ for (auto instance : flutter_instances_) {
+ std::chrono::nanoseconds wait_duration = instance->ProcessMessages();
+ if (wait_duration != std::chrono::nanoseconds::max()) {
+ next_event_time =
+ std::min(next_event_time, TimePoint::clock::now() + wait_duration);
+ }
+ }
+ return next_event_time;
+}
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/run_loop.h b/app_flowy/packages/flowy_editor/example/windows/runner/run_loop.h
new file mode 100644
index 0000000000..000d362463
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/run_loop.h
@@ -0,0 +1,40 @@
+#ifndef RUNNER_RUN_LOOP_H_
+#define RUNNER_RUN_LOOP_H_
+
+#include
+
+#include
+#include
+
+// A runloop that will service events for Flutter instances as well
+// as native messages.
+class RunLoop {
+ public:
+ RunLoop();
+ ~RunLoop();
+
+ // Prevent copying
+ RunLoop(RunLoop const&) = delete;
+ RunLoop& operator=(RunLoop const&) = delete;
+
+ // Runs the run loop until the application quits.
+ void Run();
+
+ // Registers the given Flutter instance for event servicing.
+ void RegisterFlutterInstance(
+ flutter::FlutterEngine* flutter_instance);
+
+ // Unregisters the given Flutter instance from event servicing.
+ void UnregisterFlutterInstance(
+ flutter::FlutterEngine* flutter_instance);
+
+ private:
+ using TimePoint = std::chrono::steady_clock::time_point;
+
+ // Processes all currently pending messages for registered Flutter instances.
+ TimePoint ProcessFlutterMessages();
+
+ std::set flutter_instances_;
+};
+
+#endif // RUNNER_RUN_LOOP_H_
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest b/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest
new file mode 100644
index 0000000000..c977c4a425
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest
@@ -0,0 +1,20 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp b/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp
new file mode 100644
index 0000000000..d19bdbbcc3
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp
@@ -0,0 +1,64 @@
+#include "utils.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector();
+ }
+
+ std::vector command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr);
+ if (target_length == 0) {
+ return std::string();
+ }
+ std::string utf8_string;
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, utf8_string.data(),
+ target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/utils.h b/app_flowy/packages/flowy_editor/example/windows/runner/utils.h
new file mode 100644
index 0000000000..3879d54755
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include
+#include
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector,
+// encoded in UTF-8. Returns an empty std::vector on failure.
+std::vector GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp b/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp
new file mode 100644
index 0000000000..c10f08dc7d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp
@@ -0,0 +1,245 @@
+#include "win32_window.h"
+
+#include
+
+#include "resource.h"
+
+namespace {
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ FreeLibrary(user32_module);
+ }
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::CreateAndShow(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast(origin.x),
+ static_cast(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ return OnCreate();
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast(window_struct->lpCreateParams));
+
+ auto that = static_cast(window_struct->lpCreateParams);
+ EnableFullDpiSupportIfAvailable(window);
+ that->window_handle_ = window;
+ } else if (Win32Window* that = GetThisFromHandle(window)) {
+ return that->MessageHandler(window, message, wparam, lparam);
+ }
+
+ return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY:
+ window_handle_ = nullptr;
+ Destroy();
+ if (quit_on_close_) {
+ PostQuitMessage(0);
+ }
+ return 0;
+
+ case WM_DPICHANGED: {
+ auto newRectSize = reinterpret_cast(lparam);
+ LONG newWidth = newRectSize->right - newRectSize->left;
+ LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+ SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+ newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+ case WM_SIZE: {
+ RECT rect = GetClientArea();
+ if (child_content_ != nullptr) {
+ // Size and position the child window.
+ MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return 0;
+ }
+
+ case WM_ACTIVATE:
+ if (child_content_ != nullptr) {
+ SetFocus(child_content_);
+ }
+ return 0;
+ }
+
+ return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+ OnDestroy();
+
+ if (window_handle_) {
+ DestroyWindow(window_handle_);
+ window_handle_ = nullptr;
+ }
+ if (g_active_window_count == 0) {
+ WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+ }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+ return reinterpret_cast(
+ GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+ child_content_ = content;
+ SetParent(content, window_handle_);
+ RECT frame = GetClientArea();
+
+ MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+ frame.bottom - frame.top, true);
+
+ SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+ RECT frame;
+ GetClientRect(window_handle_, &frame);
+ return frame;
+}
+
+HWND Win32Window::GetHandle() {
+ return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+ quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+ // No-op; provided for subclasses.
+ return true;
+}
+
+void Win32Window::OnDestroy() {
+ // No-op; provided for subclasses.
+}
diff --git a/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h b/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h
new file mode 100644
index 0000000000..17ba431125
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h
@@ -0,0 +1,98 @@
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include
+
+#include
+#include
+#include
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+ struct Point {
+ unsigned int x;
+ unsigned int y;
+ Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+ };
+
+ struct Size {
+ unsigned int width;
+ unsigned int height;
+ Size(unsigned int width, unsigned int height)
+ : width(width), height(height) {}
+ };
+
+ Win32Window();
+ virtual ~Win32Window();
+
+ // Creates and shows a win32 window with |title| and position and size using
+ // |origin| and |size|. New windows are created on the default monitor. Window
+ // sizes are specified to the OS in physical pixels, hence to ensure a
+ // consistent size to will treat the width height passed in to this function
+ // as logical pixels and scale to appropriate for the default monitor. Returns
+ // true if the window was created successfully.
+ bool CreateAndShow(const std::wstring& title,
+ const Point& origin,
+ const Size& size);
+
+ // Release OS resources associated with window.
+ void Destroy();
+
+ // Inserts |content| into the window tree.
+ void SetChildContent(HWND content);
+
+ // Returns the backing Window handle to enable clients to set icon and other
+ // window properties. Returns nullptr if the window has been destroyed.
+ HWND GetHandle();
+
+ // If true, closing this window will quit the application.
+ void SetQuitOnClose(bool quit_on_close);
+
+ // Return a RECT representing the bounds of the current client area.
+ RECT GetClientArea();
+
+ protected:
+ // Processes and route salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT MessageHandler(HWND window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Called when CreateAndShow is called, allowing subclass window-related
+ // setup. Subclasses should return false if setup fails.
+ virtual bool OnCreate();
+
+ // Called when Destroy is called.
+ virtual void OnDestroy();
+
+ private:
+ friend class WindowClassRegistrar;
+
+ // OS callback called by message pump. Handles the WM_NCCREATE message which
+ // is passed when the non-client area is being created and enables automatic
+ // non-client DPI scaling so that the non-client area automatically
+ // responsponds to changes in DPI. All other messages are handled by
+ // MessageHandler.
+ static LRESULT CALLBACK WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Retrieves a class instance pointer for |window|
+ static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+ bool quit_on_close_ = false;
+
+ // window handle for top level window.
+ HWND window_handle_ = nullptr;
+
+ // window handle for hosted content.
+ HWND child_content_ = nullptr;
+};
+
+#endif // RUNNER_WIN32_WINDOW_H_
diff --git a/app_flowy/packages/flowy_editor/ios/.gitignore b/app_flowy/packages/flowy_editor/ios/.gitignore
new file mode 100644
index 0000000000..aa479fd3ce
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/ios/.gitignore
@@ -0,0 +1,37 @@
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+.DS_Store
+*.swp
+profile
+
+DerivedData/
+build/
+GeneratedPluginRegistrant.h
+GeneratedPluginRegistrant.m
+
+.generated/
+
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+
+xcuserdata
+
+*.moved-aside
+
+*.pyc
+*sync/
+Icon?
+.tags*
+
+/Flutter/Generated.xcconfig
+/Flutter/flutter_export_environment.sh
\ No newline at end of file
diff --git a/app_flowy/packages/flowy_editor/ios/Assets/.gitkeep b/app_flowy/packages/flowy_editor/ios/Assets/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app_flowy/packages/flowy_editor/ios/Classes/FlowyEditorPlugin.h b/app_flowy/packages/flowy_editor/ios/Classes/FlowyEditorPlugin.h
new file mode 100644
index 0000000000..507d758742
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/ios/Classes/FlowyEditorPlugin.h
@@ -0,0 +1,4 @@
+#import
+
+@interface FlowyEditorPlugin : NSObject
+@end
diff --git a/app_flowy/packages/flowy_editor/ios/Classes/FlowyEditorPlugin.m b/app_flowy/packages/flowy_editor/ios/Classes/FlowyEditorPlugin.m
new file mode 100644
index 0000000000..0b1c881d06
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/ios/Classes/FlowyEditorPlugin.m
@@ -0,0 +1,15 @@
+#import "FlowyEditorPlugin.h"
+#if __has_include()
+#import
+#else
+// Support project import fallback if the generated compatibility header
+// is not copied when this plugin is created as a library.
+// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
+#import "flowy_editor-Swift.h"
+#endif
+
+@implementation FlowyEditorPlugin
++ (void)registerWithRegistrar:(NSObject*)registrar {
+ [SwiftFlowyEditorPlugin registerWithRegistrar:registrar];
+}
+@end
diff --git a/app_flowy/packages/flowy_editor/ios/Classes/SwiftFlowyEditorPlugin.swift b/app_flowy/packages/flowy_editor/ios/Classes/SwiftFlowyEditorPlugin.swift
new file mode 100644
index 0000000000..f72f23f7fc
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/ios/Classes/SwiftFlowyEditorPlugin.swift
@@ -0,0 +1,14 @@
+import Flutter
+import UIKit
+
+public class SwiftFlowyEditorPlugin: NSObject, FlutterPlugin {
+ public static func register(with registrar: FlutterPluginRegistrar) {
+ let channel = FlutterMethodChannel(name: "flowy_editor", binaryMessenger: registrar.messenger())
+ let instance = SwiftFlowyEditorPlugin()
+ registrar.addMethodCallDelegate(instance, channel: channel)
+ }
+
+ public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ result("iOS " + UIDevice.current.systemVersion)
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/ios/flowy_editor.podspec b/app_flowy/packages/flowy_editor/ios/flowy_editor.podspec
new file mode 100644
index 0000000000..9dd96c52ff
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/ios/flowy_editor.podspec
@@ -0,0 +1,23 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint flowy_editor.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+ s.name = 'flowy_editor'
+ s.version = '0.0.1'
+ s.summary = 'A new flutter plugin project.'
+ s.description = <<-DESC
+A new flutter plugin project.
+ DESC
+ s.homepage = 'http://example.com'
+ s.license = { :file => '../LICENSE' }
+ s.author = { 'Your Company' => 'email@example.com' }
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*'
+ s.dependency 'Flutter'
+ s.platform = :ios, '8.0'
+
+ # Flutter.framework does not contain a i386 slice.
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
+ s.swift_version = '5.0'
+end
diff --git a/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/app_flowy/packages/flowy_editor/lib/flowy_editor.dart
new file mode 100644
index 0000000000..559d85181a
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/flowy_editor.dart
@@ -0,0 +1,9 @@
+library flowy_editor;
+
+export 'src/widget/editor.dart';
+export 'src/widget/builder.dart';
+export 'src/widget/toolbar.dart';
+export 'src/widget/flowy_toolbar.dart';
+export 'src/service/style.dart';
+export 'src/service/controller.dart';
+export 'src/model/document/document.dart';
diff --git a/app_flowy/packages/flowy_editor/lib/flowy_editor_web.dart b/app_flowy/packages/flowy_editor/lib/flowy_editor_web.dart
new file mode 100644
index 0000000000..6d854f4da5
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/flowy_editor_web.dart
@@ -0,0 +1,44 @@
+import 'dart:async';
+// In order to *not* need this ignore, consider extracting the "web" version
+// of your plugin as a separate package, instead of inlining it in the same
+// package as the core of your plugin.
+// ignore: avoid_web_libraries_in_flutter
+import 'dart:html' as html show window;
+
+import 'package:flutter/services.dart';
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+
+/// A web implementation of the FlowyEditor plugin.
+class FlowyEditorWeb {
+ static void registerWith(Registrar registrar) {
+ final channel = MethodChannel(
+ 'flowy_editor',
+ const StandardMethodCodec(),
+ registrar,
+ );
+
+ final pluginInstance = FlowyEditorWeb();
+ channel.setMethodCallHandler(pluginInstance.handleMethodCall);
+ }
+
+ /// Handles method calls over the MethodChannel of this plugin.
+ /// Note: Check the "federated" architecture for a new way of doing this:
+ /// https://flutter.dev/go/federated-plugins
+ Future handleMethodCall(MethodCall call) async {
+ switch (call.method) {
+ case 'getPlatformVersion':
+ return getPlatformVersion();
+ default:
+ throw PlatformException(
+ code: 'Unimplemented',
+ details: 'flowy_editor for web doesn\'t implement \'${call.method}\'',
+ );
+ }
+ }
+
+ /// Returns a [String] containing the version of the platform.
+ Future getPlatformVersion() {
+ final version = html.window.navigator.userAgent;
+ return Future.value(version);
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/attribute.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/attribute.dart
new file mode 100644
index 0000000000..92874a2b85
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/attribute.dart
@@ -0,0 +1,296 @@
+import 'package:quiver/core.dart';
+
+enum AttributeScope {
+ INLINE, // refer to https://quilljs.com/docs/formats/#inline
+ BLOCK, // refer to https://quilljs.com/docs/formats/#block
+ EMBEDS, // refer to https://quilljs.com/docs/formats/#embeds
+ IGNORE, // attributes that can be ignored
+}
+
+class Attribute {
+ Attribute(this.key, this.scope, this.value);
+
+ final String key;
+ final AttributeScope scope;
+ final T value;
+
+ static final Map _registry = {
+ Attribute.bold.key: Attribute.bold,
+ Attribute.italic.key: Attribute.italic,
+ Attribute.underline.key: Attribute.underline,
+ Attribute.strikeThrough.key: Attribute.strikeThrough,
+ Attribute.font.key: Attribute.font,
+ Attribute.size.key: Attribute.size,
+ Attribute.link.key: Attribute.link,
+ Attribute.color.key: Attribute.color,
+ Attribute.background.key: Attribute.background,
+ Attribute.placeholder.key: Attribute.placeholder,
+ Attribute.header.key: Attribute.header,
+ Attribute.indent.key: Attribute.indent,
+ Attribute.align.key: Attribute.align,
+ Attribute.list.key: Attribute.list,
+ Attribute.codeBlock.key: Attribute.codeBlock,
+ Attribute.quoteBlock.key: Attribute.quoteBlock,
+ Attribute.width.key: Attribute.width,
+ Attribute.height.key: Attribute.height,
+ Attribute.style.key: Attribute.style,
+ Attribute.token.key: Attribute.token,
+ };
+
+ // Attribute Properties
+
+ static final BoldAttribute bold = BoldAttribute();
+
+ static final ItalicAttribute italic = ItalicAttribute();
+
+ static final UnderlineAttribute underline = UnderlineAttribute();
+
+ static final StrikeThroughAttribute strikeThrough = StrikeThroughAttribute();
+
+ static final FontAttribute font = FontAttribute(null);
+
+ static final SizeAttribute size = SizeAttribute(null);
+
+ static final LinkAttribute link = LinkAttribute(null);
+
+ static final ColorAttribute color = ColorAttribute(null);
+
+ static final BackgroundAttribute background = BackgroundAttribute(null);
+
+ static final PlaceholderAttribute placeholder = PlaceholderAttribute();
+
+ static final HeaderAttribute header = HeaderAttribute();
+
+ static final IndentAttribute indent = IndentAttribute();
+
+ static final AlignAttribute align = AlignAttribute(null);
+
+ static final ListAttribute list = ListAttribute(null);
+
+ static final CodeBlockAttribute codeBlock = CodeBlockAttribute();
+
+ static final QuoteBlockAttribute quoteBlock = QuoteBlockAttribute();
+
+ static final WidthAttribute width = WidthAttribute(null);
+
+ static final HeightAttribute height = HeightAttribute(null);
+
+ static final StyleAttribute style = StyleAttribute(null);
+
+ static final TokenAttribute token = TokenAttribute('');
+
+ static Attribute get h1 => HeaderAttribute(level: 1);
+
+ static Attribute get h2 => HeaderAttribute(level: 2);
+
+ static Attribute get h3 => HeaderAttribute(level: 3);
+
+ static Attribute get h4 => HeaderAttribute(level: 4);
+
+ static Attribute get h5 => HeaderAttribute(level: 5);
+
+ static Attribute get h6 => HeaderAttribute(level: 6);
+
+ static Attribute get leftAlignment => AlignAttribute('left');
+
+ static Attribute get centerAlignment => AlignAttribute('center');
+
+ static Attribute get rightAlignment => AlignAttribute('right');
+
+ static Attribute get justifyAlignment => AlignAttribute('justify');
+
+ static Attribute get bullet => ListAttribute('bullet');
+
+ static Attribute get ordered => ListAttribute('ordered');
+
+ static Attribute get checked => ListAttribute('checked');
+
+ static Attribute get unchecked => ListAttribute('unchecked');
+
+ static Attribute get indentL1 => IndentAttribute(level: 1);
+
+ static Attribute get indentL2 => IndentAttribute(level: 2);
+
+ static Attribute get indentL3 => IndentAttribute(level: 3);
+
+ static Attribute get indentL4 => IndentAttribute(level: 4);
+
+ static Attribute get indentL5 => IndentAttribute(level: 5);
+
+ static Attribute get indentL6 => IndentAttribute(level: 6);
+
+ static Attribute getIndentLevel(int? level) {
+ switch (level) {
+ case 1:
+ return indentL1;
+ case 2:
+ return indentL2;
+ case 3:
+ return indentL3;
+ case 4:
+ return indentL4;
+ case 5:
+ return indentL5;
+ default:
+ return indentL6;
+ }
+ }
+
+ // Keys Container
+ static final Set inlineKeys = {
+ Attribute.bold.key,
+ Attribute.italic.key,
+ Attribute.underline.key,
+ Attribute.strikeThrough.key,
+ Attribute.link.key,
+ Attribute.color.key,
+ Attribute.background.key,
+ Attribute.placeholder.key,
+ };
+
+ static final Set blockKeys = {
+ Attribute.header.key,
+ Attribute.indent.key,
+ Attribute.align.key,
+ Attribute.list.key,
+ Attribute.codeBlock.key,
+ Attribute.quoteBlock.key,
+ };
+
+ static final Set blockKeysExceptHeader = blockKeys
+ ..remove(Attribute.header.key);
+
+ // Utils
+
+ bool get isInline => AttributeScope.INLINE == scope;
+
+ bool get isIgnored => AttributeScope.IGNORE == scope;
+
+ bool get isBlockExceptHeader => blockKeysExceptHeader.contains(key);
+
+ Map toJson() => {key: value};
+
+ static Attribute fromKeyValue(String key, dynamic value) {
+ if (!_registry.containsKey(key)) {
+ throw ArgumentError.value(key, 'key "$key" not found.');
+ }
+ final origin = _registry[key]!;
+ final attribute = clone(origin, value);
+ return attribute;
+ }
+
+ static Attribute clone(Attribute origin, dynamic value) {
+ return Attribute(origin.key, origin.scope, value);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+ if (other is! Attribute) return false;
+ final typedOther = other;
+ return key == typedOther.key &&
+ scope == typedOther.scope &&
+ value == typedOther.value;
+ }
+
+ @override
+ int get hashCode => hash3(key, scope, value);
+
+ @override
+ String toString() {
+ return 'Attribute{key: $key, scope: $scope, value: $value}';
+ }
+}
+
+/* -------------------------------------------------------------------------- */
+/* Attributes Impl */
+/* -------------------------------------------------------------------------- */
+
+/* --------------------------------- INLINE --------------------------------- */
+
+class BoldAttribute extends Attribute {
+ BoldAttribute() : super('bold', AttributeScope.INLINE, true);
+}
+
+class ItalicAttribute extends Attribute {
+ ItalicAttribute() : super('italic', AttributeScope.INLINE, true);
+}
+
+class UnderlineAttribute extends Attribute {
+ UnderlineAttribute() : super('underline', AttributeScope.INLINE, true);
+}
+
+class StrikeThroughAttribute extends Attribute {
+ StrikeThroughAttribute()
+ : super('strikethrough', AttributeScope.INLINE, true);
+}
+
+class FontAttribute extends Attribute {
+ FontAttribute(String? value) : super('font', AttributeScope.INLINE, value);
+}
+
+class SizeAttribute extends Attribute {
+ SizeAttribute(String? value) : super('size', AttributeScope.INLINE, value);
+}
+
+class LinkAttribute extends Attribute {
+ LinkAttribute(String? value) : super('link', AttributeScope.INLINE, value);
+}
+
+class ColorAttribute extends Attribute {
+ ColorAttribute(String? value) : super('color', AttributeScope.INLINE, value);
+}
+
+class BackgroundAttribute extends Attribute {
+ BackgroundAttribute(String? value)
+ : super('background', AttributeScope.INLINE, value);
+}
+
+class PlaceholderAttribute extends Attribute {
+ PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true);
+}
+
+/* ---------------------------------- BLOCK --------------------------------- */
+
+class HeaderAttribute extends Attribute {
+ HeaderAttribute({int? level}) : super('header', AttributeScope.BLOCK, level);
+}
+
+class IndentAttribute extends Attribute {
+ IndentAttribute({int? level}) : super('indent', AttributeScope.BLOCK, level);
+}
+
+class AlignAttribute extends Attribute {
+ AlignAttribute(String? value) : super('align', AttributeScope.BLOCK, value);
+}
+
+class ListAttribute extends Attribute {
+ ListAttribute(String? value) : super('list', AttributeScope.BLOCK, value);
+}
+
+class CodeBlockAttribute extends Attribute {
+ CodeBlockAttribute() : super('code_block', AttributeScope.BLOCK, true);
+}
+
+class QuoteBlockAttribute extends Attribute {
+ QuoteBlockAttribute() : super('quote_block', AttributeScope.BLOCK, true);
+}
+
+/* --------------------------------- IGNORE --------------------------------- */
+
+class WidthAttribute extends Attribute {
+ WidthAttribute(String? value) : super('width', AttributeScope.IGNORE, value);
+}
+
+class HeightAttribute extends Attribute {
+ HeightAttribute(String? value)
+ : super('height', AttributeScope.IGNORE, value);
+}
+
+class StyleAttribute extends Attribute {
+ StyleAttribute(String? value) : super('style', AttributeScope.IGNORE, value);
+}
+
+class TokenAttribute extends Attribute {
+ TokenAttribute(String? value) : super('token', AttributeScope.IGNORE, value);
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/document.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/document.dart
new file mode 100644
index 0000000000..274f3959d4
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/document.dart
@@ -0,0 +1,277 @@
+import 'dart:async';
+
+import 'package:tuple/tuple.dart';
+
+import '../quill_delta.dart';
+import '../heuristic/rule.dart';
+import '../document/style.dart';
+import 'history.dart';
+import 'attribute.dart';
+import 'node/block.dart';
+import 'node/container.dart';
+import 'node/embed.dart';
+import 'node/line.dart';
+import 'node/node.dart';
+
+/// The rich text document
+class Document {
+ Document() : _delta = Delta()..insert('\n') {
+ _loadDocument(_delta);
+ }
+
+ Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
+ _loadDocument(_delta);
+ }
+
+ Document.fromDelta(Delta delta) : _delta = delta {
+ _loadDocument(_delta);
+ }
+
+ /// The root node of the document tree
+ final Root _root = Root();
+
+ Root get root => _root;
+
+ int get length => _root.length;
+
+ Delta _delta;
+
+ Delta toDelta() => Delta.from(_delta);
+
+ final Rules _rules = Rules.getInstance();
+
+ final StreamController> _observer =
+ StreamController.broadcast();
+
+ final History _history = History();
+
+ Stream> get changes => _observer.stream;
+
+ bool get hasUndo => _history.hasUndo;
+
+ bool get hasRedo => _history.hasRedo;
+
+ Delta insert(int index, Object? data, {int replaceLength = 0}) {
+ assert(index >= 0);
+ assert(data is String || data is Embeddable);
+ if (data is Embeddable) {
+ data = data.toJson();
+ } else if ((data as String).isEmpty) {
+ return Delta();
+ }
+
+ final delta = _rules.apply(
+ RuleType.INSERT,
+ this,
+ index,
+ data: data,
+ length: replaceLength,
+ );
+ compose(delta, ChangeSource.LOCAL);
+ return delta;
+ }
+
+ Delta delete(int index, int length) {
+ assert(index >= 0 && length > 0);
+ final delta = _rules.apply(RuleType.DELETE, this, index, length: length);
+ if (delta.isNotEmpty) {
+ compose(delta, ChangeSource.LOCAL);
+ }
+ return delta;
+ }
+
+ Delta replace(int index, int length, Object? data) {
+ assert(index >= 0);
+ assert(data is String || data is Embeddable);
+
+ final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true;
+ assert(dataIsNotEmpty || length > 0);
+
+ var delta = Delta();
+
+ // We have to insert before applying delete rules
+ // Otherwise delete would be operating on stale document snapshot.
+ if (dataIsNotEmpty) {
+ delta = insert(index, data, replaceLength: length);
+ }
+
+ if (length > 0) {
+ final deleteDelta = delete(index, length);
+ delta = delta.compose(deleteDelta);
+ }
+
+ return delta;
+ }
+
+ Delta format(int index, int length, Attribute? attribute) {
+ assert(index >= 0 && length >= 0 && attribute != null);
+
+ var delta = Delta();
+
+ final formatDelta = _rules.apply(
+ RuleType.FORMAT,
+ this,
+ index,
+ length: length,
+ attribute: attribute,
+ );
+ if (formatDelta.isNotEmpty) {
+ compose(formatDelta, ChangeSource.LOCAL);
+ delta = delta.compose(formatDelta);
+ }
+
+ return delta;
+ }
+
+ Style collectStyle(int index, int length) {
+ final res = queryChild(index);
+ return (res.node as Line).collectStyle(res.offset, length);
+ }
+
+ ChildQuery queryChild(int offset) {
+ final res = _root.queryChild(offset, true);
+ if (res.node is Line) {
+ return res;
+ }
+ final block = res.node as Block;
+ return block.queryChild(res.offset, true);
+ }
+
+ Tuple2 undo() => _history.undo(this);
+
+ Tuple2 redo() => _history.redo(this);
+
+ void compose(Delta delta, ChangeSource changeSource) {
+ assert(!_observer.isClosed);
+ delta.trim();
+ assert(delta.isNotEmpty);
+
+ var offset = 0;
+ delta = _transform(delta);
+ final originDelta = toDelta();
+ for (final op in delta.toList()) {
+ final style =
+ op.attributes != null ? Style.fromJson(op.attributes) : null;
+
+ if (op.isInsert) {
+ _root.insert(offset, _normalize(op.data), style);
+ } else if (op.isDelete) {
+ _root.delete(offset, op.length);
+ } else if (op.attributes != null) {
+ _root.retain(offset, op.length, style);
+ }
+
+ if (!op.isDelete) {
+ offset += op.length!;
+ }
+ }
+
+ try {
+ _delta = _delta.compose(delta);
+ } catch (e) {
+ throw '_delta compose failed';
+ }
+
+ if (_delta != _root.toDelta()) {
+ throw 'Compose failed';
+ }
+ final change = Tuple3(originDelta, delta, changeSource);
+ _observer.add(change);
+ _history.handleDocChange(change);
+ }
+
+ static Delta _transform(Delta delta) {
+ final res = Delta();
+ final ops = delta.toList();
+ for (var i = 0; i < ops.length; i++) {
+ final op = ops[i];
+ res.push(op);
+ _handleImageInsert(i, ops, op, res);
+ }
+ return res;
+ }
+
+ static void _handleImageInsert(
+ int i, List ops, Operation op, Delta res) {
+ final nextOpIsImage =
+ i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String;
+ if (nextOpIsImage && !(op.data as String).endsWith('\n')) {
+ res.push(Operation.insert('\n'));
+ }
+ // Currently embed is equivalent to image and hence `is! String`
+ final opInsertImage = op.isInsert && op.data is! String;
+ final nextOpIsLineBreak = i + 1 < ops.length &&
+ ops[i + 1].isInsert &&
+ ops[i + 1].data is String &&
+ (ops[i + 1].data as String).startsWith('\n');
+ if (opInsertImage && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) {
+ // automatically append '\n' for image
+ res.push(Operation.insert('\n'));
+ }
+ }
+
+ Object _normalize(Object? data) {
+ if (data is String) {
+ return data;
+ }
+
+ if (data is Embeddable) {
+ return data;
+ }
+ return Embeddable.fromJson(data as Map);
+ }
+
+ void close() {
+ _observer.close();
+ _history.clear();
+ }
+
+ void _loadDocument(Delta delta) {
+ assert((delta.last.data as String).endsWith('\n'),
+ 'Delta must ends with a line break.');
+ var offset = 0;
+ for (final op in delta.toList()) {
+ if (!op.isInsert) {
+ throw ArgumentError.value(delta,
+ 'Document Delta can only contain insert operations but ${op.key} found.');
+ }
+ final style =
+ op.attributes != null ? Style.fromJson(op.attributes) : null;
+ final data = _normalize(op.data);
+ _root.insert(offset, data, style);
+ offset += op.length!;
+ }
+ final node = _root.last;
+ if (node is Line &&
+ node.parent is! Block &&
+ node.style.isEmpty &&
+ _root.childCount > 1) {
+ _root.remove(node);
+ }
+ }
+
+ bool isEmpty() {
+ if (root.children.length != 1) {
+ return false;
+ }
+
+ final node = root.children.first;
+ if (!node.isLast) {
+ return false;
+ }
+
+ final delta = node.toDelta();
+ return delta.length == 1 &&
+ delta.first.data == '\n' &&
+ delta.first.key == Operation.insertKey;
+ }
+
+ String toPlainText() {
+ return root.children.map((child) => child.toPlainText()).join();
+ }
+}
+
+enum ChangeSource {
+ LOCAL,
+ REMOTE,
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/history.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/history.dart
new file mode 100644
index 0000000000..22b287f970
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/history.dart
@@ -0,0 +1,134 @@
+import 'package:tuple/tuple.dart';
+
+import '../quill_delta.dart';
+import 'document.dart';
+
+class History {
+ History({
+ this.ignoreChange = false,
+ this.minRecordThreshold = 400,
+ this.capacity = 100,
+ this.userOnly = false,
+ this.lastRecorded = 0,
+ });
+
+ final HistoryStack stack = HistoryStack.empty();
+
+ bool get hasUndo => stack.undo.isNotEmpty;
+
+ bool get hasRedo => stack.redo.isNotEmpty;
+
+ /// used for disable redo or undo function
+ bool ignoreChange;
+
+ int lastRecorded;
+
+ /// Collaborative editing's conditions should be true
+ final bool userOnly;
+
+ /// max operation count for undo
+ final int capacity;
+
+ /// record delay
+ final int minRecordThreshold;
+
+ void handleDocChange(Tuple3 change) {
+ if (ignoreChange) return;
+ if (!userOnly || change.item3 == ChangeSource.LOCAL) {
+ record(change.item2, change.item1);
+ } else {
+ transform(change.item2);
+ }
+ }
+
+ void clear() {
+ stack.clear();
+ }
+
+ void record(Delta change, Delta before) {
+ if (change.isEmpty) return;
+ stack.redo.clear();
+ var undoDelta = change.invert(before);
+ final timestamp = DateTime.now().millisecondsSinceEpoch;
+
+ if (timestamp - lastRecorded < minRecordThreshold &&
+ stack.undo.isNotEmpty) {
+ final lastDelta = stack.undo.removeLast();
+ undoDelta = undoDelta.compose(lastDelta);
+ } else {
+ lastRecorded = timestamp;
+ }
+
+ if (undoDelta.isEmpty) return;
+ stack.undo.add(undoDelta);
+
+ if (stack.undo.length > capacity) {
+ stack.undo.removeAt(0);
+ }
+ }
+
+ void transform(Delta delta) {
+ transformStack(stack.undo, delta);
+ transformStack(stack.redo, delta);
+ }
+
+ void transformStack(List stack, Delta delta) {
+ for (var i = stack.length - 1; i >= 0; i -= 1) {
+ final oldDelta = stack[i];
+ stack[i] = delta.transform(oldDelta, true);
+ delta = oldDelta.transform(delta, false);
+ if (stack[i].length == 0) {
+ stack.removeAt(i);
+ }
+ }
+ }
+
+ Tuple2 undo(Document doc) {
+ return _change(doc, stack.undo, stack.redo);
+ }
+
+ Tuple2 redo(Document doc) {
+ return _change(doc, stack.redo, stack.undo);
+ }
+
+ Tuple2 _change(Document doc, List source, List target) {
+ if (source.isEmpty) {
+ return const Tuple2(false, 0);
+ }
+
+ final delta = source.removeLast();
+ // look for insert or delete
+ int? len = 0;
+ final ops = delta.toList();
+ for (var i = 0; i < ops.length; i++) {
+ if (ops[i].key == Operation.insertKey) {
+ len = ops[i].length;
+ } else if (ops[i].key == Operation.deleteKey) {
+ len = ops[i].length! * -1;
+ }
+ }
+ final base = Delta.from(doc.toDelta());
+ final inverseDelta = delta.invert(base);
+ target.add(inverseDelta);
+ lastRecorded = 0;
+ ignoreChange = true;
+ doc.compose(delta, ChangeSource.LOCAL);
+ ignoreChange = false;
+ return Tuple2(true, len);
+ }
+}
+
+class HistoryStack {
+ HistoryStack.empty()
+ : undo = [],
+ redo = [];
+
+ final List undo;
+
+ final List redo;
+
+ void clear() {
+ undo.clear();
+ redo.clear();
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/node/block.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/node/block.dart
new file mode 100644
index 0000000000..d2624a18ff
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/node/block.dart
@@ -0,0 +1,72 @@
+import '../../quill_delta.dart';
+import 'container.dart';
+import 'line.dart';
+import 'node.dart';
+
+/// Represents a group of adjacent [Line]s with the same block style.
+///
+/// Block elements are:
+/// - Quoteblock
+/// - Header
+/// - Indent
+/// - List
+/// - Text Alignment
+/// - Text Direction
+/// - Code Block
+class Block extends Container {
+ @override
+ Line get defaultChild => Line();
+
+ /// Creates new unmounted [Block].
+ @override
+ Node newInstance() => Block();
+
+ @override
+ Delta toDelta() {
+ return children
+ .map((child) => child.toDelta())
+ .fold(Delta(), (a, b) => a.concat(b));
+ }
+
+ @override
+ void adjust() {
+ if (isEmpty) {
+ final sibling = previous;
+ unlink();
+ if (sibling != null) {
+ sibling.adjust();
+ }
+ return;
+ }
+
+ var block = this;
+ final prev = block.previous;
+ // merging it with previous block if style is the same
+ if (!block.isFirst &&
+ block.previous is Block &&
+ prev!.style == block.style) {
+ block
+ ..moveChildToNewParent(prev as Container?)
+ ..unlink();
+ block = prev as Block;
+ }
+ final next = block.next;
+ // merging it with next block if style is the same
+ if (!block.isLast && block.next is Block && next!.style == block.style) {
+ (next as Block).moveChildToNewParent(block);
+ next.unlink();
+ }
+ }
+
+ @override
+ String toString() {
+ final block = style.attributes.toString();
+ final buffer = StringBuffer(' {$block}\n');
+ for (final child in children) {
+ final tree = child.isLast ? '└' : '├';
+ buffer.write(' $tree $child');
+ if (!child.isLast) buffer.writeln();
+ }
+ return buffer.toString();
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/node/container.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/node/container.dart
new file mode 100644
index 0000000000..e336ad867d
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/node/container.dart
@@ -0,0 +1,159 @@
+import 'dart:collection';
+
+import '../style.dart';
+import 'line.dart';
+import 'node.dart';
+import 'leaf.dart';
+
+/// Container can accommodate other nodes.
+///
+/// Delegates insert, retain and delete operations to children nodes. For each
+/// operation container looks for a child at specified index position and
+/// forwards operation to that child.
+///
+/// Most of the operation handling logic is implemented by [Line] and [Text].
+abstract class Container extends Node {
+ final LinkedList _children = LinkedList();
+
+ /// List of children.
+ LinkedList get children => _children;
+
+ /// Returns total number of child nodes in this container.
+ ///
+ /// To get text length of this container see [length].
+ int get childCount => _children.length;
+
+ /// Returns the first child [Node].
+ Node get first => _children.first;
+
+ /// Returns the last child [Node].
+ Node get last => _children.last;
+
+ /// Returns `true` if this container has no child nodes.
+ bool get isEmpty => _children.isEmpty;
+
+ /// Returns `true` if this container has at least 1 child.
+ bool get isNotEmpty => _children.isNotEmpty;
+
+ /// Returns an instance of default child for this container node.
+ ///
+ /// Always returns fresh instance.
+ T get defaultChild;
+
+ /// Content length of this node's children.
+ ///
+ /// To get number of children in this node use [childCount].
+ @override
+ int get length => _children.fold(0, (curr, node) => curr + node.length);
+
+ /// Adds [node] to the end of this container children list.
+ void add(T node) {
+ assert(node?.parent == null);
+ node?.parent = this;
+ _children.add(node as Node);
+ }
+
+ /// Adds [node] to the beginning of this container children list.
+ void addFirst(T node) {
+ assert(node?.parent == null);
+ node?.parent = this;
+ _children.addFirst(node as Node);
+ }
+
+ /// Removes [node] from this container.
+ void remove(T node) {
+ assert(node?.parent == this);
+ node?.parent = null;
+ _children.remove(node as Node);
+ }
+
+ /// Moves children of this node to [newParent].
+ void moveChildToNewParent(Container? newParent) {
+ if (isEmpty) return;
+
+ final last = newParent!.isEmpty ? null : newParent.last as T?;
+ while (isNotEmpty) {
+ final child = first as T;
+ child?.unlink();
+ newParent.add(child);
+ }
+
+ /// In case [newParent] already had children we need to make sure
+ /// combined list is optimized.
+ if (last != null) last.adjust();
+ }
+
+ /// Queries the child [Node] at specified character [offset] in this container.
+ ///
+ /// The result may contain the found node or `null` if no node is found
+ /// at specified offset.
+ ///
+ /// [ChildQuery.offset] is set to relative offset within returned child node
+ /// which points at the same character position in the document as the
+ /// original [offset].
+ ChildQuery queryChild(int offset, bool inclusive) {
+ if (offset < 0 || offset > length) {
+ return ChildQuery(null, 0);
+ }
+
+ for (final child in children) {
+ final childLen = child.length;
+ if (offset < childLen ||
+ (inclusive && offset == childLen && (child.isLast))) {
+ return ChildQuery(child, offset);
+ }
+ offset -= childLen;
+ }
+ return ChildQuery(null, 0);
+ }
+
+ @override
+ void insert(int index, Object data, Style? style) {
+ assert(index == 0 || (index > 0 && index < length));
+
+ if (isNotEmpty) {
+ final child = queryChild(index, false);
+ child.node!.insert(child.offset, data, style);
+ return;
+ }
+
+ // empty
+ assert(index == 0);
+ final node = defaultChild;
+ add(node);
+ node?.insert(index, data, style);
+ }
+
+ @override
+ void retain(int index, int? length, Style? attributes) {
+ assert(isNotEmpty);
+ final child = queryChild(index, false);
+ child.node!.retain(child.offset, length, attributes);
+ }
+
+ @override
+ void delete(int index, int? length) {
+ assert(isNotEmpty);
+ final child = queryChild(index, false);
+ child.node!.delete(child.offset, length);
+ }
+
+ @override
+ String toPlainText() => children.map((child) => child.toPlainText()).join();
+
+ @override
+ String toString() => _children.join('\n');
+}
+
+/// Result of a child query in a [Container].
+class ChildQuery {
+ ChildQuery(this.node, this.offset);
+
+ /// The child node if found, otherwise `null`.
+ final Node? node;
+
+ /// Starting offset within the child [node] which points at the same
+ /// character in the document as the original offset passed to
+ /// [Container.queryChild] method.
+ final int offset;
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/node/embed.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/node/embed.dart
new file mode 100644
index 0000000000..f49b676c87
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/node/embed.dart
@@ -0,0 +1,36 @@
+/// An object which can be embedded into a Quill document.
+///
+/// See also:
+///
+/// * [BlockEmbed] which represents a block embed.
+class Embeddable {
+ Map toJson() => {type: data};
+
+ static Embeddable fromJson(Map json) {
+ final mp = Map.from(json);
+ assert(mp.length == 1, 'Embeddable map has one key');
+ return BlockEmbed(mp.keys.first, mp.values.first);
+ }
+
+ /// The type of this object.
+ final String type;
+
+ /// The data payload of this object
+ final dynamic data;
+
+ Embeddable(this.type, this.data);
+}
+
+/// An object which occupies an entire line in a document and cannot co-exist
+/// inline with regular text.
+///
+/// There are two built-in embed types supported by Quill documents, however
+/// the document model itself does not make any assumptions about the types
+/// of embedded objects and allows users to define their own types.
+class BlockEmbed extends Embeddable {
+ BlockEmbed(String type, String data) : super(type, data);
+
+ static BlockEmbed horizontalRule = BlockEmbed('divider', 'hr');
+
+ static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl);
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/node/leaf.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/node/leaf.dart
new file mode 100644
index 0000000000..8a85f09612
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/node/leaf.dart
@@ -0,0 +1,254 @@
+import 'dart:math' as math;
+
+import '../../quill_delta.dart';
+import '../style.dart';
+import 'embed.dart';
+import 'line.dart';
+import 'node.dart';
+
+/// A leaf in Quill document tree.
+abstract class Leaf extends Node {
+ /// Creates a new [Leaf] with specified [data].
+ factory Leaf(Object data) {
+ if (data is Embeddable) {
+ return Embed(data);
+ }
+ final text = data as String;
+ assert(text.isNotEmpty);
+ return Text(text);
+ }
+
+ Leaf.val(Object val) : _value = val;
+
+ /// Contents of this node, either a String if this is a [Text] or an
+ /// [Embed] if this is an [BlockEmbed].
+ Object get value => _value;
+ Object _value;
+
+ @override
+ void applyStyle(Style value) {
+ assert(value.isInline || value.isIgnored || value.isEmpty,
+ 'Unable to apply Style to leaf: $value');
+ super.applyStyle(value);
+ }
+
+ @override
+ Line? get parent => super.parent as Line?;
+
+ @override
+ int get length {
+ if (_value is String) {
+ return (_value as String).length;
+ }
+ // return 1 for embedded object
+ return 1;
+ }
+
+ @override
+ Delta toDelta() {
+ final data =
+ _value is Embeddable ? (_value as Embeddable).toJson() : _value;
+ return Delta()..insert(data, style.toJson());
+ }
+
+ @override
+ void insert(int index, Object data, Style? style) {
+ assert(index >= 0 && index <= length);
+ final node = Leaf(data);
+ if (index < length) {
+ splitAt(index)!.insertBefore(node);
+ } else {
+ insertAfter(node);
+ }
+ node.format(style);
+ }
+
+ @override
+ void retain(int index, int? len, Style? style) {
+ if (style == null) {
+ return;
+ }
+
+ final local = math.min(length - index, len!);
+ final remain = len - local;
+ final node = _isolate(index, local);
+
+ if (remain > 0) {
+ assert(node.next != null);
+ node.next!.retain(0, remain, style);
+ }
+ node.format(style);
+ }
+
+ @override
+ void delete(int index, int? len) {
+ assert(index < length);
+
+ final local = math.min(length - index, len!);
+ final target = _isolate(index, local);
+ final prev = target.previous as Leaf?;
+ final next = target.next as Leaf?;
+ target.unlink();
+
+ final remain = len - local;
+ if (remain > 0) {
+ assert(next != null);
+ next!.delete(0, remain);
+ }
+
+ if (prev != null) {
+ prev.adjust();
+ }
+ }
+
+ /// Adjust this text node by merging it with adjacent nodes if they share
+ /// the same style.
+ @override
+ void adjust() {
+ if (this is Embed) {
+ // Embed nodes cannot be merged with text nor other embeds (in fact,
+ // there could be no two adjacent embeds on the same line since an
+ // embed occupies an entire line).
+ return;
+ }
+
+ // This is a text node and it can only be merged with other text nodes.
+ var node = this as Text;
+
+ // Merging it with previous node if style is the same.
+ final prev = node.previous;
+ if (!node.isFirst && prev is Text && prev.style == node.style) {
+ prev._value = prev.value + node.value;
+ node.unlink();
+ node = prev;
+ }
+
+ // Merging it with next node if style is the same.
+ final next = node.next;
+ if (!node.isLast && next is Text && next.style == node.style) {
+ node._value = node.value + next.value;
+ next.unlink();
+ }
+ }
+
+ /// Splits this leaf node at [index] and returns new node.
+ ///
+ /// If this is the last node in its list and [index] equals this node's
+ /// length then this method returns `null` as there is nothing left to split.
+ /// If there is another leaf node after this one and [index] equals this
+ /// node's length then the next leaf node is returned.
+ ///
+ /// If [index] equals to `0` then this node itself is returned unchanged.
+ ///
+ /// In case a new node is actually split from this one, it inherits this
+ /// node's style.
+ Leaf? splitAt(int index) {
+ assert(index >= 0 && index <= length);
+ if (index == 0) {
+ return this;
+ }
+ if (index == length) {
+ return isLast ? null : next as Leaf?;
+ }
+
+ assert(this is Text);
+ final text = _value as String;
+ _value = text.substring(0, index);
+ final split = Leaf(text.substring(index))..applyStyle(style);
+ insertAfter(split);
+ return split;
+ }
+
+ /// Cuts a leaf from [index] to the end of this node and returns new node
+ /// in detached state (e.g. [mounted] returns `false`).
+ ///
+ /// Splitting logic is identical to one described in [splitAt], meaning this
+ /// method may return `null`.
+ Leaf? cutAt(int index) {
+ assert(index >= 0 && index <= length);
+ final cut = splitAt(index);
+ cut?.unlink();
+ return cut;
+ }
+
+ /// Formats this node and optimizes it with adjacent leaf nodes if needed.
+ void format(Style? style) {
+ if (style != null && style.isNotEmpty) {
+ applyStyle(style);
+ }
+ adjust();
+ }
+
+ /// Isolates a new leaf starting at [index] with specified [length].
+ ///
+ /// Splitting logic is identical to one described in [splitAt], with one
+ /// exception that it is required for [index] to always be less than this
+ /// node's length. As a result this method always returns a [LeafNode]
+ /// instance. Returned node may still be the same as this node
+ /// if provided [index] is `0`.
+ Leaf _isolate(int index, int length) {
+ assert(
+ index >= 0 && index < this.length && (index + length <= this.length));
+ final target = splitAt(index)!..splitAt(length);
+ return target;
+ }
+}
+
+/* ---------------------------------- Impl ---------------------------------- */
+
+/// A span of formatted text within a line in a Quill document.
+///
+/// Text is a leaf node of a document tree.
+///
+/// Parent of a text node is always a [Line], and as a consequence text
+/// node's [value] cannot contain any line-break characters.
+///
+/// See also:
+///
+/// * [Embed], a leaf node representing an embeddable object.
+/// * [Line], a node representing a line of text.
+class Text extends Leaf {
+ Text([String text = ''])
+ : assert(!text.contains('\n')),
+ super.val(text);
+
+ @override
+ Node newInstance() => Text();
+
+ @override
+ String get value => _value as String;
+
+ @override
+ String toPlainText() => value;
+}
+
+/// An embed node inside of a line in a Quill document.
+///
+/// Embed node is a leaf node similar to [Text]. It represents an arbitrary
+/// piece of non-textual content embedded into a document, such as, image,
+/// horizontal rule, video, or any other object with defined structure,
+/// like a tweet, for instance.
+///
+/// Embed node's length is always `1` character and it is represented with
+/// unicode object replacement character in the document text.
+///
+/// Any inline style can be applied to an embed, however this does not
+/// necessarily mean the embed will look according to that style. For instance,
+/// applying "bold" style to an image gives no effect, while adding a "link" to
+/// an image actually makes the image react to user's action.
+class Embed extends Leaf {
+ Embed(Embeddable data) : super.val(data);
+
+ static const kObjectReplacementCharacter = '\uFFFC';
+
+ @override
+ Node newInstance() => throw UnimplementedError();
+
+ @override
+ Embeddable get value => super.value as Embeddable;
+
+ /// // Embed nodes are represented as unicode object replacement character in
+ // plain text.
+ @override
+ String toPlainText() => kObjectReplacementCharacter;
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/node/line.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/node/line.dart
new file mode 100644
index 0000000000..0b29b62c05
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/node/line.dart
@@ -0,0 +1,362 @@
+import 'dart:math' as math;
+
+import '../../quill_delta.dart';
+import '../attribute.dart';
+import '../style.dart';
+import 'block.dart';
+import 'container.dart';
+import 'embed.dart';
+import 'leaf.dart';
+import 'node.dart';
+
+/// A line of rich text in a Quill document.
+///
+/// Line serves as a container for [Leaf]s, like [Text] and [Embed].
+///
+/// When a line contains an embed, it fully occupies the line, no other embeds
+/// or text nodes are allowed.
+class Line extends Container {
+ @override
+ Leaf get defaultChild => Text();
+
+ @override
+ int get length => super.length + 1;
+
+ /// Returns `true` if this line contains an embedded object.
+ bool get hasEmbed {
+ if (childCount != 1) {
+ return false;
+ }
+ return children.single is Embed;
+ }
+
+ /// Returns next [Line] or `null` if this is the last line in the document.
+ Line? get nextLine {
+ if (!isLast) {
+ return next is Block ? (next as Block).first as Line? : next as Line?;
+ }
+ if (parent is! Block) {
+ return null;
+ }
+
+ if (parent!.isLast) {
+ return null;
+ }
+ return parent!.next is Block
+ ? (parent!.next as Block).first as Line?
+ : parent!.next as Line?;
+ }
+
+ @override
+ Node newInstance() => Line();
+
+ @override
+ Delta toDelta() {
+ final delta = children
+ .map((child) => child.toDelta())
+ .fold(Delta(), (dynamic a, b) => a.concat(b));
+ var attributes = style;
+ if (parent is Block) {
+ final block = parent as Block;
+ attributes = attributes.mergeAll(block.style);
+ }
+ delta.insert('\n', attributes.toJson());
+ return delta;
+ }
+
+ @override
+ String toPlainText() => '${super.toPlainText()}\n';
+
+ @override
+ String toString() {
+ final body = children.join(' → ');
+ final styleString = style.isNotEmpty ? ' $style' : '';
+ return '¶ $body ⏎$styleString';
+ }
+
+ @override
+ void insert(int index, Object data, Style? style) {
+ if (data is Embeddable) {
+ // We do not check whether this line already has any children here as
+ // inserting an embed into a line with other text is acceptable from the
+ // Delta format perspective.
+ // We rely on heuristic rules to ensure that embeds occupy an entire line.
+ _insertSafe(index, data, style);
+ return;
+ }
+
+ final text = data as String;
+ final lineBreak = text.indexOf('\n');
+ if (lineBreak < 0) {
+ _insertSafe(index, text, style);
+ // No need to update line or block format since those attributes can only
+ // be attached to `\n` character and we already know it's not present.
+ return;
+ }
+
+ final prefix = text.substring(0, lineBreak);
+ _insertSafe(index, prefix, style);
+ if (prefix.isNotEmpty) {
+ index += prefix.length;
+ }
+
+ // Next line inherits our format.
+ final nextLine = _getNextLine(index);
+
+ // Reset our format and unwrap from a block if needed.
+ clearStyle();
+ if (parent is Block) {
+ _unwrap();
+ }
+
+ // Now we can apply new format and re-layout.
+ _format(style);
+
+ // Continue with remaining part.
+ final remain = text.substring(lineBreak + 1);
+ nextLine.insert(0, remain, style);
+ }
+
+ @override
+ void retain(int index, int? len, Style? style) {
+ if (style == null) {
+ return;
+ }
+ final thisLength = length;
+
+ final local = math.min(thisLength - index, len!);
+ // If index is at newline character then this is a line/block style update.
+ final isLineFormat = (index + local == thisLength) && local == 1;
+
+ if (isLineFormat) {
+ assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK),
+ 'It is not allowed to apply inline attributes to line itself.');
+ _format(style);
+ } else {
+ // Otherwise forward to children as it's an inline format update.
+ assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE));
+ assert(index + local != thisLength);
+ super.retain(index, local, style);
+ }
+
+ final remain = len - local;
+ if (remain > 0) {
+ assert(nextLine != null);
+ nextLine!.retain(0, remain, style);
+ }
+ }
+
+ @override
+ void delete(int index, int? len) {
+ final local = math.min(length - index, len!);
+ final isLFDeleted = index + local == length; // Line feed
+ if (isLFDeleted) {
+ // Our newline character deleted with all style information.
+ clearStyle();
+ if (local > 1) {
+ // Exclude newline character from delete range for children.
+ super.delete(index, local - 1);
+ }
+ } else {
+ super.delete(index, local);
+ }
+
+ final remaining = len - local;
+ if (remaining > 0) {
+ assert(nextLine != null);
+ nextLine!.delete(0, remaining);
+ }
+
+ if (isLFDeleted && isNotEmpty) {
+ // Since we lost our line-break and still have child text nodes those must
+ // migrate to the next line.
+
+ // nextLine might have been unmounted since last assert so we need to
+ // check again we still have a line after us.
+ assert(nextLine != null);
+
+ // Move remaining children in this line to the next line so that all
+ // attributes of nextLine are preserved.
+ nextLine!.moveChildToNewParent(this);
+ moveChildToNewParent(nextLine);
+ }
+
+ if (isLFDeleted) {
+ // Now we can remove this line.
+ final block = parent!; // remember reference before un-linking.
+ unlink();
+ block.adjust();
+ }
+ }
+
+ /// Formats this line.
+ void _format(Style? newStyle) {
+ if (newStyle == null || newStyle.isEmpty) {
+ return;
+ }
+
+ applyStyle(newStyle);
+ final blockStyle = newStyle.getBlockExceptHeader();
+ if (blockStyle == null) {
+ return;
+ } // No block-level changes
+
+ if (parent is Block) {
+ final parentStyle = (parent as Block).style.getBlockExceptHeader();
+ if (blockStyle.value == null) {
+ _unwrap();
+ } else if (blockStyle != parentStyle) {
+ _unwrap();
+ final block = Block()..applyAttribute(blockStyle);
+ _wrap(block);
+ block.adjust();
+ } // else the same style, no-op.
+ } else if (blockStyle.value != null) {
+ // Only wrap with a new block if this is not an unset
+ final block = Block()..applyAttribute(blockStyle);
+ _wrap(block);
+ block.adjust();
+ }
+ }
+
+ /// Wraps this line with new parent [block].
+ ///
+ /// This line can not be in a [Block] when this method is called.
+ void _wrap(Block block) {
+ assert(parent != null && parent is! Block);
+ insertAfter(block);
+ unlink();
+ block.add(this);
+ }
+
+ /// Unwraps this line from it's parent [Block].
+ ///
+ /// This method asserts if current [parent] of this line is not a [Block].
+ void _unwrap() {
+ if (parent is! Block) {
+ throw ArgumentError('Invalid parent');
+ }
+ final block = parent as Block;
+
+ assert(block.children.contains(this));
+
+ if (isFirst) {
+ unlink();
+ block.insertBefore(this);
+ } else if (isLast) {
+ unlink();
+ block.insertAfter(this);
+ } else {
+ final before = block.clone() as Block;
+ block.insertBefore(before);
+
+ var child = block.first as Line;
+ while (child != this) {
+ child.unlink();
+ before.add(child);
+ child = block.first as Line;
+ }
+ unlink();
+ block.insertBefore(this);
+ }
+ block.adjust();
+ }
+
+ Line _getNextLine(int index) {
+ assert(index == 0 || (index > 0 && index < length));
+
+ final line = clone() as Line;
+ insertAfter(line);
+ if (index == length - 1) {
+ return line;
+ }
+
+ final query = queryChild(index, false);
+ while (!query.node!.isLast) {
+ final next = (last as Leaf)..unlink();
+ line.addFirst(next);
+ }
+ final child = query.node as Leaf;
+ final cut = child.splitAt(query.offset);
+ cut?.unlink();
+ line.addFirst(cut);
+ return line;
+ }
+
+ void _insertSafe(int index, Object data, Style? style) {
+ assert(index == 0 || (index > 0 && index < length));
+
+ if (data is String) {
+ assert(!data.contains('\n'));
+ if (data.isEmpty) {
+ return;
+ }
+ }
+
+ if (isEmpty) {
+ final child = Leaf(data);
+ add(child);
+ child.format(style);
+ } else {
+ final result = queryChild(index, true);
+ result.node!.insert(result.offset, data, style);
+ }
+ }
+
+ /// Returns style for specified text range.
+ ///
+ /// Only attributes applied to all characters within this range are
+ /// included in the result. Inline and line level attributes are
+ /// handled separately, e.g.:
+ ///
+ /// - line attribute X is included in the result only if it exists for
+ /// every line within this range (partially included lines are counted).
+ /// - inline attribute X is included in the result only if it exists
+ /// for every character within this range (line-break characters excluded).
+ Style collectStyle(int offset, int len) {
+ final local = math.min(length - offset, len);
+ var result = Style();
+ final excluded = {};
+
+ void _handle(Style style) {
+ if (result.isEmpty) {
+ excluded.addAll(style.values);
+ } else {
+ for (final attr in result.values) {
+ if (!style.containsKey(attr.key)) {
+ excluded.add(attr);
+ }
+ }
+ }
+ final remaining = style.removeAll(excluded);
+ result = result.removeAll(excluded);
+ result = result.mergeAll(remaining);
+ }
+
+ final data = queryChild(offset, true);
+ var node = data.node as Leaf?;
+ if (node != null) {
+ result = result.mergeAll(node.style);
+ var pos = node.length - data.offset;
+ while (!node!.isLast && pos < local) {
+ node = node.next as Leaf?;
+ _handle(node!.style);
+ pos += node.length;
+ }
+ }
+
+ result = result.mergeAll(style);
+ if (parent is Block) {
+ final block = parent as Block;
+ result = result.mergeAll(block.style);
+ }
+
+ final remaining = len - local;
+ if (remaining > 0) {
+ final rest = nextLine!.collectStyle(0, remaining);
+ _handle(rest);
+ }
+
+ return result;
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/node/node.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/node/node.dart
new file mode 100644
index 0000000000..b5a916142b
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/node/node.dart
@@ -0,0 +1,127 @@
+import 'dart:collection';
+
+import '../../quill_delta.dart';
+import '../attribute.dart';
+import '../style.dart';
+import 'container.dart';
+import 'line.dart';
+
+/// An abstract node in a document tree.
+///
+/// Represents a segment of a Quill document with specified [offset]
+/// and [length].
+///
+/// The [offset] property is relative to [parent]. See also [documentOffset]
+/// which provides absolute offset of this node within the document.
+///
+/// The current parent node is exposed by the [parent] property.
+abstract class Node extends LinkedListEntry {
+ /// Current parent of this node. May be null if this node is not mounted.
+ Container? parent;
+
+ Style get style => _style;
+ Style _style = Style();
+
+ /// Returns `true` if this node is the first node in the [parent] list.
+ bool get isFirst => list!.first == this;
+
+ /// Returns `true` if this node is the last node in the [parent] list.
+ bool get isLast => list!.last == this;
+
+ /// Length of this node in characters.
+ int get length;
+
+ Node clone() => newInstance()..applyStyle(style);
+
+ /// Offset in characters of this node relative to [parent] node.
+ ///
+ /// To get offset of this node in the document see [documentOffset].
+ int get offset {
+ var offset = 0;
+ if (list == null || isFirst) {
+ return offset;
+ }
+ var curr = this;
+ do {
+ curr = curr.previous!;
+ offset += curr.length;
+ } while (!curr.isFirst);
+ return offset;
+ }
+
+ /// Offset in characters of this node in the document.
+ int get documentOffset {
+ final parentOffset = (parent is! Root) ? parent!.documentOffset : 0;
+ return parentOffset + offset;
+ }
+
+ /// Returns `true` if this node contains character at specified [offset] in
+ /// the document.
+ bool containsOffset(int offset) {
+ final docOffset = documentOffset;
+ return docOffset <= offset && offset < docOffset + length;
+ }
+
+ void applyAttribute(Attribute attribute) {
+ _style = _style.merge(attribute);
+ }
+
+ void applyStyle(Style otherStyle) {
+ _style = _style.mergeAll(otherStyle);
+ }
+
+ void clearStyle() {
+ _style = Style();
+ }
+
+ @override
+ void insertBefore(Node entry) {
+ assert(entry.parent == null && parent != null);
+ entry.parent = parent;
+ super.insertBefore(entry);
+ }
+
+ @override
+ void insertAfter(Node entry) {
+ assert(entry.parent == null && parent != null);
+ entry.parent = parent;
+ super.insertAfter(entry);
+ }
+
+ @override
+ void unlink() {
+ assert(parent != null);
+ parent = null;
+ super.unlink();
+ }
+
+ void adjust() {/* no-op */}
+
+ // Subclass overridden method
+
+ Node newInstance();
+
+ String toPlainText();
+
+ Delta toDelta();
+
+ void insert(int index, Object data, Style? style);
+
+ void retain(int index, int? length, Style? style);
+
+ void delete(int index, int? length);
+}
+
+/// Root node of document tree.
+class Root extends Container> {
+ @override
+ Node newInstance() => Root();
+
+ @override
+ Container get defaultChild => Line();
+
+ @override
+ Delta toDelta() => children
+ .map((child) => child.toDelta())
+ .fold(Delta(), (a, b) => a.concat(b));
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/style.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/style.dart
new file mode 100644
index 0000000000..719e56822f
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/document/style.dart
@@ -0,0 +1,113 @@
+import 'package:collection/collection.dart';
+import 'package:quiver/core.dart';
+
+import 'attribute.dart';
+
+class Style {
+ Style() : _attributes = {};
+
+ Style.attr(this._attributes);
+
+ final Map _attributes;
+
+ static Style fromJson(Map? attributes) {
+ if (attributes == null) {
+ return Style();
+ }
+ final result = attributes.map((key, value) {
+ final attr = Attribute.fromKeyValue(key, value);
+ return MapEntry(key, attr);
+ });
+ return Style.attr(result);
+ }
+
+ Map? toJson() => _attributes.isEmpty
+ ? null
+ : _attributes.map((_, attr) {
+ return MapEntry(attr.key, attr.value);
+ });
+
+ // Properties
+
+ Map get attributes => _attributes;
+
+ Iterable get keys => _attributes.keys;
+
+ Iterable get values => _attributes.values;
+
+ bool get isEmpty => _attributes.isEmpty;
+
+ bool get isNotEmpty => _attributes.isNotEmpty;
+
+ bool get isInline => isNotEmpty && values.every((ele) => ele.isInline);
+
+ bool get isIgnored => isNotEmpty && values.every((ele) => ele.isIgnored);
+
+ Attribute get single => values.single;
+
+ bool containsKey(String key) => _attributes.containsKey(key);
+
+ Attribute? getBlockExceptHeader() {
+ for (final value in values) {
+ if (value.isBlockExceptHeader) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ // Operators
+
+ Style merge(Attribute attribute) {
+ final merged = Map.from(_attributes);
+ if (attribute.value == null) {
+ merged.remove(attribute.key);
+ } else {
+ merged[attribute.key] = attribute;
+ }
+ return Style.attr(merged);
+ }
+
+ Style mergeAll(Style other) {
+ var result = Style.attr(_attributes);
+ other.values.forEach((attr) {
+ result = result.merge(attr);
+ });
+ return result;
+ }
+
+ Style removeAll(Set attributes) {
+ final merged = Map.from(_attributes);
+ attributes.map((ele) => ele.key).forEach(merged.remove);
+ return Style.attr(merged);
+ }
+
+ Style put(Attribute attribute) {
+ final merged = Map.from(_attributes);
+ merged[attribute.key] = attribute;
+ return Style.attr(merged);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other is! Style) {
+ return false;
+ }
+ final typedOther = other;
+ const eq = MapEquality();
+ return eq.equals(_attributes, typedOther._attributes);
+ }
+
+ @override
+ int get hashCode {
+ final hashes =
+ _attributes.entries.map((entry) => hash2(entry.key, entry.value));
+ return hashObjects(hashes);
+ }
+
+ @override
+ String toString() => "{${_attributes.values.join(', ')}}";
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/heuristic/delete.dart b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/delete.dart
new file mode 100644
index 0000000000..0bbb11c303
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/delete.dart
@@ -0,0 +1,124 @@
+import '../quill_delta.dart';
+import '../document/attribute.dart';
+import 'rule.dart';
+
+abstract class DeleteRule extends Rule {
+ const DeleteRule();
+
+ @override
+ RuleType get type => RuleType.DELETE;
+
+ @override
+ void validateArgs(int? length, Object? data, Attribute? attribute) {
+ assert(length != null);
+ assert(data == null);
+ assert(attribute == null);
+ }
+}
+
+class CatchAllDeleteRule extends DeleteRule {
+ const CatchAllDeleteRule();
+
+ @override
+ Delta applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ return Delta()
+ ..retain(index)
+ ..delete(length!);
+ }
+}
+
+class PreserveLineStyleOnMergeRule extends DeleteRule {
+ const PreserveLineStyleOnMergeRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ final it = DeltaIterator(document)..skip(index);
+ var op = it.next(1);
+ if (op.data != '\n') {
+ return null;
+ }
+
+ final isNotPlain = op.isNotPlain;
+ final attrs = op.attributes;
+
+ it.skip(length! - 1);
+ final delta = Delta()
+ ..retain(index)
+ ..delete(length);
+
+ while (it.hasNext) {
+ op = it.next();
+ final text = op.data is String ? (op.data as String?)! : '';
+ final lineBreak = text.indexOf('\n');
+ if (lineBreak == -1) {
+ delta.retain(op.length!);
+ continue;
+ }
+
+ var attributes = op.attributes == null
+ ? null
+ : op.attributes!.map(
+ (key, dynamic value) => MapEntry(key, null));
+
+ if (isNotPlain) {
+ attributes ??= {};
+ attributes.addAll(attrs!);
+ }
+ delta..retain(lineBreak)..retain(1, attributes);
+ break;
+ }
+ return delta;
+ }
+}
+
+class EnsureEmbedLineRule extends DeleteRule {
+ const EnsureEmbedLineRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ final it = DeltaIterator(document);
+
+ var op = it.skip(index);
+ int? indexDelta = 0, lengthDelta = 0, remain = length;
+ var embedFound = op != null && op.data is! String;
+ final hasLineBreakBefore =
+ !embedFound && (op == null || (op.data as String).endsWith('\n'));
+ if (embedFound) {
+ var candidate = it.next(1);
+ if (remain != null) {
+ remain--;
+ if (candidate.data == '\n') {
+ indexDelta++;
+ lengthDelta--;
+
+ candidate = it.next(1);
+ remain--;
+ if (candidate.data == '\n') {
+ lengthDelta++;
+ }
+ }
+ }
+ }
+
+ op = it.skip(remain!);
+ if (op != null &&
+ (op.data is String ? op.data as String? : '')!.endsWith('\n')) {
+ final candidate = it.next(1);
+ if (candidate.data is! String && !hasLineBreakBefore) {
+ embedFound = true;
+ lengthDelta--;
+ }
+ }
+
+ if (!embedFound) {
+ return null;
+ }
+
+ return Delta()
+ ..retain(index + indexDelta)
+ ..delete(length! + lengthDelta);
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/heuristic/format.dart b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/format.dart
new file mode 100644
index 0000000000..8f83328b04
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/format.dart
@@ -0,0 +1,134 @@
+import '../quill_delta.dart';
+import '../document/attribute.dart';
+import 'rule.dart';
+
+abstract class FormatRule extends Rule {
+ const FormatRule();
+
+ @override
+ RuleType get type => RuleType.FORMAT;
+
+ @override
+ void validateArgs(int? length, Object? data, Attribute? attribute) {
+ assert(length != null);
+ assert(data == null);
+ assert(attribute != null);
+ }
+}
+
+/* -------------------------------- Rule Impl ------------------------------- */
+
+class ResolveLineFormatRule extends FormatRule {
+ const ResolveLineFormatRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (attribute!.scope != AttributeScope.BLOCK) {
+ return null;
+ }
+
+ var delta = Delta()..retain(index);
+ final it = DeltaIterator(document)..skip(index);
+ Operation op;
+ for (var cur = 0; cur < length! && it.hasNext; cur += op.length!) {
+ op = it.next(length - cur);
+ if (op.data is! String || !(op.data as String).contains('\n')) {
+ delta.retain(op.length!);
+ continue;
+ }
+ final text = op.data as String;
+ final tmp = Delta();
+ var offset = 0;
+
+ for (var lineBreak = text.indexOf('\n');
+ lineBreak >= 0;
+ lineBreak = text.indexOf('\n', offset)) {
+ tmp..retain(lineBreak - offset)..retain(1, attribute.toJson());
+ offset = lineBreak + 1;
+ }
+ tmp.retain(text.length - offset);
+ delta = delta.concat(tmp);
+ }
+
+ while (it.hasNext) {
+ op = it.next();
+ final text = op.data is String ? (op.data as String?)! : '';
+ final lineBreak = text.indexOf('\n');
+ if (lineBreak < 0) {
+ delta.retain(op.length!);
+ continue;
+ }
+ delta..retain(lineBreak)..retain(1, attribute.toJson());
+ break;
+ }
+ return delta;
+ }
+}
+
+class FormatLinkAtCaretPositionRule extends FormatRule {
+ const FormatLinkAtCaretPositionRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (attribute!.key != Attribute.link.key || length! > 0) {
+ return null;
+ }
+
+ final delta = Delta();
+ final it = DeltaIterator(document);
+ final before = it.skip(index), after = it.next();
+ int? beg = index, retain = 0;
+ if (before != null && before.hasAttribute(attribute.key)) {
+ beg -= before.length!;
+ retain = before.length;
+ }
+ if (after.hasAttribute(attribute.key)) {
+ if (retain != null) retain += after.length!;
+ }
+ if (retain == 0) {
+ return null;
+ }
+
+ delta..retain(beg)..retain(retain!, attribute.toJson());
+ return delta;
+ }
+}
+
+class ResolveInlineFormatRule extends FormatRule {
+ const ResolveInlineFormatRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (attribute!.scope != AttributeScope.INLINE) {
+ return null;
+ }
+
+ final delta = Delta()..retain(index);
+ final it = DeltaIterator(document)..skip(index);
+
+ Operation op;
+ for (var cur = 0; cur < length! && it.hasNext; cur += op.length!) {
+ op = it.next(length - cur);
+ final text = op.data is String ? (op.data as String?)! : '';
+ var lineBreak = text.indexOf('\n');
+ if (lineBreak < 0) {
+ delta.retain(op.length!, attribute.toJson());
+ continue;
+ }
+ var pos = 0;
+ while (lineBreak >= 0) {
+ delta..retain(lineBreak - pos, attribute.toJson())..retain(1);
+ pos = lineBreak + 1;
+ lineBreak = text.indexOf('\n', pos);
+ }
+ if (pos < op.length!) {
+ delta.retain(op.length! - pos, attribute.toJson());
+ }
+ }
+
+ return delta;
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/heuristic/insert.dart b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/insert.dart
new file mode 100644
index 0000000000..eb30fb6c2e
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/insert.dart
@@ -0,0 +1,382 @@
+import 'package:tuple/tuple.dart';
+
+import '../quill_delta.dart';
+import '../document/style.dart';
+import '../document/attribute.dart';
+import 'rule.dart';
+
+abstract class InsertRule extends Rule {
+ const InsertRule();
+
+ @override
+ RuleType get type => RuleType.INSERT;
+
+ @override
+ void validateArgs(int? length, Object? data, Attribute? attribute) {
+ assert(data != null);
+ assert(attribute == null);
+ }
+}
+
+/* -------------------------------- Rule Impl ------------------------------- */
+
+class PreserveLineStyleOnSplitRule extends InsertRule {
+ const PreserveLineStyleOnSplitRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (data is! String || data != '\n') {
+ return null;
+ }
+
+ final it = DeltaIterator(document);
+ final before = it.skip(index);
+ if (before == null ||
+ before.data is! String ||
+ (before.data as String).endsWith('\n')) {
+ return null;
+ }
+ final after = it.next();
+ if (after.data is! String || (after.data as String).startsWith('\n')) {
+ return null;
+ }
+
+ final text = after.data as String;
+ final delta = Delta()..retain(index + (length ?? 0));
+ if (text.contains('\n')) {
+ assert(after.isPlain);
+ delta.insert('\n');
+ return delta;
+ }
+
+ final nextNewLine = _getNextNewLine(it);
+ final attributes = nextNewLine.item1?.attributes;
+
+ return delta..insert('\n', attributes);
+ }
+}
+
+class PreserveBlockStyleOnInsertRule extends InsertRule {
+ const PreserveBlockStyleOnInsertRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (data is! String || !data.contains('\n')) {
+ return null;
+ }
+
+ final it = DeltaIterator(document)..skip(index);
+
+ final nextNewLine = _getNextNewLine(it);
+ final lineStyle = Style.fromJson(
+ nextNewLine.item1?.attributes ?? {},
+ );
+
+ final attribute = lineStyle.getBlockExceptHeader();
+ if (attribute == null) {
+ return null;
+ }
+
+ final blockStyle = {attribute.key: attribute.value};
+
+ Map? resetStyle;
+
+ if (lineStyle.containsKey(Attribute.header.key)) {
+ resetStyle = Attribute.header.toJson();
+ }
+
+ final lines = data.split('\n');
+ final delta = Delta()..retain(index + (length ?? 0));
+ for (var i = 0; i < lines.length; i++) {
+ final line = lines[i];
+ if (line.isNotEmpty) {
+ delta.insert(line);
+ }
+ if (i == 0) {
+ delta.insert('\n', lineStyle.toJson());
+ } else if (i < lines.length - 1) {
+ delta.insert('\n', blockStyle);
+ }
+ }
+
+ if (resetStyle != null) {
+ delta
+ ..retain(nextNewLine.item2!)
+ ..retain((nextNewLine.item1!.data as String).indexOf('\n'))
+ ..retain(1, resetStyle);
+ }
+
+ return delta;
+ }
+}
+
+class AutoExitBlockRule extends InsertRule {
+ const AutoExitBlockRule();
+
+ bool _isEmptyLine(Operation? before, Operation? after) {
+ if (before == null) {
+ return true;
+ }
+ return before.data is String &&
+ (before.data as String).endsWith('\n') &&
+ after!.data is String &&
+ (after.data as String).startsWith('\n');
+ }
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (data is! String || data != '\n') {
+ return null;
+ }
+
+ final it = DeltaIterator(document);
+ final prev = it.skip(index), cur = it.next();
+ final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader();
+ if (cur.isPlain || blockStyle == null) {
+ return null;
+ }
+ if (!_isEmptyLine(prev, cur)) {
+ return null;
+ }
+
+ if ((cur.value as String).length > 1) {
+ return null;
+ }
+
+ final nextNewLine = _getNextNewLine(it);
+ if (nextNewLine.item1 != null &&
+ nextNewLine.item1!.attributes != null &&
+ Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() ==
+ blockStyle) {
+ return null;
+ }
+
+ final attributes = cur.attributes ?? {};
+ final k = attributes.keys
+ .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k));
+ attributes[k] = null;
+ // retain(1) should be '\n', set it with no attribute
+ return Delta()..retain(index + (length ?? 0))..retain(1, attributes);
+ }
+}
+
+class ResetLineFormatOnNewLineRule extends InsertRule {
+ const ResetLineFormatOnNewLineRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (data is! String || data != '\n') {
+ return null;
+ }
+
+ final itr = DeltaIterator(document)..skip(index);
+ final cur = itr.next();
+ if (cur.data is! String || !(cur.data as String).startsWith('\n')) {
+ return null;
+ }
+
+ Map? resetStyle;
+ if (cur.attributes != null &&
+ cur.attributes!.containsKey(Attribute.header.key)) {
+ resetStyle = Attribute.header.toJson();
+ }
+ return Delta()
+ ..retain(index + (length ?? 0))
+ ..insert('\n', cur.attributes)
+ ..retain(1, resetStyle)
+ ..trim();
+ }
+}
+
+class InsertEmbedsRule extends InsertRule {
+ const InsertEmbedsRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (data is String) {
+ return null;
+ }
+
+ final delta = Delta()..retain(index + (length ?? 0));
+ final it = DeltaIterator(document);
+ final prev = it.skip(index), cur = it.next();
+
+ final textBefore = prev?.data is String ? prev!.data as String? : '';
+ final textAfter = cur.data is String ? (cur.data as String?)! : '';
+
+ final isNewlineBefore = prev == null || textBefore!.endsWith('\n');
+ final isNewlineAfter = textAfter.startsWith('\n');
+
+ if (isNewlineBefore && isNewlineAfter) {
+ return delta..insert(data);
+ }
+
+ Map? lineStyle;
+ if (textAfter.contains('\n')) {
+ lineStyle = cur.attributes;
+ } else {
+ while (it.hasNext) {
+ final op = it.next();
+ if ((op.data is String ? op.data as String? : '')!.contains('\n')) {
+ lineStyle = op.attributes;
+ break;
+ }
+ }
+ }
+
+ if (!isNewlineBefore) {
+ delta.insert('\n', lineStyle);
+ }
+ delta.insert(data);
+ if (!isNewlineAfter) {
+ delta.insert('\n');
+ }
+ return delta;
+ }
+}
+
+class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
+ const ForceNewlineForInsertsAroundEmbedRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (data is! String) {
+ return null;
+ }
+
+ final text = data;
+ final it = DeltaIterator(document);
+ final prev = it.skip(index), cur = it.next();
+ final cursorBeforeEmbed = cur.data is! String;
+ final cursorAfterEmbed = prev != null && prev.data is! String;
+
+ if (!cursorBeforeEmbed && !cursorAfterEmbed) {
+ return null;
+ }
+ final delta = Delta()..retain(index + (length ?? 0));
+ if (cursorBeforeEmbed && !text.endsWith('\n')) {
+ return delta..insert(text)..insert('\n');
+ }
+ if (cursorAfterEmbed && !text.startsWith('\n')) {
+ return delta..insert('\n')..insert(text);
+ }
+ return delta..insert(text);
+ }
+}
+
+class AutoFormatLinksRule extends InsertRule {
+ const AutoFormatLinksRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (data is! String || data != ' ') {
+ return null;
+ }
+
+ final it = DeltaIterator(document);
+ final prev = it.skip(index);
+ if (prev == null || prev.data is! String) {
+ return null;
+ }
+
+ try {
+ final cand = (prev.data as String).split('\n').last.split(' ').last;
+ final link = Uri.parse(cand);
+ if (!['https', 'http'].contains(link.scheme)) {
+ return null;
+ }
+ final attributes = prev.attributes ?? {};
+
+ if (attributes.containsKey(Attribute.link.key)) {
+ return null;
+ }
+
+ attributes.addAll(LinkAttribute(link.toString()).toJson());
+ return Delta()
+ ..retain(index + (length ?? 0) - cand.length)
+ ..retain(cand.length, attributes)
+ ..insert(data, prev.attributes);
+ } on FormatException {
+ return null;
+ }
+ }
+}
+
+class PreserveInlineStylesRule extends InsertRule {
+ const PreserveInlineStylesRule();
+
+ @override
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ if (data is! String || data.contains('\n')) {
+ return null;
+ }
+
+ final it = DeltaIterator(document);
+ final prev = it.skip(index);
+ if (prev == null ||
+ prev.data is! String ||
+ (prev.data as String).contains('\n')) {
+ return null;
+ }
+
+ final attributes = prev.attributes;
+ final text = data;
+ if (attributes == null || !attributes.containsKey(Attribute.link.key)) {
+ return Delta()
+ ..retain(index + (length ?? 0))
+ ..insert(text, attributes);
+ }
+
+ attributes.remove(Attribute.link.key);
+ final delta = Delta()
+ ..retain(index + (length ?? 0))
+ ..insert(text, attributes.isEmpty ? null : attributes);
+ final next = it.next();
+
+ final nextAttributes = next.attributes ?? const {};
+ if (!nextAttributes.containsKey(Attribute.link.key)) {
+ return delta;
+ }
+ if (attributes[Attribute.link.key] == nextAttributes[Attribute.link.key]) {
+ return Delta()
+ ..retain(index + (length ?? 0))
+ ..insert(text, attributes);
+ }
+ return delta;
+ }
+}
+
+class CatchAllInsertRule extends InsertRule {
+ const CatchAllInsertRule();
+
+ @override
+ Delta applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ return Delta()
+ ..retain(index + (length ?? 0))
+ ..insert(data);
+ }
+}
+
+/* --------------------------------- Helper --------------------------------- */
+
+Tuple2 _getNextNewLine(DeltaIterator iterator) {
+ Operation op;
+ for (var skipped = 0; iterator.hasNext; skipped += op.length!) {
+ op = iterator.next();
+ final lineBreak =
+ (op.data is String ? op.data as String? : '')!.indexOf('\n');
+ if (lineBreak >= 0) {
+ return Tuple2(op, skipped);
+ }
+ }
+ return const Tuple2(null, null);
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/heuristic/rule.dart b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/rule.dart
new file mode 100644
index 0000000000..8dad3fa964
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/rule.dart
@@ -0,0 +1,81 @@
+import '../document/attribute.dart';
+import '../document/document.dart';
+import '../quill_delta.dart';
+import 'insert.dart';
+import 'delete.dart';
+import 'format.dart';
+
+enum RuleType {
+ INSERT,
+ DELETE,
+ FORMAT,
+}
+
+abstract class Rule {
+ const Rule();
+
+ RuleType get type;
+
+ Delta? apply(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ validateArgs(length, data, attribute);
+ return applyRule(
+ document,
+ index,
+ length: length,
+ data: data,
+ attribute: attribute,
+ );
+ }
+
+ Delta? applyRule(Delta document, int index,
+ {int? length, Object? data, Attribute? attribute});
+
+ void validateArgs(int? length, Object? data, Attribute? attribute);
+}
+
+class Rules {
+ Rules(this._rules);
+
+ final List _rules;
+
+ static final Rules _instance = Rules([
+ const FormatLinkAtCaretPositionRule(),
+ const ResolveLineFormatRule(),
+ const ResolveInlineFormatRule(),
+ const InsertEmbedsRule(),
+ const ForceNewlineForInsertsAroundEmbedRule(),
+ const AutoExitBlockRule(),
+ const PreserveBlockStyleOnInsertRule(),
+ const PreserveLineStyleOnSplitRule(),
+ const ResetLineFormatOnNewLineRule(),
+ const AutoFormatLinksRule(),
+ const PreserveInlineStylesRule(),
+ const CatchAllInsertRule(),
+ const EnsureEmbedLineRule(),
+ const PreserveLineStyleOnMergeRule(),
+ const CatchAllDeleteRule(),
+ ]);
+
+ static Rules getInstance() => _instance;
+
+ Delta apply(RuleType ruleType, Document document, int index,
+ {int? length, Object? data, Attribute? attribute}) {
+ final delta = document.toDelta();
+ for (final rule in _rules) {
+ if (rule.type != ruleType) {
+ continue;
+ }
+ try {
+ final result = rule.apply(delta, index,
+ length: length, data: data, attribute: attribute);
+ if (result != null) {
+ return result..trim();
+ }
+ } catch (e) {
+ rethrow;
+ }
+ }
+ throw 'Apply rules failed';
+ }
+}
diff --git a/app_flowy/packages/flowy_editor/lib/src/model/quill_delta.dart b/app_flowy/packages/flowy_editor/lib/src/model/quill_delta.dart
new file mode 100644
index 0000000000..895b27fe87
--- /dev/null
+++ b/app_flowy/packages/flowy_editor/lib/src/model/quill_delta.dart
@@ -0,0 +1,684 @@
+// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code
+// is governed by a BSD-style license that can be found in the LICENSE file.
+
+/// Implementation of Quill Delta format in Dart.
+library quill_delta;
+
+import 'dart:math' as math;
+
+import 'package:collection/collection.dart';
+import 'package:quiver/core.dart';
+
+const _attributeEquality = DeepCollectionEquality();
+const _valueEquality = DeepCollectionEquality();
+
+/// Decoder function to convert raw `data` object into a user-defined data type.
+///
+/// Useful with embedded content.
+typedef DataDecoder = Object? Function(Object data);
+
+/// Default data decoder which simply passes through the original value.
+Object? _passThroughDataDecoder(Object? data) => data;
+
+/// Operation performed on a rich-text document.
+class Operation {
+ Operation._(this.key, this.length, this.data, Map? attributes)
+ : assert(_validKeys.contains(key), 'Invalid operation key "$key".'),
+ assert(() {
+ if (key != Operation.insertKey) return true;
+ return data is String ? data.length == length : length == 1;
+ }(), 'Length of insert operation must be equal to the data length.'),
+ _attributes =
+ attributes != null ? Map.from(attributes) : null;
+
+ /// Creates operation which deletes [length] of characters.
+ factory Operation.delete(int length) =>
+ Operation._(Operation.deleteKey, length, '', null);
+
+ /// Creates operation which inserts [text] with optional [attributes].
+ factory Operation.insert(dynamic data, [Map? attributes]) =>
+ Operation._(Operation.insertKey, data is String ? data.length : 1, data,
+ attributes);
+
+ /// Creates operation which retains [length] of characters and optionally
+ /// applies attributes.
+ factory Operation.retain(int? length, [Map? attributes]) =>
+ Operation._(Operation.retainKey, length, '', attributes);
+
+ /// Key of insert operations.
+ static const String insertKey = 'insert';
+
+ /// Key of delete operations.
+ static const String deleteKey = 'delete';
+
+ /// Key of retain operations.
+ static const String retainKey = 'retain';
+
+ /// Key of attributes collection.
+ static const String attributesKey = 'attributes';
+
+ static const List _validKeys = [insertKey, deleteKey, retainKey];
+
+ /// Key of this operation, can be "insert", "delete" or "retain".
+ final String key;
+
+ /// Length of this operation.
+ final int? length;
+
+ /// Payload of "insert" operation, for other types is set to empty string.
+ final Object? data;
+
+ /// Rich-text attributes set by this operation, can be `null`.
+ Map? get attributes =>
+ _attributes == null ? null : Map.from(_attributes!);
+ final Map? _attributes;
+
+ /// Creates new [Operation] from JSON payload.
+ ///
+ /// If `dataDecoder` parameter is not null then it is used to additionally
+ /// decode the operation's data object. Only applied to insert operations.
+ static Operation fromJson(Map data, {DataDecoder? dataDecoder}) {
+ dataDecoder ??= _passThroughDataDecoder;
+ final map = Map.from(data);
+ if (map.containsKey(Operation.insertKey)) {
+ final data = dataDecoder(map[Operation.insertKey]);
+ final dataLength = data is String ? data.length : 1;
+ return Operation._(
+ Operation.insertKey, dataLength, data, map[Operation.attributesKey]);
+ } else if (map.containsKey(Operation.deleteKey)) {
+ final int? length = map[Operation.deleteKey];
+ return Operation._(Operation.deleteKey, length, '', null);
+ } else if (map.containsKey(Operation.retainKey)) {
+ final int? length = map[Operation.retainKey];
+ return Operation._(
+ Operation.retainKey, length, '', map[Operation.attributesKey]);
+ }
+ throw ArgumentError.value(data, 'Invalid data for Delta operation.');
+ }
+
+ /// Returns JSON-serializable representation of this operation.
+ Map toJson() {
+ final json = {key: value};
+ if (_attributes != null) json[Operation.attributesKey] = attributes;
+ return json;
+ }
+
+ /// Returns value of this operation.
+ ///
+ /// For insert operations this returns text, for delete and retain - length.
+ dynamic get value => (key == Operation.insertKey) ? data : length;
+
+ /// Returns `true` if this is a delete operation.
+ bool get isDelete => key == Operation.deleteKey;
+
+ /// Returns `true` if this is an insert operation.
+ bool get isInsert => key == Operation.insertKey;
+
+ /// Returns `true` if this is a retain operation.
+ bool get isRetain => key == Operation.retainKey;
+
+ /// Returns `true` if this operation has no attributes, e.g. is plain text.
+ bool get isPlain => _attributes == null || _attributes!.isEmpty;
+
+ /// Returns `true` if this operation sets at least one attribute.
+ bool get isNotPlain => !isPlain;
+
+ /// Returns `true` is this operation is empty.
+ ///
+ /// An operation is considered empty if its [length] is equal to `0`.
+ bool get isEmpty => length == 0;
+
+ /// Returns `true` is this operation is not empty.
+ bool get isNotEmpty => length! > 0;
+
+ @override
+ bool operator ==(other) {
+ if (identical(this, other)) return true;
+ if (other is! Operation) return false;
+ final typedOther = other;
+ return key == typedOther.key &&
+ length == typedOther.length &&
+ _valueEquality.equals(data, typedOther.data) &&
+ hasSameAttributes(typedOther);
+ }
+
+ /// Returns `true` if this operation has attribute specified by [name].
+ bool hasAttribute(String name) =>
+ isNotPlain && _attributes!.containsKey(name);
+
+ /// Returns `true` if [other] operation has the same attributes as this one.
+ bool hasSameAttributes(Operation other) {
+ return _attributeEquality.equals(_attributes, other._attributes);
+ }
+
+ @override
+ int get hashCode {
+ if (_attributes != null && _attributes!.isNotEmpty) {
+ final attrsHash =
+ hashObjects(_attributes!.entries.map((e) => hash2(e.key, e.value)));
+ return hash3(key, value, attrsHash);
+ }
+ return hash2(key, value);
+ }
+
+ @override
+ String toString() {
+ final attr = attributes == null ? '' : ' + $attributes';
+ final text = isInsert
+ ? (data is String
+ ? (data as String).replaceAll('\n', '⏎')
+ : data.toString())
+ : '$length';
+ return '$key⟨ $text ⟩$attr';
+ }
+}
+
+/// Delta represents a document or a modification of a document as a sequence of
+/// insert, delete and retain operations.
+///
+/// Delta consisting of only "insert" operations is usually referred to as
+/// "document delta". When delta includes also "retain" or "delete" operations
+/// it is a "change delta".
+class Delta {
+ /// Creates new empty [Delta].
+ factory Delta() => Delta._([]);
+
+ Delta._(List operations) : _operations = operations;
+
+ /// Creates new [Delta] from [other].
+ factory Delta.from(Delta other) =>
+ Delta._(List.from(other._operations));
+
+ /// Transforms two attribute sets.
+ static Map? transformAttributes(
+ Map? a, Map? b, bool priority) {
+ if (a == null) return b;
+ if (b == null) return null;
+
+ if (!priority) return b;
+
+ final result = b.keys.fold