diff --git a/.github/workflows/dart_test.yml b/.github/workflows/dart_test.yml
index 74b20a2425..f0b42a506d 100644
--- a/.github/workflows/dart_test.yml
+++ b/.github/workflows/dart_test.yml
@@ -8,6 +8,7 @@ on:
pull_request:
branches:
- 'main'
+ - 'feat/flowy_editor'
env:
CARGO_TERM_COLOR: always
@@ -71,3 +72,8 @@ jobs:
flutter pub get
flutter test
+ - name: Run FlowyEditor tests
+ working-directory: frontend/app_flowy/packages/flowy_editor
+ run: |
+ flutter pub get
+ flutter test
\ No newline at end of file
diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json
index 2f5e152d62..d1732c5231 100644
--- a/frontend/.vscode/settings.json
+++ b/frontend/.vscode/settings.json
@@ -2,23 +2,21 @@
"[dart]": {
"editor.formatOnSave": true,
"editor.formatOnType": true,
- "editor.rulers": [
- 120
- ],
+ "editor.rulers": [80],
"editor.selectionHighlight": false,
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets",
- "editor.wordBasedSuggestions": false
+ "editor.wordBasedSuggestions": false,
},
"svgviewer.enableautopreview": true,
"svgviewer.previewcolumn": "Active",
"svgviewer.showzoominout": true,
- "editor.wordWrapColumn": 120,
+ "editor.wordWrapColumn": 80,
"editor.minimap.maxColumn": 140,
"prettier.printWidth": 140,
"editor.wordWrap": "wordWrapColumn",
- "dart.lineLength": 120,
+ "dart.lineLength": 80,
"files.associations": {
"*.log.*": "log"
},
diff --git a/frontend/app_flowy/packages/flowy_editor/.gitignore b/frontend/app_flowy/packages/flowy_editor/.gitignore
new file mode 100644
index 0000000000..96486fd930
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/.gitignore
@@ -0,0 +1,30 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# 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
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.packages
+build/
diff --git a/frontend/app_flowy/packages/flowy_editor/.metadata b/frontend/app_flowy/packages/flowy_editor/.metadata
new file mode 100644
index 0000000000..d3da16e67e
--- /dev/null
+++ b/frontend/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: cd41fdd495f6944ecd3506c21e94c6567b073278
+ channel: stable
+
+project_type: package
diff --git a/frontend/app_flowy/packages/flowy_editor/.vscode/launch.json b/frontend/app_flowy/packages/flowy_editor/.vscode/launch.json
new file mode 100644
index 0000000000..f27c363a13
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/.vscode/launch.json
@@ -0,0 +1,45 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "example",
+ "cwd": "example",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "example (profile mode)",
+ "cwd": "example",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "example (release mode)",
+ "cwd": "example",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flowy_editor",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flowy_editor (profile mode)",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flowy_editor (release mode)",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ ]
+}
\ No newline at end of file
diff --git a/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md b/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md
new file mode 100644
index 0000000000..41cc7d8192
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.
diff --git a/frontend/app_flowy/packages/flowy_editor/LICENSE b/frontend/app_flowy/packages/flowy_editor/LICENSE
new file mode 100644
index 0000000000..ba75c69f7f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/LICENSE
@@ -0,0 +1 @@
+TODO: Add your license here.
diff --git a/frontend/app_flowy/packages/flowy_editor/README.md b/frontend/app_flowy/packages/flowy_editor/README.md
new file mode 100644
index 0000000000..8b55e735b5
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/README.md
@@ -0,0 +1,39 @@
+
+
+TODO: Put a short description of the package here that helps potential users
+know whether this package might be useful for them.
+
+## Features
+
+TODO: List what your package can do. Maybe include images, gifs, or videos.
+
+## Getting started
+
+TODO: List prerequisites and provide or point to information on how to
+start using the package.
+
+## Usage
+
+TODO: Include short and useful examples for package users. Add longer examples
+to `/example` folder.
+
+```dart
+const like = 'sample';
+```
+
+## Additional information
+
+TODO: Tell users more about the package: where to find more information, how to
+contribute to the package, how to file issues, what response they can expect
+from the package authors, and more.
diff --git a/frontend/app_flowy/packages/flowy_editor/analysis_options.yaml b/frontend/app_flowy/packages/flowy_editor/analysis_options.yaml
new file mode 100644
index 0000000000..a5744c1cfb
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json
new file mode 100644
index 0000000000..fb3628de47
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json
@@ -0,0 +1,58 @@
+{
+ "document": {
+ "type": "root",
+ "children": [
+ {
+ "type": "text",
+ "delta": [],
+ "attributes": {
+ "subtype": "with-heading"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [],
+ "attributes": {
+ "tag": "*"
+ },
+ "children": [
+ {
+ "type": "text",
+ "delta": [],
+ "attributes": {
+ "text-type": "heading2",
+ "check": true
+ }
+ },
+ {
+ "type": "text",
+ "delta": [],
+ "attributes": {
+ "text-type": "checkbox",
+ "check": true
+ }
+ },
+ {
+ "type": "text",
+ "delta": [],
+ "attributes": {
+ "tag": "**"
+ }
+ }
+ ]
+ },
+ {
+ "type": "image",
+ "attributes": {
+ "url": "x.png"
+ }
+ },
+ {
+ "type": "video",
+ "attributes": {
+ "url": "x.mp4"
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/check.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/check.svg
new file mode 100644
index 0000000000..8446cced9f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/check.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg
new file mode 100644
index 0000000000..be88518d0d
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/point.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg
new file mode 100644
index 0000000000..0f3d33f6d3
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg
new file mode 100644
index 0000000000..85640695af
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg
new file mode 100644
index 0000000000..c2c962fa0b
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg
@@ -0,0 +1,8 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg
new file mode 100644
index 0000000000..3e57a6b000
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg
new file mode 100644
index 0000000000..6b739a761f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg
new file mode 100644
index 0000000000..2db0ab3b64
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg
new file mode 100644
index 0000000000..8e55d9e2e3
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg
new file mode 100644
index 0000000000..b37bb9acc0
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg
new file mode 100644
index 0000000000..933471e6a7
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/uncheck.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/uncheck.svg
new file mode 100644
index 0000000000..6c487795c6
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/assets/images/uncheck.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/.gitignore
new file mode 100644
index 0000000000..a8e938c083
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/.gitignore
@@ -0,0 +1,47 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# 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/frontend/app_flowy/packages/flowy_editor/example/.metadata b/frontend/app_flowy/packages/flowy_editor/example/.metadata
new file mode 100644
index 0000000000..ed0b5185fb
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/.metadata
@@ -0,0 +1,45 @@
+# 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.
+
+version:
+ revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ channel: stable
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ - platform: android
+ create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ - platform: ios
+ create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ - platform: linux
+ create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ - platform: macos
+ create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ - platform: web
+ create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ - platform: windows
+ create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+ base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json b/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json
new file mode 100644
index 0000000000..091adbfb6b
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json
@@ -0,0 +1,25 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "example",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "example (profile mode)",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "example (release mode)",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/app_flowy/packages/flowy_editor/example/README.md b/frontend/app_flowy/packages/flowy_editor/example/README.md
new file mode 100644
index 0000000000..2b3fce4c86
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/README.md
@@ -0,0 +1,16 @@
+# example
+
+A new Flutter project.
+
+## 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://docs.flutter.dev/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/frontend/app_flowy/packages/flowy_editor/example/analysis_options.yaml b/frontend/app_flowy/packages/flowy_editor/example/analysis_options.yaml
new file mode 100644
index 0000000000..61b6c4de17
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/analysis_options.yaml
@@ -0,0 +1,29 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at
+ # https://dart-lang.github.io/linter/lints/index.html.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/android/.gitignore
new file mode 100644
index 0000000000..6f568019d3
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/.gitignore
@@ -0,0 +1,13 @@
+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
+**/*.keystore
+**/*.jks
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/build.gradle b/frontend/app_flowy/packages/flowy_editor/example/android/app/build.gradle
new file mode 100644
index 0000000000..0833ecfca8
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/build.gradle
@@ -0,0 +1,71 @@
+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 flutter.compileSdkVersion
+ ndkVersion flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ 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.example.example"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
+ minSdkVersion flutter.minSdkVersion
+ targetSdkVersion flutter.targetSdkVersion
+ 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/frontend/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000000..45d523a2a2
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..3f41384dbc
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
new file mode 100644
index 0000000000..e793a000d6
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000000..06952be745
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..cb1ef88056
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000000..45d523a2a2
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/build.gradle b/frontend/app_flowy/packages/flowy_editor/example/android/build.gradle
new file mode 100644
index 0000000000..83ae220041
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ ext.kotlin_version = '1.6.10'
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.1.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/gradle.properties b/frontend/app_flowy/packages/flowy_editor/example/android/gradle.properties
new file mode 100644
index 0000000000..94adc3a3f9
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties b/frontend/app_flowy/packages/flowy_editor/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..cc5527d781
--- /dev/null
+++ b/frontend/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-7.4-all.zip
diff --git a/frontend/app_flowy/packages/flowy_editor/example/android/settings.gradle b/frontend/app_flowy/packages/flowy_editor/example/android/settings.gradle
new file mode 100644
index 0000000000..44e62bcf06
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json
new file mode 100644
index 0000000000..307b4bf92f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json
@@ -0,0 +1,245 @@
+{
+ "document": {
+ "type": "editor",
+ "attributes": {},
+ "children": [
+ {
+ "type": "image",
+ "attributes": {
+ "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "👋 Welcome to AppFlowy!",
+ "attributes": {
+ "href": "https://www.appflowy.io/",
+ "heading": "h1"
+ }
+ }
+ ],
+ "attributes": {
+ "heading": "h1"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ { "insert": "Here are the basics", "attributes": { "heading": "h2" } }
+ ],
+ "attributes": {
+ "heading": "h2"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [{ "insert": "Click anywhere and just start typing." }],
+ "attributes": {
+ "list": "todo",
+ "todo": false
+ }
+ },
+ {
+ "type": "text",
+ "delta": [{ "insert": "Click anywhere and just start typing." }],
+ "attributes": {
+ "list": "bullet"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [{ "insert": "Click anywhere and just start typing." }],
+ "attributes": {
+ "list": "bullet"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Highlight",
+ "attributes": { "highlight": "0xFFFFFF00" }
+ },
+ { "insert": " Click anywhere and just start typing" },
+ { "insert": " any text, and use the menu at the bottom to " },
+ { "insert": "style", "attributes": { "italic": true } },
+ { "insert": " your ", "attributes": { "bold": true } },
+ { "insert": "writing", "attributes": { "underline": true } },
+ {
+ "insert": " however you like.",
+ "attributes": { "strikethrough": true }
+ }
+ ],
+ "attributes": {
+ "checkbox": false
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ { "insert": "Have a question? ", "attributes": { "heading": "h2" } }
+ ],
+ "attributes": {
+ "heading": "h2"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "1. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {
+ "quotes": true
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "2. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "3. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "4. Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "5. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "6. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "7. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "8. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "9. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "10. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "11. Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Click the '?' at the bottom right for help and support."
+ }
+ ],
+ "attributes": {}
+ }
+ ]
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json
new file mode 100644
index 0000000000..c69237f24f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json
@@ -0,0 +1,247 @@
+{
+ "document": {
+ "type": "editor",
+ "attributes": {},
+ "children": [
+ {
+ "type": "image",
+ "attributes": {
+ "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "🌶 Read Me"
+ }
+ ],
+ "attributes": {
+ "subtype": "heading",
+ "heading": "h1"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "👋 Welcome to Appflowy"
+ }
+ ],
+ "attributes": {
+ "subtype": "heading",
+ "heading": "h2"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Here are the basics:"
+ }
+ ],
+ "attributes": {
+ "subtype": "heading",
+ "heading": "h3"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ { "insert": "Click " },
+ { "insert": "anywhere", "attributes": { "underline": true } },
+ { "insert": " and just typing." }
+ ]
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hit"
+ },
+ {
+ "insert": " / ",
+ "attributes": { "highlightColor": "0xFFFFFF00" }
+ },
+ {
+ "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc."
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Highlight any text, and use the menu that pops up to "
+ },
+ { "insert": "style", "attributes": { "bold": true } },
+ { "insert": " your ", "attributes": { "italic": true } },
+ { "insert": "writing", "attributes": { "strikethrough": true } },
+ { "insert": "." }
+ ]
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Here are the plugins:"
+ }
+ ],
+ "attributes": {
+ "subtype": "heading",
+ "heading": "h3"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "checkbox",
+ "checkbox": false
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "checkbox",
+ "checkbox": false
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "checkbox",
+ "checkbox": false
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "bulleted-list"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "bulleted-list"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello "
+ },
+ {
+ "insert": "world",
+ "attributes": { "bold": true }
+ }
+ ],
+ "attributes": {
+ "subtype": "bulleted-list"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "quote"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "quote"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "quote"
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "number-list",
+ "number": 1
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "number-list",
+ "number": 2
+ }
+ },
+ {
+ "type": "text",
+ "delta": [
+ {
+ "insert": "Hello world"
+ }
+ ],
+ "attributes": {
+ "subtype": "number-list",
+ "number": 3
+ }
+ }
+ ]
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/ios/.gitignore
new file mode 100644
index 0000000000..7a7f9873ad
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.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/ephemeral/
+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/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000000..8d4492f977
--- /dev/null
+++ b/frontend/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
+ 9.0
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000000..ec97fc6f30
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000000..c4855bfe20
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Podfile b/frontend/app_flowy/packages/flowy_editor/example/ios/Podfile
new file mode 100644
index 0000000000..1e8c3c90a5
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..813642b9a4
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,484 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ 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 */; };
+/* 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 */
+ 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 = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; 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 = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 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 */,
+ );
+ 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 = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ 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 = 1300;
+ 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 */
+ 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";
+ };
+ 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)";
+ DEVELOPMENT_TEAM = 446D3AAR7E;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ 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_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ 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)";
+ DEVELOPMENT_TEAM = 446D3AAR7E;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ 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)";
+ DEVELOPMENT_TEAM = 446D3AAR7E;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ 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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..919434a625
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000000..f9b0d7c5ea
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000000..c87d15a335
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..1d526a16ed
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000000..f9b0d7c5ea
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000000..70693e4a8c
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..d36b1fab2d
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000000..0bedcf2fd4
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000000..89c2725b70
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000000..f2e259c7c9
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000000..f3c28516fb
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist
new file mode 100644
index 0000000000..907f329fe0
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Example
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ 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
+
+ CADisableMinimumFrameDurationOnPhone
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000000..308a2a560b
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/expandable_floating_action_button.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/expandable_floating_action_button.dart
new file mode 100644
index 0000000000..01da3ab593
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/lib/expandable_floating_action_button.dart
@@ -0,0 +1,234 @@
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+
+// copy from https://docs.flutter.dev/cookbook/effects/expandable-fab
+@immutable
+class ExpandableFab extends StatefulWidget {
+ const ExpandableFab({
+ super.key,
+ this.initialOpen,
+ required this.distance,
+ required this.children,
+ });
+
+ final bool? initialOpen;
+ final double distance;
+ final List children;
+
+ @override
+ State createState() => _ExpandableFabState();
+}
+
+class _ExpandableFabState extends State
+ with SingleTickerProviderStateMixin {
+ late final AnimationController _controller;
+ late final Animation _expandAnimation;
+ bool _open = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _open = widget.initialOpen ?? false;
+ _controller = AnimationController(
+ value: _open ? 1.0 : 0.0,
+ duration: const Duration(milliseconds: 250),
+ vsync: this,
+ );
+ _expandAnimation = CurvedAnimation(
+ curve: Curves.fastOutSlowIn,
+ reverseCurve: Curves.easeOutQuad,
+ parent: _controller,
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ void _toggle() {
+ setState(() {
+ _open = !_open;
+ if (_open) {
+ _controller.forward();
+ } else {
+ _controller.reverse();
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox.expand(
+ child: Stack(
+ alignment: Alignment.bottomRight,
+ clipBehavior: Clip.none,
+ children: [
+ _buildTapToCloseFab(),
+ ..._buildExpandingActionButtons(),
+ _buildTapToOpenFab(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTapToCloseFab() {
+ return SizedBox(
+ width: 56.0,
+ height: 56.0,
+ child: Center(
+ child: Material(
+ shape: const CircleBorder(),
+ clipBehavior: Clip.antiAlias,
+ elevation: 4.0,
+ child: InkWell(
+ onTap: _toggle,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Icon(
+ Icons.close,
+ color: Theme.of(context).primaryColor,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ List _buildExpandingActionButtons() {
+ final children = [];
+ final count = widget.children.length;
+ final step = 90.0 / (count - 1);
+ for (var i = 0, angleInDegrees = 0.0;
+ i < count;
+ i++, angleInDegrees += step) {
+ children.add(
+ _ExpandingActionButton(
+ directionInDegrees: angleInDegrees,
+ maxDistance: widget.distance,
+ progress: _expandAnimation,
+ child: widget.children[i],
+ ),
+ );
+ }
+ return children;
+ }
+
+ Widget _buildTapToOpenFab() {
+ return IgnorePointer(
+ ignoring: _open,
+ child: AnimatedContainer(
+ transformAlignment: Alignment.center,
+ transform: Matrix4.diagonal3Values(
+ _open ? 0.7 : 1.0,
+ _open ? 0.7 : 1.0,
+ 1.0,
+ ),
+ duration: const Duration(milliseconds: 250),
+ curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
+ child: AnimatedOpacity(
+ opacity: _open ? 0.0 : 1.0,
+ curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
+ duration: const Duration(milliseconds: 250),
+ child: FloatingActionButton(
+ onPressed: _toggle,
+ child: const Icon(Icons.create),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+@immutable
+class _ExpandingActionButton extends StatelessWidget {
+ const _ExpandingActionButton({
+ required this.directionInDegrees,
+ required this.maxDistance,
+ required this.progress,
+ required this.child,
+ });
+
+ final double directionInDegrees;
+ final double maxDistance;
+ final Animation progress;
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedBuilder(
+ animation: progress,
+ builder: (context, child) {
+ final offset = Offset.fromDirection(
+ directionInDegrees * (math.pi / 180.0),
+ progress.value * maxDistance,
+ );
+ return Positioned(
+ right: 4.0 + offset.dx,
+ bottom: 4.0 + offset.dy,
+ child: Transform.rotate(
+ angle: (1.0 - progress.value) * math.pi / 2,
+ child: child!,
+ ),
+ );
+ },
+ child: FadeTransition(
+ opacity: progress,
+ child: child,
+ ),
+ );
+ }
+}
+
+@immutable
+class ActionButton extends StatelessWidget {
+ const ActionButton({
+ super.key,
+ this.onPressed,
+ required this.icon,
+ });
+
+ final VoidCallback? onPressed;
+ final Widget icon;
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return Material(
+ shape: const CircleBorder(),
+ clipBehavior: Clip.antiAlias,
+ color: theme.colorScheme.secondary,
+ elevation: 4.0,
+ child: IconButton(
+ onPressed: onPressed,
+ icon: icon,
+ color: theme.colorScheme.onSecondary,
+ ),
+ );
+ }
+}
+
+@immutable
+class FakeItem extends StatelessWidget {
+ const FakeItem({
+ super.key,
+ required this.isBig,
+ });
+
+ final bool isBig;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
+ height: isBig ? 128.0 : 36.0,
+ decoration: BoxDecoration(
+ borderRadius: const BorderRadius.all(Radius.circular(8.0)),
+ color: Colors.grey.shade300,
+ ),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart
new file mode 100644
index 0000000000..1a68f38ead
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart
@@ -0,0 +1,166 @@
+import 'dart:convert';
+
+import 'package:example/expandable_floating_action_button.dart';
+import 'package:example/plugin/image_node_widget.dart';
+import 'package:flutter/material.dart';
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flutter/services.dart';
+
+void main() {
+ runApp(const MyApp());
+}
+
+class MyApp extends StatelessWidget {
+ const MyApp({Key? key}) : super(key: key);
+
+ // This widget is the root of your application.
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ // This is the theme of your application.
+ //
+ // Try running your application with "flutter run". You'll see the
+ // application has a blue toolbar. Then, without quitting the app, try
+ // changing the primarySwatch below to Colors.green and then invoke
+ // "hot reload" (press "r" in the console where you ran "flutter run",
+ // or simply save your changes to "hot reload" in a Flutter IDE).
+ // Notice that the counter didn't reset back to zero; the application
+ // is not restarted.
+ primarySwatch: Colors.blue,
+ ),
+ home: const MyHomePage(title: 'FlowyEditor Example'),
+ );
+ }
+}
+
+class MyHomePage extends StatefulWidget {
+ const MyHomePage({Key? key, required this.title}) : super(key: key);
+
+ // This widget is the home page of your application. It is stateful, meaning
+ // that it has a State object (defined below) that contains fields that affect
+ // how it looks.
+
+ // This class is the configuration for the state. It holds the values (in this
+ // case the title) provided by the parent (in this case the App widget) and
+ // used by the build method of the State. Fields in a Widget subclass are
+ // always marked "final".
+
+ final String title;
+
+ @override
+ State createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State {
+ late EditorState _editorState;
+ final editorKey = GlobalKey();
+ int page = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ // Here we take the value from the MyHomePage object that was created by
+ // the App.build method, and use it to set our appbar title.
+ title: Text(widget.title),
+ ),
+ body: _buildBody(),
+ floatingActionButton: ExpandableFab(
+ distance: 112.0,
+ children: [
+ ActionButton(
+ onPressed: () {
+ if (page == 0) return;
+ setState(() {
+ page = 0;
+ });
+ },
+ icon: const Icon(Icons.note_add),
+ ),
+ ActionButton(
+ onPressed: () {
+ if (page == 1) return;
+ setState(() {
+ page = 1;
+ });
+ },
+ icon: const Icon(Icons.text_fields),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildBody() {
+ if (page == 0) {
+ return _buildFlowyEditor();
+ } else if (page == 1) {
+ return _buildTextField();
+ }
+ return Container();
+ }
+
+ Widget _buildFlowyEditor() {
+ return FutureBuilder(
+ future: rootBundle.loadString('assets/example.json'),
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ } else {
+ final data = Map.from(json.decode(snapshot.data!));
+ final document = StateTree.fromJson(data);
+ _editorState = EditorState(
+ document: document,
+ );
+ return Container(
+ padding: const EdgeInsets.only(left: 20, right: 20),
+ child: FlowyEditor(
+ key: editorKey,
+ editorState: _editorState,
+ keyEventHandlers: const [],
+ customBuilders: {
+ 'image': ImageNodeBuilder(),
+ },
+ ),
+ // shortcuts: [
+ // // TODO: this won't work, just a example for now.
+ // {
+ // 'h1': (editorState, eventName) {
+ // debugPrint('shortcut => $eventName');
+ // final selectedNodes = editorState.selectedNodes;
+ // if (selectedNodes.isEmpty) {
+ // return;
+ // }
+ // final textNode = selectedNodes.first as TextNode;
+ // TransactionBuilder(editorState)
+ // ..formatText(textNode, 0, textNode.toRawString().length, {
+ // 'heading': 'h1',
+ // })
+ // ..commit();
+ // }
+ // },
+ // {
+ // 'bold': (editorState, eventName) =>
+ // debugPrint('shortcut => $eventName')
+ // },
+ // {
+ // 'underline': (editorState, eventName) =>
+ // debugPrint('shortcut => $eventName')
+ // },
+ // ],
+ );
+ }
+ },
+ );
+ }
+
+ Widget _buildTextField() {
+ return const Center(
+ child: TextField(),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart
new file mode 100644
index 0000000000..6a01fb6430
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart
@@ -0,0 +1,112 @@
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flutter/material.dart';
+
+/// 1. define your custom type in example.json
+/// For example I need to define an image plugin, then I define type equals
+/// "image", and add "image_src" into "attributes".
+/// {
+/// "type": "image",
+/// "attributes", { "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" }
+/// }
+/// 2. create a class extends [NodeWidgetBuilder]
+/// 3. override the function `Widget build(NodeWidgetContext context)`
+/// and return a widget to render. The returned widget should be
+/// a StatefulWidget and mixin with [Selectable].
+///
+/// 4. override the getter `nodeValidator`
+/// to verify the data structure in [Node].
+/// 5. register the plugin with `type` to `flowy_editor` in `main.dart`.
+/// 6. Congratulations!
+
+class ImageNodeBuilder extends NodeWidgetBuilder {
+ @override
+ Widget build(NodeWidgetContext context) {
+ return ImageNodeWidget(
+ key: context.node.key,
+ node: context.node,
+ editorState: context.editorState,
+ );
+ }
+
+ @override
+ NodeValidator get nodeValidator => ((node) {
+ return node.type == 'image';
+ });
+}
+
+class ImageNodeWidget extends StatefulWidget {
+ final Node node;
+ final EditorState editorState;
+
+ const ImageNodeWidget({
+ Key? key,
+ required this.node,
+ required this.editorState,
+ }) : super(key: key);
+
+ @override
+ State createState() => _ImageNodeWidgetState();
+}
+
+class _ImageNodeWidgetState extends State with Selectable {
+ Node get node => widget.node;
+ EditorState get editorState => widget.editorState;
+ String get src => widget.node.attributes['image_src'] as String;
+
+ @override
+ Position end() {
+ // TODO: implement end
+ throw UnimplementedError();
+ }
+
+ @override
+ Position start() {
+ // TODO: implement start
+ throw UnimplementedError();
+ }
+
+ @override
+ List getRectsInSelection(Selection selection) {
+ // TODO: implement getRectsInSelection
+ throw UnimplementedError();
+ }
+
+ @override
+ Selection getSelectionInRange(Offset start, Offset end) {
+ // TODO: implement getSelectionInRange
+ throw UnimplementedError();
+ }
+
+ @override
+ Offset localToGlobal(Offset offset) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Rect getCursorRectInPosition(Position position) {
+ // TODO: implement getCursorRectInPosition
+ throw UnimplementedError();
+ }
+
+ @override
+ Position getPositionInOffset(Offset start) {
+ // TODO: implement getPositionInOffset
+ throw UnimplementedError();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return _build(context);
+ }
+
+ Widget _build(BuildContext context) {
+ return Column(
+ children: [
+ Image.network(
+ src,
+ width: MediaQuery.of(context).size.width,
+ )
+ ],
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/linux/.gitignore
new file mode 100644
index 0000000000..d3896c9844
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/linux/CMakeLists.txt
new file mode 100644
index 0000000000..74c66dd446
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/CMakeLists.txt
@@ -0,0 +1,138 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.10)
+project(runner LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "example")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "com.example.example")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Load bundled libraries from the lib/ directory relative to the binary.
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Root filesystem for cross-building.
+if(FLUTTER_TARGET_PLATFORM_SYSROOT)
+ set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+endif()
+
+# Define build configuration options.
+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()
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_14)
+ target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+ target_compile_options(${TARGET} PRIVATE "$<$>:-O3>")
+ target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Define the application target. To change its name, change BINARY_NAME above,
+# not the value here, or `flutter run` will no longer work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+ "main.cc"
+ "my_application.cc"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
+
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+ PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+ file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+ " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+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)
+
+foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
+ install(FILES "${bundled_library}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endforeach(bundled_library)
+
+# 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.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+ install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000000..d5bd01648a
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/CMakeLists.txt
@@ -0,0 +1,88 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.10)
+
+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.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+ set(NEW_LIST "")
+ foreach(element ${${LIST_NAME}})
+ list(APPEND NEW_LIST "${PREFIX}${element}")
+ endforeach(element)
+ set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# 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/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "fl_basic_message_channel.h"
+ "fl_binary_codec.h"
+ "fl_binary_messenger.h"
+ "fl_dart_project.h"
+ "fl_engine.h"
+ "fl_json_message_codec.h"
+ "fl_json_method_codec.h"
+ "fl_message_codec.h"
+ "fl_method_call.h"
+ "fl_method_channel.h"
+ "fl_method_codec.h"
+ "fl_method_response.h"
+ "fl_plugin_registrar.h"
+ "fl_plugin_registry.h"
+ "fl_standard_message_codec.h"
+ "fl_standard_method_codec.h"
+ "fl_string_codec.h"
+ "fl_value.h"
+ "fl_view.h"
+ "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+ PkgConfig::GTK
+ PkgConfig::GLIB
+ PkgConfig::GIO
+)
+add_dependencies(flutter 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.
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+ ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000000..f6f23bfe97
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
+ url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.h b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000000..e0f0a47bc0
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000000..f16b4c3421
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,24 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+ url_launcher_linux
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux 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)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/main.cc b/frontend/app_flowy/packages/flowy_editor/example/linux/main.cc
new file mode 100644
index 0000000000..e7c5c54370
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/main.cc
@@ -0,0 +1,6 @@
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+ g_autoptr(MyApplication) app = my_application_new();
+ return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.cc b/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.cc
new file mode 100644
index 0000000000..0ba8f43096
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.cc
@@ -0,0 +1,104 @@
+#include "my_application.h"
+
+#include
+#ifdef GDK_WINDOWING_X11
+#include
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+ GtkApplication parent_instance;
+ char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+ MyApplication* self = MY_APPLICATION(application);
+ GtkWindow* window =
+ GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+ // Use a header bar when running in GNOME as this is the common style used
+ // by applications and is the setup most users will be using (e.g. Ubuntu
+ // desktop).
+ // If running on X and not using GNOME then just use a traditional title bar
+ // in case the window manager does more exotic layout, e.g. tiling.
+ // If running on Wayland assume the header bar will work (may need changing
+ // if future cases occur).
+ gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+ GdkScreen* screen = gtk_window_get_screen(window);
+ if (GDK_IS_X11_SCREEN(screen)) {
+ const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+ if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+ use_header_bar = FALSE;
+ }
+ }
+#endif
+ if (use_header_bar) {
+ GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+ gtk_widget_show(GTK_WIDGET(header_bar));
+ gtk_header_bar_set_title(header_bar, "example");
+ gtk_header_bar_set_show_close_button(header_bar, TRUE);
+ gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+ } else {
+ gtk_window_set_title(window, "example");
+ }
+
+ gtk_window_set_default_size(window, 1280, 720);
+ gtk_widget_show(GTK_WIDGET(window));
+
+ g_autoptr(FlDartProject) project = fl_dart_project_new();
+ fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
+
+ FlView* view = fl_view_new(project);
+ gtk_widget_show(GTK_WIDGET(view));
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+ fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+ gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
+ MyApplication* self = MY_APPLICATION(application);
+ // Strip out the first argument as it is the binary name.
+ self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+ g_autoptr(GError) error = nullptr;
+ if (!g_application_register(application, nullptr, &error)) {
+ g_warning("Failed to register: %s", error->message);
+ *exit_status = 1;
+ return TRUE;
+ }
+
+ g_application_activate(application);
+ *exit_status = 0;
+
+ return TRUE;
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+ MyApplication* self = MY_APPLICATION(object);
+ g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+ G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+ G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+ G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
+ G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+ return MY_APPLICATION(g_object_new(my_application_get_type(),
+ "application-id", APPLICATION_ID,
+ "flags", G_APPLICATION_NON_UNIQUE,
+ nullptr));
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.h b/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.h
new file mode 100644
index 0000000000..72271d5e41
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/linux/my_application.h
@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+ GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif // FLUTTER_MY_APPLICATION_H_
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/macos/.gitignore
new file mode 100644
index 0000000000..746adbb6b9
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/.gitignore
@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000000..4b81f9b2d2
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000000..5caa9d1579
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000000..8236f5728c
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,12 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import url_launcher_macos
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile
new file mode 100644
index 0000000000..dade8dfad0
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock
new file mode 100644
index 0000000000..4f162e68af
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock
@@ -0,0 +1,22 @@
+PODS:
+ - FlutterMacOS (1.0.0)
+ - url_launcher_macos (0.0.1):
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - FlutterMacOS (from `Flutter/ephemeral`)
+ - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
+
+EXTERNAL SOURCES:
+ FlutterMacOS:
+ :path: Flutter/ephemeral
+ url_launcher_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
+
+SPEC CHECKSUMS:
+ FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+ url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
+
+PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
+
+COCOAPODS: 1.11.3
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..057a1a8224
--- /dev/null
+++ b/frontend/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 */
+ 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 */; };
+ 8FD791997F0D60CE136153FB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F21284F13DB2F7E10C6EB1F7 /* Pods_Runner.framework */; };
+/* 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 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 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 = ""; };
+ 4C1351C0AA74138239028404 /* 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 = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+ BBAF6135AB8D71FE6D8B315C /* 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 = ""; };
+ BE3A038D8FDF07F3AD1C02FB /* 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 = ""; };
+ F21284F13DB2F7E10C6EB1F7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8FD791997F0D60CE136153FB /* 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 */,
+ 7B5E3B15415D0C17244EF9E7 /* Pods */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* 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 = "";
+ };
+ 7B5E3B15415D0C17244EF9E7 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ BBAF6135AB8D71FE6D8B315C /* Pods-Runner.debug.xcconfig */,
+ 4C1351C0AA74138239028404 /* Pods-Runner.release.xcconfig */,
+ BE3A038D8FDF07F3AD1C02FB /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ F21284F13DB2F7E10C6EB1F7 /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 87BB3D0057F20B3618A17B82 /* [CP] Check Pods Manifest.lock */,
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ 09CDF3F9864A27F94DEE8EC6 /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* example.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 1300;
+ 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 */
+ 09CDF3F9864A27F94DEE8EC6 /* [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;
+ };
+ 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";
+ };
+ 87BB3D0057F20B3618A17B82 /* [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;
+ };
+/* 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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000000..fb7259e177
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..21a3cc14c7
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000000..d53ef64377
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..a2ec33f19f
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Base.lproj/MainMenu.xib b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000000..80e867a4e0
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,343 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 0000000000..8b42559e87
--- /dev/null
+++ b/frontend/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 = example
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.example.example
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved.
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 0000000000..36b0fd9464
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 0000000000..dff4f49561
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 0000000000..42bcbf4780
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 0000000000..c946719a1a
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.network.server
+
+ com.apple.security.network.client
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Info.plist
new file mode 100644
index 0000000000..4789daa6a4
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 0000000000..2722837ec9
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements
new file mode 100644
index 0000000000..48271acc95
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Runner/Release.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock
new file mode 100644
index 0000000000..cfadcb8242
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock
@@ -0,0 +1,299 @@
+# 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.8.2"
+ 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.2.0"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.1"
+ 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.16.0"
+ cupertino_icons:
+ dependency: "direct main"
+ description:
+ name: cupertino_icons
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.5"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.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_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
+ flutter_svg:
+ dependency: transitive
+ description:
+ name: flutter_svg
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.1+1"
+ 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.4"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.11"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.4"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.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.1"
+ path_drawing:
+ dependency: transitive
+ description:
+ name: path_drawing
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ path_parsing:
+ dependency: transitive
+ description:
+ name: path_parsing
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.0.0"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+ provider:
+ dependency: "direct main"
+ description:
+ name: provider
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.0.3"
+ 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.2"
+ 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"
+ 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.4.9"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.1.5"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.0.17"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.0.17"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.12"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.1.0"
+sdks:
+ dart: ">=2.17.0 <3.0.0"
+ flutter: ">=2.11.0-0.1.pre"
diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml
new file mode 100644
index 0000000000..9a80a73a0a
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml
@@ -0,0 +1,94 @@
+name: example
+description: A new Flutter project.
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.17.0 <3.0.0"
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+ flutter:
+ sdk: flutter
+
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^1.0.2
+ flowy_editor:
+ path: ../
+ provider: ^6.0.3
+ url_launcher: ^6.1.5
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ # The "flutter_lints" package below contains a set of recommended lints to
+ # encourage good coding practices. The lint set provided by the package is
+ # activated in the `analysis_options.yaml` file located at the root of your
+ # package. See that file for information about deactivating specific lint
+ # rules and activating additional ones.
+ flutter_lints: ^2.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+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:
+ - document.json
+ - example.json
+ # - 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/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart b/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart
new file mode 100644
index 0000000000..092d222f7e
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart
@@ -0,0 +1,30 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:example/main.dart';
+
+void main() {
+ testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const MyApp());
+
+ // Verify that our counter starts at 0.
+ expect(find.text('0'), findsOneWidget);
+ expect(find.text('1'), findsNothing);
+
+ // Tap the '+' icon and trigger a frame.
+ await tester.tap(find.byIcon(Icons.add));
+ await tester.pump();
+
+ // Verify that our counter has incremented.
+ expect(find.text('0'), findsNothing);
+ expect(find.text('1'), findsOneWidget);
+ });
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/favicon.png b/frontend/app_flowy/packages/flowy_editor/example/web/favicon.png
new file mode 100644
index 0000000000..8aaa46ac1a
Binary files /dev/null and b/frontend/app_flowy/packages/flowy_editor/example/web/favicon.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png
new file mode 100644
index 0000000000..b749bfef07
Binary files /dev/null and b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-192.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png
new file mode 100644
index 0000000000..88cfd48dff
Binary files /dev/null and b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-512.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-192.png b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000000..eb9b4d76e5
Binary files /dev/null and b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-192.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-512.png b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000000..d69c56691f
Binary files /dev/null and b/frontend/app_flowy/packages/flowy_editor/example/web/icons/Icon-maskable-512.png differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/index.html b/frontend/app_flowy/packages/flowy_editor/example/web/index.html
new file mode 100644
index 0000000000..41b3bc336f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/web/index.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ example
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/web/manifest.json b/frontend/app_flowy/packages/flowy_editor/example/web/manifest.json
new file mode 100644
index 0000000000..096edf8fe4
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/web/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "example",
+ "short_name": "example",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/.gitignore b/frontend/app_flowy/packages/flowy_editor/example/windows/.gitignore
new file mode 100644
index 0000000000..d492d0d98c
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt
new file mode 100644
index 0000000000..c0270746b1
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/CMakeLists.txt
@@ -0,0 +1,101 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.14)
+project(example LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "example")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Define build configuration option.
+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()
+# Define settings for the Profile build mode.
+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.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+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()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build; see runner/CMakeLists.txt.
+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/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt
new file mode 100644
index 0000000000..930d2071a3
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/CMakeLists.txt
@@ -0,0 +1,104 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.14)
+
+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/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000000..4f7884874d
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,14 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+ UrlLauncherWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("UrlLauncherWindows"));
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000000..dc139d85a9
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake
new file mode 100644
index 0000000000..88b22e5c77
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/flutter/generated_plugins.cmake
@@ -0,0 +1,24 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+ url_launcher_windows
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+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)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt
new file mode 100644
index 0000000000..b9e550fba8
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/CMakeLists.txt
@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/Runner.rc
new file mode 100644
index 0000000000..5fdea291cf
--- /dev/null
+++ b/frontend/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.example" "\0"
+ VALUE "FileDescription", "example" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "example" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0"
+ VALUE "OriginalFilename", "example.exe" "\0"
+ VALUE "ProductName", "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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp
new file mode 100644
index 0000000000..b43b9095ea
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.cpp
@@ -0,0 +1,61 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : 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());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ 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 opportunity 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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h
new file mode 100644
index 0000000000..6da0652f05
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(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 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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp
new file mode 100644
index 0000000000..bcb57b0e2a
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/main.cpp
@@ -0,0 +1,43 @@
+#include
+#include
+#include
+
+#include "flutter_window.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);
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.CreateAndShow(L"example", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resource.h b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resource.h
new file mode 100644
index 0000000000..66a65d1e4a
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resources/app_icon.ico b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/resources/app_icon.ico differ
diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest
new file mode 100644
index 0000000000..c977c4a425
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/runner.exe.manifest
@@ -0,0 +1,20 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.cpp
new file mode 100644
index 0000000000..f5bf9fa0f5
--- /dev/null
+++ b/frontend/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);
+ std::string utf8_string;
+ if (target_length == 0 || target_length > utf8_string.max_size()) {
+ return 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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.h b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/utils.h
new file mode 100644
index 0000000000..3879d54755
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.cpp
new file mode 100644
index 0000000000..c10f08dc7d
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h b/frontend/app_flowy/packages/flowy_editor/example/windows/runner/win32_window.h
new file mode 100644
index 0000000000..17ba431125
--- /dev/null
+++ b/frontend/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/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart
new file mode 100644
index 0000000000..4e1f39775f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart
@@ -0,0 +1,42 @@
+typedef Attributes = Map;
+
+int hashAttributes(Attributes attributes) {
+ return Object.hashAllUnordered(
+ attributes.entries.map((e) => Object.hash(e.key, e.value)));
+}
+
+Attributes invertAttributes(Attributes? attr, Attributes? base) {
+ attr ??= {};
+ base ??= {};
+ final Attributes baseInverted = base.keys.fold({}, (memo, key) {
+ if (base![key] != attr![key] && attr.containsKey(key)) {
+ memo[key] = base[key];
+ }
+ return memo;
+ });
+ return attr.keys.fold(baseInverted, (memo, key) {
+ if (attr![key] != base![key] && base.containsKey(key)) {
+ memo[key] = null;
+ }
+ return memo;
+ });
+}
+
+Attributes? composeAttributes(Attributes? a, Attributes? b) {
+ a ??= {};
+ b ??= {};
+ final Attributes attributes = {};
+ attributes.addAll(Map.from(b)..removeWhere((_, value) => value == null));
+
+ for (final entry in a.entries) {
+ if (!b.containsKey(entry.key)) {
+ attributes[entry.key] = entry.value;
+ }
+ }
+
+ if (attributes.isEmpty) {
+ return null;
+ }
+
+ return attributes;
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart
new file mode 100644
index 0000000000..3a7ad36456
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart
@@ -0,0 +1,224 @@
+import 'dart:collection';
+import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/text_delta.dart';
+import 'package:flowy_editor/operation/operation.dart';
+import 'package:flutter/material.dart';
+import './attributes.dart';
+
+class Node extends ChangeNotifier with LinkedListEntry {
+ Node? parent;
+ final String type;
+ final LinkedList children;
+ final Attributes attributes;
+
+ GlobalKey? key;
+ // TODO: abstract a selectable node??
+ final layerLink = LayerLink();
+
+ String? get subtype {
+ // TODO: make 'subtype' as a const value.
+ if (attributes.containsKey('subtype')) {
+ assert(attributes['subtype'] is String?,
+ 'subtype must be a [String] or [null]');
+ return attributes['subtype'] as String?;
+ }
+ return null;
+ }
+
+ Path get path => _path();
+
+ Node({
+ required this.type,
+ required this.children,
+ required this.attributes,
+ this.parent,
+ }) {
+ for (final child in children) {
+ child.parent = this;
+ }
+ }
+
+ factory Node.fromJson(Map json) {
+ assert(json['type'] is String);
+
+ // TODO: check the type that not exist on plugins.
+ final jType = json['type'] as String;
+ final jChildren = json['children'] as List?;
+ final jAttributes = json['attributes'] != null
+ ? Attributes.from(json['attributes'] as Map)
+ : Attributes.from({});
+
+ final LinkedList children = LinkedList();
+ if (jChildren != null) {
+ children.addAll(
+ jChildren.map(
+ (jChild) => Node.fromJson(
+ Map.from(jChild),
+ ),
+ ),
+ );
+ }
+
+ Node node;
+
+ if (jType == "text") {
+ final jDelta = json['delta'] as List?;
+ final delta = jDelta == null ? Delta() : Delta.fromJson(jDelta);
+ node = TextNode(
+ type: jType,
+ children: children,
+ attributes: jAttributes,
+ delta: delta);
+ } else {
+ node = Node(
+ type: jType,
+ children: children,
+ attributes: jAttributes,
+ );
+ }
+
+ for (final child in children) {
+ child.parent = node;
+ }
+
+ return node;
+ }
+
+ void updateAttributes(Attributes attributes) {
+ bool shouldNotifyParent =
+ this.attributes['subtype'] != attributes['subtype'];
+
+ for (final attribute in attributes.entries) {
+ if (attribute.value == null) {
+ this.attributes.remove(attribute.key);
+ } else {
+ this.attributes[attribute.key] = attribute.value;
+ }
+ }
+ // Notify the new attributes
+ // if attributes contains 'subtype', should notify parent to rebuild node
+ // else, just notify current node.
+ shouldNotifyParent ? parent?.notifyListeners() : notifyListeners();
+ }
+
+ Node? childAtIndex(int index) {
+ if (children.length <= index) {
+ return null;
+ }
+
+ return children.elementAt(index);
+ }
+
+ Node? childAtPath(Path path) {
+ if (path.isEmpty) {
+ return this;
+ }
+
+ return childAtIndex(path.first)?.childAtPath(path.sublist(1));
+ }
+
+ @override
+ void insertAfter(Node entry) {
+ entry.parent = parent;
+ super.insertAfter(entry);
+
+ // Notify the new node.
+ parent?.notifyListeners();
+ }
+
+ @override
+ void insertBefore(Node entry) {
+ entry.parent = parent;
+ super.insertBefore(entry);
+
+ // Notify the new node.
+ parent?.notifyListeners();
+ }
+
+ @override
+ void unlink() {
+ super.unlink();
+
+ parent?.notifyListeners();
+ parent = null;
+ }
+
+ Map toJson() {
+ var map = {
+ 'type': type,
+ };
+ if (children.isNotEmpty) {
+ map['children'] = children.map((node) => node.toJson());
+ }
+ if (attributes.isNotEmpty) {
+ map['attributes'] = attributes;
+ }
+ return map;
+ }
+
+ Path _path([Path previous = const []]) {
+ if (parent == null) {
+ return previous;
+ }
+ var index = 0;
+ for (var child in parent!.children) {
+ if (child == this) {
+ break;
+ }
+ index += 1;
+ }
+ return parent!._path([index, ...previous]);
+ }
+}
+
+class TextNode extends Node {
+ Delta _delta;
+
+ TextNode({
+ required super.type,
+ required super.children,
+ required super.attributes,
+ required Delta delta,
+ }) : _delta = delta;
+
+ TextNode.empty()
+ : _delta = Delta([TextInsert(' ')]),
+ super(
+ type: 'text',
+ children: LinkedList(),
+ attributes: {},
+ );
+
+ Delta get delta {
+ return _delta;
+ }
+
+ set delta(Delta v) {
+ _delta = v;
+ notifyListeners();
+ }
+
+ @override
+ Map toJson() {
+ final map = super.toJson();
+ map['delta'] = _delta.toJson();
+ return map;
+ }
+
+ TextNode copyWith({
+ String? type,
+ LinkedList? children,
+ Attributes? attributes,
+ Delta? delta,
+ }) =>
+ TextNode(
+ type: type ?? this.type,
+ children: children ?? this.children,
+ attributes: attributes ?? this.attributes,
+ delta: delta ?? this.delta,
+ );
+
+ // TODO: It's unneccesry to compute everytime.
+ String toRawString() =>
+ _delta.operations.whereType().map((op) => op.content).join();
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart
new file mode 100644
index 0000000000..8f24947649
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart
@@ -0,0 +1,9 @@
+import 'dart:math';
+
+import 'package:flutter/foundation.dart';
+
+typedef Path = List;
+
+bool pathEquals(Path path1, Path path2) {
+ return listEquals(path1, path2);
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart
new file mode 100644
index 0000000000..a87064d85a
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/material.dart';
+
+import './path.dart';
+
+class Position {
+ final Path path;
+ final int offset;
+
+ Position({
+ required this.path,
+ this.offset = 0,
+ });
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! Position) {
+ return false;
+ }
+ return pathEquals(path, other.path) && offset == other.offset;
+ }
+
+ @override
+ int get hashCode {
+ final pathHash = hashList(path);
+ return Object.hash(pathHash, offset);
+ }
+
+ Position copyWith({Path? path, int? offset}) {
+ return Position(
+ path: path ?? this.path,
+ offset: offset ?? this.offset,
+ );
+ }
+
+ @override
+ String toString() => 'path = $path, offset = $offset';
+
+ Map toJson() {
+ return {
+ "path": path.toList(),
+ "offset": offset,
+ };
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart
new file mode 100644
index 0000000000..f1fa0682f6
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart
@@ -0,0 +1,58 @@
+import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/extensions/path_extensions.dart';
+
+class Selection {
+ final Position start;
+ final Position end;
+
+ Selection({
+ required this.start,
+ required this.end,
+ });
+
+ Selection.single({
+ required Path path,
+ required int startOffset,
+ int? endOffset,
+ }) : start = Position(path: path, offset: startOffset),
+ end = Position(path: path, offset: endOffset ?? startOffset);
+
+ Selection.collapsed(Position position)
+ : start = position,
+ end = position;
+
+ Selection collapse({bool atStart = false}) {
+ if (atStart) {
+ return Selection(start: start, end: start);
+ } else {
+ return Selection(start: end, end: end);
+ }
+ }
+
+ bool get isCollapsed => start == end;
+ bool get isSingle => pathEquals(start.path, end.path);
+ bool get isUpward =>
+ start.path >= end.path && !pathEquals(start.path, end.path);
+ bool get isDownward =>
+ start.path <= end.path && !pathEquals(start.path, end.path);
+
+ Selection copyWith({Position? start, Position? end}) {
+ return Selection(
+ start: start ?? this.start,
+ end: end ?? this.end,
+ );
+ }
+
+ Selection copy() => Selection(start: start, end: end);
+
+ @override
+ String toString() => '[Selection] start = $start, end = $end';
+
+ Map toJson() {
+ return {
+ "start": start.toJson(),
+ "end": end.toJson(),
+ };
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart
new file mode 100644
index 0000000000..cf49f48ac8
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart
@@ -0,0 +1,80 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/text_delta.dart';
+import './attributes.dart';
+
+class StateTree {
+ final Node root;
+
+ StateTree({
+ required this.root,
+ });
+
+ factory StateTree.fromJson(Attributes json) {
+ assert(json['document'] is Map);
+
+ final document = Map.from(json['document'] as Map);
+ final root = Node.fromJson(document);
+ return StateTree(root: root);
+ }
+
+ Node? nodeAtPath(Path path) {
+ return root.childAtPath(path);
+ }
+
+ bool insert(Path path, List nodes) {
+ if (path.isEmpty) {
+ return false;
+ }
+ Node? insertedNode = root.childAtPath(
+ path.sublist(0, path.length - 1) + [path.last - 1],
+ );
+ if (insertedNode == null) {
+ return false;
+ }
+ for (var i = 0; i < nodes.length; i++) {
+ final node = nodes[i];
+ insertedNode!.insertAfter(node);
+ insertedNode = node;
+ }
+ return true;
+ }
+
+ bool textEdit(Path path, Delta delta) {
+ if (path.isEmpty) {
+ return false;
+ }
+ final node = root.childAtPath(path);
+ if (node == null || node is! TextNode) {
+ return false;
+ }
+ node.delta = node.delta.compose(delta);
+ return false;
+ }
+
+ delete(Path path, [int length = 1]) {
+ if (path.isEmpty) {
+ return null;
+ }
+ var deletedNode = root.childAtPath(path);
+ while (deletedNode != null && length > 0) {
+ final next = deletedNode.next;
+ deletedNode.unlink();
+ length--;
+ deletedNode = next;
+ }
+ }
+
+ Attributes? update(Path path, Attributes attributes) {
+ if (path.isEmpty) {
+ return null;
+ }
+ final updatedNode = root.childAtPath(path);
+ if (updatedNode == null) {
+ return null;
+ }
+ final previousAttributes = Attributes.from(updatedNode.attributes);
+ updatedNode.updateAttributes(attributes);
+ return previousAttributes;
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart
new file mode 100644
index 0000000000..64335d4a05
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart
@@ -0,0 +1,483 @@
+import 'dart:collection';
+import 'dart:math';
+
+import 'package:flowy_editor/document/attributes.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import './attributes.dart';
+
+// constant number: 2^53 - 1
+const int _maxInt = 9007199254740991;
+
+abstract class TextOperation {
+ bool get isEmpty => length == 0;
+
+ int get length;
+
+ Attributes? get attributes => null;
+
+ Map toJson();
+}
+
+class TextInsert extends TextOperation {
+ String content;
+ final Attributes? _attributes;
+
+ TextInsert(this.content, [Attributes? attrs]) : _attributes = attrs;
+
+ @override
+ int get length {
+ return content.length;
+ }
+
+ @override
+ Attributes? get attributes {
+ return _attributes;
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! TextInsert) {
+ return false;
+ }
+ return content == other.content &&
+ mapEquals(_attributes, other._attributes);
+ }
+
+ @override
+ int get hashCode {
+ final contentHash = content.hashCode;
+ final attrs = _attributes;
+ return Object.hash(
+ contentHash, attrs == null ? null : hashAttributes(attrs));
+ }
+
+ @override
+ Map toJson() {
+ final result = {
+ 'insert': content,
+ };
+ final attrs = _attributes;
+ if (attrs != null) {
+ result['attributes'] = {...attrs};
+ }
+ return result;
+ }
+}
+
+class TextRetain extends TextOperation {
+ int _length;
+ final Attributes? _attributes;
+
+ TextRetain(length, [Attributes? attributes])
+ : _length = length,
+ _attributes = attributes;
+
+ @override
+ bool get isEmpty {
+ return length == 0;
+ }
+
+ @override
+ int get length {
+ return _length;
+ }
+
+ set length(int v) {
+ _length = v;
+ }
+
+ @override
+ Attributes? get attributes {
+ return _attributes;
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! TextRetain) {
+ return false;
+ }
+ return _length == other.length && mapEquals(_attributes, other._attributes);
+ }
+
+ @override
+ int get hashCode {
+ final attrs = _attributes;
+ return Object.hash(_length, attrs == null ? null : hashAttributes(attrs));
+ }
+
+ @override
+ Map toJson() {
+ final result = {
+ 'retain': _length,
+ };
+ final attrs = _attributes;
+ if (attrs != null) {
+ result['attributes'] = {...attrs};
+ }
+ return result;
+ }
+}
+
+class TextDelete extends TextOperation {
+ int _length;
+
+ TextDelete(int length) : _length = length;
+
+ @override
+ int get length {
+ return _length;
+ }
+
+ set length(int v) {
+ _length = v;
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! TextDelete) {
+ return false;
+ }
+ return _length == other.length;
+ }
+
+ @override
+ int get hashCode {
+ return _length.hashCode;
+ }
+
+ @override
+ Map toJson() {
+ return {
+ 'delete': _length,
+ };
+ }
+}
+
+class _OpIterator {
+ final UnmodifiableListView _operations;
+ int _index = 0;
+ int _offset = 0;
+
+ _OpIterator(List operations)
+ : _operations = UnmodifiableListView(operations);
+
+ bool get hasNext {
+ return peekLength() < _maxInt;
+ }
+
+ TextOperation? peek() {
+ if (_index >= _operations.length) {
+ return null;
+ }
+
+ return _operations[_index];
+ }
+
+ int peekLength() {
+ if (_index < _operations.length) {
+ final op = _operations[_index];
+ return op.length - _offset;
+ }
+ return _maxInt;
+ }
+
+ TextOperation next([int? length]) {
+ length ??= _maxInt;
+
+ if (_index >= _operations.length) {
+ return TextRetain(_maxInt);
+ }
+
+ final nextOp = _operations[_index];
+
+ final offset = _offset;
+ final opLength = nextOp.length;
+ if (length >= opLength - offset) {
+ length = opLength - offset;
+ _index += 1;
+ _offset = 0;
+ } else {
+ _offset += length;
+ }
+ if (nextOp is TextDelete) {
+ return TextDelete(length);
+ }
+
+ if (nextOp is TextRetain) {
+ return TextRetain(
+ length,
+ nextOp.attributes,
+ );
+ }
+
+ if (nextOp is TextInsert) {
+ return TextInsert(
+ nextOp.content.substring(offset, offset + length),
+ nextOp.attributes,
+ );
+ }
+
+ return TextRetain(_maxInt);
+ }
+
+ List rest() {
+ if (!hasNext) {
+ return [];
+ } else if (_offset == 0) {
+ return _operations.sublist(_index);
+ } else {
+ final offset = _offset;
+ final index = _index;
+ final _next = next();
+ final rest = _operations.sublist(_index);
+ _offset = offset;
+ _index = index;
+ return [_next] + rest;
+ }
+ }
+}
+
+TextOperation? _textOperationFromJson(Map json) {
+ TextOperation? result;
+
+ if (json['insert'] is String) {
+ final attrs = json['attributes'] as Map?;
+ result =
+ TextInsert(json['insert'] as String, attrs == null ? null : {...attrs});
+ } else if (json['retain'] is int) {
+ final attrs = json['attributes'] as Map?;
+ result =
+ TextRetain(json['retain'] as int, attrs == null ? null : {...attrs});
+ } else if (json['delete'] is int) {
+ result = TextDelete(json['delete'] as int);
+ }
+
+ return result;
+}
+
+// basically copy from: https://github.com/quilljs/delta
+class Delta {
+ final List operations;
+
+ factory Delta.fromJson(List list) {
+ final operations = [];
+
+ for (final obj in list) {
+ final op = _textOperationFromJson(obj as Map);
+ if (op != null) {
+ operations.add(op);
+ }
+ }
+
+ return Delta(operations);
+ }
+
+ Delta([List? ops]) : operations = ops ?? [];
+
+ Delta addAll(List textOps) {
+ textOps.forEach(add);
+ return this;
+ }
+
+ Delta add(TextOperation textOp) {
+ if (textOp.isEmpty) {
+ return this;
+ }
+
+ if (operations.isNotEmpty) {
+ final lastOp = operations.last;
+ if (lastOp is TextDelete && textOp is TextDelete) {
+ lastOp.length += textOp.length;
+ return this;
+ }
+ if (mapEquals(lastOp.attributes, textOp.attributes)) {
+ if (lastOp is TextInsert && textOp is TextInsert) {
+ lastOp.content += textOp.content;
+ return this;
+ }
+ // if there is an delete before the insert
+ // swap the order
+ if (lastOp is TextDelete && textOp is TextInsert) {
+ operations.removeLast();
+ operations.add(textOp);
+ operations.add(lastOp);
+ return this;
+ }
+ if (lastOp is TextRetain && textOp is TextRetain) {
+ lastOp.length += textOp.length;
+ return this;
+ }
+ }
+ }
+
+ operations.add(textOp);
+ return this;
+ }
+
+ Delta slice(int start, [int? end]) {
+ final result = Delta();
+ final iterator = _OpIterator(operations);
+ int index = 0;
+
+ while ((end == null || index < end) && iterator.hasNext) {
+ TextOperation? nextOp;
+ if (index < start) {
+ nextOp = iterator.next(start - index);
+ } else {
+ nextOp = iterator.next(end == null ? null : end - index);
+ result.add(nextOp);
+ }
+
+ index += nextOp.length;
+ }
+
+ return result;
+ }
+
+ Delta insert(String content, [Attributes? attributes]) {
+ final op = TextInsert(content, attributes);
+ return add(op);
+ }
+
+ Delta retain(int length, [Attributes? attributes]) {
+ final op = TextRetain(length, attributes);
+ return add(op);
+ }
+
+ Delta delete(int length) {
+ final op = TextDelete(length);
+ return add(op);
+ }
+
+ int get length {
+ return operations.fold(
+ 0, (previousValue, element) => previousValue + element.length);
+ }
+
+ Delta compose(Delta other) {
+ final thisIter = _OpIterator(operations);
+ final otherIter = _OpIterator(other.operations);
+ final ops = [];
+
+ final firstOther = otherIter.peek();
+ if (firstOther != null &&
+ firstOther is TextRetain &&
+ firstOther.attributes == null) {
+ int firstLeft = firstOther.length;
+ while (
+ thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) {
+ firstLeft -= thisIter.peekLength();
+ final next = thisIter.next();
+ ops.add(next);
+ }
+ if (firstOther.length - firstLeft > 0) {
+ otherIter.next(firstOther.length - firstLeft);
+ }
+ }
+
+ final delta = Delta(ops);
+ while (thisIter.hasNext || otherIter.hasNext) {
+ if (otherIter.peek() is TextInsert) {
+ final next = otherIter.next();
+ delta.add(next);
+ } else if (thisIter.peek() is TextDelete) {
+ final next = thisIter.next();
+ delta.add(next);
+ } else {
+ // otherIs
+ final length = min(thisIter.peekLength(), otherIter.peekLength());
+ final thisOp = thisIter.next(length);
+ final otherOp = otherIter.next(length);
+ final attributes =
+ composeAttributes(thisOp.attributes, otherOp.attributes);
+ if (otherOp is TextRetain && otherOp.length > 0) {
+ TextOperation? newOp;
+ if (thisOp is TextRetain) {
+ newOp = TextRetain(length, attributes);
+ } else if (thisOp is TextInsert) {
+ newOp = TextInsert(thisOp.content, attributes);
+ }
+
+ if (newOp != null) {
+ delta.add(newOp);
+ }
+
+ // Optimization if rest of other is just retain
+ if (!otherIter.hasNext &&
+ delta.operations[delta.operations.length - 1] == newOp) {
+ final rest = Delta(thisIter.rest());
+ return delta.concat(rest).chop();
+ }
+ } else if (otherOp is TextDelete && (thisOp is TextRetain)) {
+ delta.add(otherOp);
+ }
+ }
+ }
+
+ return delta.chop();
+ }
+
+ Delta concat(Delta other) {
+ var ops = [...operations];
+ if (other.operations.isNotEmpty) {
+ ops.add(other.operations[0]);
+ ops.addAll(other.operations.sublist(1));
+ }
+ return Delta(ops);
+ }
+
+ Delta chop() {
+ if (operations.isEmpty) {
+ return this;
+ }
+ final lastOp = operations.last;
+ if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) {
+ operations.removeLast();
+ }
+ return this;
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! Delta) {
+ return false;
+ }
+ return listEquals(operations, other.operations);
+ }
+
+ @override
+ int get hashCode {
+ return hashList(operations);
+ }
+
+ Delta invert(Delta base) {
+ final inverted = Delta();
+ operations.fold(0, (int previousValue, op) {
+ if (op is TextInsert) {
+ inverted.delete(op.length);
+ } else if (op is TextRetain && op.attributes == null) {
+ inverted.retain(op.length);
+ return previousValue + op.length;
+ } else if (op is TextDelete || op is TextRetain) {
+ final length = op.length;
+ final slice = base.slice(previousValue, previousValue + length);
+ for (final baseOp in slice.operations) {
+ if (op is TextDelete) {
+ inverted.add(baseOp);
+ } else if (op is TextRetain && op.attributes != null) {
+ inverted.retain(baseOp.length,
+ invertAttributes(op.attributes, baseOp.attributes));
+ }
+ }
+ return previousValue + length;
+ }
+ return previousValue;
+ });
+ return inverted.chop();
+ }
+
+ List toJson() {
+ return operations.map((e) => e.toJson()).toList();
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart
new file mode 100644
index 0000000000..92a05fc880
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart
@@ -0,0 +1,111 @@
+import 'dart:async';
+import 'package:flowy_editor/service/service.dart';
+import 'package:flutter/material.dart';
+
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/document/state_tree.dart';
+import 'package:flowy_editor/operation/operation.dart';
+import 'package:flowy_editor/operation/transaction.dart';
+import 'package:flowy_editor/undo_manager.dart';
+
+class ApplyOptions {
+ /// This flag indicates that
+ /// whether the transaction should be recorded into
+ /// the undo stack.
+ final bool recordUndo;
+ final bool recordRedo;
+ const ApplyOptions({
+ this.recordUndo = true,
+ this.recordRedo = false,
+ });
+}
+
+class EditorState {
+ final StateTree document;
+
+ List selectedNodes = [];
+
+ // Service reference.
+ final service = FlowyService();
+
+ final UndoManager undoManager = UndoManager();
+ Selection? _cursorSelection;
+
+ Selection? get cursorSelection {
+ return _cursorSelection;
+ }
+
+ /// add the set reason in the future, don't use setter
+ updateCursorSelection(Selection? cursorSelection) {
+ // broadcast to other users here
+ if (cursorSelection == null) {
+ service.selectionService.clearSelection();
+ } else {
+ service.selectionService.updateSelection(cursorSelection);
+ }
+ _cursorSelection = cursorSelection;
+ }
+
+ Timer? _debouncedSealHistoryItemTimer;
+
+ EditorState({
+ required this.document,
+ }) {
+ undoManager.state = this;
+ }
+
+ apply(Transaction transaction,
+ [ApplyOptions options = const ApplyOptions()]) {
+ for (final op in transaction.operations) {
+ _applyOperation(op);
+ }
+ // updateCursorSelection(transaction.afterSelection);
+
+ // FIXME: don't use delay
+ Future.delayed(const Duration(milliseconds: 16), () {
+ updateCursorSelection(transaction.afterSelection);
+ });
+
+ if (options.recordUndo) {
+ final undoItem = undoManager.getUndoHistoryItem();
+ undoItem.addAll(transaction.operations);
+ if (undoItem.beforeSelection == null &&
+ transaction.beforeSelection != null) {
+ undoItem.beforeSelection = transaction.beforeSelection;
+ }
+ undoItem.afterSelection = transaction.afterSelection;
+ _debouncedSealHistoryItem();
+ } else if (options.recordRedo) {
+ final redoItem = HistoryItem();
+ redoItem.addAll(transaction.operations);
+ redoItem.beforeSelection = transaction.beforeSelection;
+ redoItem.afterSelection = transaction.afterSelection;
+ undoManager.redoStack.push(redoItem);
+ }
+ }
+
+ _debouncedSealHistoryItem() {
+ _debouncedSealHistoryItemTimer?.cancel();
+ _debouncedSealHistoryItemTimer =
+ Timer(const Duration(milliseconds: 1000), () {
+ if (undoManager.undoStack.isNonEmpty) {
+ debugPrint('Seal history item');
+ final last = undoManager.undoStack.last;
+ last.seal();
+ }
+ });
+ }
+
+ _applyOperation(Operation op) {
+ if (op is InsertOperation) {
+ document.insert(op.path, op.nodes);
+ } else if (op is UpdateOperation) {
+ document.update(op.path, op.attributes);
+ } else if (op is DeleteOperation) {
+ document.delete(op.path, op.nodes.length);
+ } else if (op is TextEditOperation) {
+ document.textEdit(op.path, op.delta);
+ }
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart
new file mode 100644
index 0000000000..52b7596240
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart
@@ -0,0 +1,21 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/extensions/object_extensions.dart';
+import 'package:flowy_editor/extensions/path_extensions.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flutter/material.dart';
+
+extension NodeExtensions on Node {
+ RenderBox? get renderBox =>
+ key?.currentContext?.findRenderObject()?.unwrapOrNull();
+
+ Selectable? get selectable => key?.currentState?.unwrapOrNull();
+
+ bool inSelection(Selection selection) {
+ if (selection.start.path <= selection.end.path) {
+ return selection.start.path <= path && path <= selection.end.path;
+ } else {
+ return selection.end.path <= path && path <= selection.start.path;
+ }
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart
new file mode 100644
index 0000000000..b1b6e53512
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart
@@ -0,0 +1,8 @@
+extension FlowyObjectExtensions on Object {
+ T? unwrapOrNull() {
+ if (this is T) {
+ return this as T;
+ }
+ return null;
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart
new file mode 100644
index 0000000000..793dc552dd
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart
@@ -0,0 +1,36 @@
+import 'package:flowy_editor/document/path.dart';
+
+import 'dart:math';
+
+extension PathExtensions on Path {
+ bool operator >=(Path other) {
+ final length = min(this.length, other.length);
+ for (var i = 0; i < length; i++) {
+ if (this[i] < other[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool operator <=(Path other) {
+ final length = min(this.length, other.length);
+ for (var i = 0; i < length; i++) {
+ if (this[i] > other[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ Path get next {
+ Path nextPath = Path.from(this, growable: true);
+ if (isEmpty) {
+ return nextPath;
+ }
+ final last = nextPath.last;
+ return nextPath
+ ..removeLast()
+ ..add(last + 1);
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart
new file mode 100644
index 0000000000..29e90784ae
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart
@@ -0,0 +1,88 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/document/text_delta.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+
+extension TextNodeExtension on TextNode {
+ bool allSatisfyBoldInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.bold, selection);
+
+ bool allSatisfyItalicInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.italic, selection);
+
+ bool allSatisfyUnderlineInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.underline, selection);
+
+ bool allSatisfyStrikethroughInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.strikethrough, selection);
+
+ bool allSatisfyInSelection(String styleKey, Selection selection) {
+ final ops = delta.operations.whereType();
+ var start = 0;
+ for (final op in ops) {
+ if (start >= selection.end.offset) {
+ break;
+ }
+ final length = op.length;
+ if (start < selection.end.offset &&
+ start + length > selection.start.offset) {
+ if (op.attributes == null ||
+ !op.attributes!.containsKey(styleKey) ||
+ op.attributes![styleKey] == false) {
+ return false;
+ }
+ }
+ start += length;
+ }
+ return true;
+ }
+}
+
+extension TextNodesExtension on List {
+ bool allSatisfyBoldInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.bold, selection);
+
+ bool allSatisfyItalicInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.italic, selection);
+
+ bool allSatisfyUnderlineInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.underline, selection);
+
+ bool allSatisfyStrikethroughInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.strikethrough, selection);
+
+ bool allSatisfyInSelection(String styleKey, Selection selection) {
+ if (isEmpty) {
+ return false;
+ }
+ if (length == 1) {
+ return first.allSatisfyInSelection(styleKey, selection);
+ } else {
+ for (var i = 0; i < length; i++) {
+ final node = this[i];
+ final Selection newSelection;
+ if (i == 0 && pathEquals(node.path, selection.start.path)) {
+ newSelection = selection.copyWith(
+ end: Position(path: node.path, offset: node.toRawString().length),
+ );
+ } else if (i == length - 1 &&
+ pathEquals(node.path, selection.end.path)) {
+ newSelection = selection.copyWith(
+ start: Position(path: node.path, offset: 0),
+ );
+ } else {
+ newSelection = Selection(
+ start: Position(path: node.path, offset: 0),
+ end: Position(path: node.path, offset: node.toRawString().length),
+ );
+ }
+ if (!node.allSatisfyInSelection(styleKey, newSelection)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart
new file mode 100644
index 0000000000..c3e15959a6
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart
@@ -0,0 +1,15 @@
+library flowy_editor;
+
+export 'package:flowy_editor/document/state_tree.dart';
+export 'package:flowy_editor/document/node.dart';
+export 'package:flowy_editor/document/path.dart';
+export 'package:flowy_editor/document/text_delta.dart';
+export 'package:flowy_editor/render/selection/selectable.dart';
+export 'package:flowy_editor/operation/transaction.dart';
+export 'package:flowy_editor/operation/transaction_builder.dart';
+export 'package:flowy_editor/operation/operation.dart';
+export 'package:flowy_editor/editor_state.dart';
+export 'package:flowy_editor/service/editor_service.dart';
+export 'package:flowy_editor/document/selection.dart';
+export 'package:flowy_editor/document/position.dart';
+export 'package:flowy_editor/service/render_plugin_service.dart';
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart
new file mode 100644
index 0000000000..136b5db4bc
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart
@@ -0,0 +1,39 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+class FlowySvg extends StatelessWidget {
+ const FlowySvg({
+ Key? key,
+ this.name,
+ this.size = const Size(20, 20),
+ this.color,
+ this.number,
+ }) : super(key: key);
+
+ final String? name;
+ final Size size;
+ final Color? color;
+ final int? number;
+
+ @override
+ Widget build(BuildContext context) {
+ if (name != null) {
+ return SizedBox.fromSize(
+ size: size,
+ child: SvgPicture.asset(
+ 'assets/images/$name.svg',
+ color: color,
+ package: 'flowy_editor',
+ ),
+ );
+ } else if (number != null) {
+ final numberText =
+ '';
+ return SizedBox.fromSize(
+ size: size,
+ child: SvgPicture.string(numberText),
+ );
+ }
+ return Container();
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart
new file mode 100644
index 0000000000..e07c196768
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart
@@ -0,0 +1,219 @@
+import 'package:flowy_editor/document/attributes.dart';
+import 'package:flowy_editor/flowy_editor.dart';
+
+abstract class Operation {
+ factory Operation.fromJson(Map map) {
+ String t = map["type"] as String;
+ if (t == "insert-operation") {
+ return InsertOperation.fromJson(map);
+ } else if (t == "update-operation") {
+ return UpdateOperation.fromJson(map);
+ } else if (t == "delete-operation") {
+ return DeleteOperation.fromJson(map);
+ } else if (t == "text-edit-operation") {
+ return TextEditOperation.fromJson(map);
+ }
+
+ throw ArgumentError('unexpected type $t');
+ }
+ final Path path;
+ Operation(this.path);
+ Operation copyWithPath(Path path);
+ Operation invert();
+ Map toJson();
+}
+
+class InsertOperation extends Operation {
+ final List nodes;
+
+ factory InsertOperation.fromJson(Map map) {
+ final path = map["path"] as List;
+ final value =
+ (map["nodes"] as List).map((n) => Node.fromJson(n)).toList();
+ return InsertOperation(path, value);
+ }
+
+ InsertOperation(Path path, this.nodes) : super(path);
+
+ InsertOperation copyWith({Path? path, List? nodes}) =>
+ InsertOperation(path ?? this.path, nodes ?? this.nodes);
+
+ @override
+ Operation copyWithPath(Path path) => copyWith(path: path);
+
+ @override
+ Operation invert() {
+ return DeleteOperation(
+ path,
+ nodes,
+ );
+ }
+
+ @override
+ Map toJson() {
+ return {
+ "type": "insert-operation",
+ "path": path.toList(),
+ "nodes": nodes.map((n) => n.toJson()),
+ };
+ }
+}
+
+class UpdateOperation extends Operation {
+ final Attributes attributes;
+ final Attributes oldAttributes;
+
+ factory UpdateOperation.fromJson(Map map) {
+ final path = map["path"] as List;
+ final attributes = map["attributes"] as Map;
+ final oldAttributes = map["oldAttributes"] as Map;
+ return UpdateOperation(path, attributes, oldAttributes);
+ }
+
+ UpdateOperation(
+ Path path,
+ this.attributes,
+ this.oldAttributes,
+ ) : super(path);
+
+ UpdateOperation copyWith(
+ {Path? path, Attributes? attributes, Attributes? oldAttributes}) =>
+ UpdateOperation(path ?? this.path, attributes ?? this.attributes,
+ oldAttributes ?? this.oldAttributes);
+
+ @override
+ Operation copyWithPath(Path path) => copyWith(path: path);
+
+ @override
+ Operation invert() {
+ return UpdateOperation(
+ path,
+ oldAttributes,
+ attributes,
+ );
+ }
+
+ @override
+ Map toJson() {
+ return {
+ "type": "update-operation",
+ "path": path.toList(),
+ "attributes": {...attributes},
+ "oldAttributes": {...oldAttributes},
+ };
+ }
+}
+
+class DeleteOperation extends Operation {
+ final List nodes;
+
+ factory DeleteOperation.fromJson(Map map) {
+ final path = map["path"] as List;
+ final List nodes =
+ (map["nodes"] as List).map((e) => Node.fromJson(e)).toList();
+ return DeleteOperation(path, nodes);
+ }
+
+ DeleteOperation(
+ Path path,
+ this.nodes,
+ ) : super(path);
+
+ DeleteOperation copyWith({Path? path, List? nodes}) =>
+ DeleteOperation(path ?? this.path, nodes ?? this.nodes);
+
+ @override
+ Operation copyWithPath(Path path) => copyWith(path: path);
+
+ @override
+ Operation invert() {
+ return InsertOperation(path, nodes);
+ }
+
+ @override
+ Map toJson() {
+ return {
+ "type": "delete-operation",
+ "path": path.toList(),
+ "nodes": nodes.map((n) => n.toJson()),
+ };
+ }
+}
+
+class TextEditOperation extends Operation {
+ final Delta delta;
+ final Delta inverted;
+
+ factory TextEditOperation.fromJson(Map map) {
+ final path = map["path"] as List;
+ final delta = Delta.fromJson(map["delta"]);
+ final invert = Delta.fromJson(map["invert"]);
+ return TextEditOperation(path, delta, invert);
+ }
+
+ TextEditOperation(
+ Path path,
+ this.delta,
+ this.inverted,
+ ) : super(path);
+
+ TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) =>
+ TextEditOperation(
+ path ?? this.path, delta ?? this.delta, inverted ?? this.inverted);
+
+ @override
+ Operation copyWithPath(Path path) => copyWith(path: path);
+
+ @override
+ Operation invert() {
+ return TextEditOperation(path, inverted, delta);
+ }
+
+ @override
+ Map toJson() {
+ return {
+ "type": "text-edit-operation",
+ "path": path.toList(),
+ "delta": delta.toJson(),
+ "invert": inverted.toJson(),
+ };
+ }
+}
+
+Path transformPath(Path preInsertPath, Path b, [int delta = 1]) {
+ if (preInsertPath.length > b.length) {
+ return b;
+ }
+ if (preInsertPath.isEmpty || b.isEmpty) {
+ return b;
+ }
+ // check the prefix
+ for (var i = 0; i < preInsertPath.length - 1; i++) {
+ if (preInsertPath[i] != b[i]) {
+ return b;
+ }
+ }
+ final prefix = preInsertPath.sublist(0, preInsertPath.length - 1);
+ final suffix = b.sublist(preInsertPath.length);
+ final preInsertLast = preInsertPath.last;
+ final bAtIndex = b[preInsertPath.length - 1];
+ if (preInsertLast <= bAtIndex) {
+ prefix.add(bAtIndex + delta);
+ } else {
+ prefix.add(bAtIndex);
+ }
+ prefix.addAll(suffix);
+ return prefix;
+}
+
+Operation transformOperation(Operation a, Operation b) {
+ if (a is InsertOperation) {
+ final newPath = transformPath(a.path, b.path);
+ return b.copyWithPath(newPath);
+ } else if (b is DeleteOperation) {
+ final newPath = transformPath(a.path, b.path, -1);
+ return b.copyWithPath(newPath);
+ }
+ // TODO: transform update and textedit
+ return b;
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart
new file mode 100644
index 0000000000..5dcf167628
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart
@@ -0,0 +1,39 @@
+import 'dart:collection';
+import 'package:flutter/material.dart';
+import 'package:flowy_editor/document/selection.dart';
+import './operation.dart';
+
+/// A [Transaction] has a list of [Operation] objects that will be applied
+/// to the editor. It is an immutable class and used to store and transmit.
+///
+/// If you want to build a new [Transaction], use [TransactionBuilder] directly.
+///
+/// There will be several ways to consume the transaction:
+/// 1. Apply to the state to update the UI.
+/// 2. Send to the backend to store and do operation transforming.
+/// 3. Used by the UndoManager to implement redo/undo.
+@immutable
+class Transaction {
+ final UnmodifiableListView operations;
+ final Selection? beforeSelection;
+ final Selection? afterSelection;
+
+ const Transaction({
+ required this.operations,
+ this.beforeSelection,
+ this.afterSelection,
+ });
+
+ Map toJson() {
+ final Map result = {
+ "operations": operations.map((e) => e.toJson()),
+ };
+ if (beforeSelection != null) {
+ result["beforeSelection"] = beforeSelection!.toJson();
+ }
+ if (afterSelection != null) {
+ result["afterSelection"] = afterSelection!.toJson();
+ }
+ return result;
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart
new file mode 100644
index 0000000000..88e0c00890
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart
@@ -0,0 +1,161 @@
+import 'dart:collection';
+
+import 'package:flowy_editor/document/attributes.dart';
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/document/text_delta.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/operation/operation.dart';
+import 'package:flowy_editor/operation/transaction.dart';
+
+/// A [TransactionBuilder] is used to build the transaction from the state.
+/// It will save make a snapshot of the cursor selection state automatically.
+/// The cursor can be resorted if the transaction is undo.
+
+class TransactionBuilder {
+ final List operations = [];
+ EditorState state;
+ Selection? beforeSelection;
+ Selection? afterSelection;
+
+ TransactionBuilder(this.state);
+
+ /// Commit the operations to the state
+ commit() {
+ final transaction = finish();
+ state.apply(transaction);
+ }
+
+ insertNode(Path path, Node node) {
+ insertNodes(path, [node]);
+ }
+
+ insertNodes(Path path, List nodes) {
+ beforeSelection = state.cursorSelection;
+ add(InsertOperation(path, nodes));
+ }
+
+ updateNode(Node node, Attributes attributes) {
+ beforeSelection = state.cursorSelection;
+ add(UpdateOperation(
+ node.path,
+ Attributes.from(node.attributes)..addAll(attributes),
+ node.attributes,
+ ));
+ }
+
+ deleteNode(Node node) {
+ deleteNodesAtPath(node.path);
+ }
+
+ deleteNodes(List nodes) {
+ nodes.forEach(deleteNode);
+ }
+
+ deleteNodesAtPath(Path path, [int length = 1]) {
+ if (path.isEmpty) {
+ return;
+ }
+ final nodes = [];
+ final prefix = path.sublist(0, path.length - 1);
+ final last = path.last;
+ for (var i = 0; i < length; i++) {
+ final node = state.document.nodeAtPath(prefix + [last + i])!;
+ nodes.add(node);
+ }
+
+ add(DeleteOperation(path, nodes));
+ }
+
+ textEdit(TextNode node, Delta Function() f) {
+ beforeSelection = state.cursorSelection;
+ final path = node.path;
+
+ final delta = f();
+
+ final inverted = delta.invert(node.delta);
+
+ add(TextEditOperation(path, delta, inverted));
+ }
+
+ mergeText(TextNode firstNode, TextNode secondNode,
+ {int? firstOffset, int secondOffset = 0}) {
+ final firstLength = firstNode.delta.length;
+ final secondLength = secondNode.delta.length;
+ textEdit(
+ firstNode,
+ () => Delta()
+ ..retain(firstOffset ?? firstLength)
+ ..delete(firstLength - (firstOffset ?? firstLength))
+ ..addAll(secondNode.delta.slice(secondOffset, secondLength).operations),
+ );
+ afterSelection = Selection.collapsed(
+ Position(
+ path: firstNode.path,
+ offset: firstOffset ?? firstLength,
+ ),
+ );
+ }
+
+ insertText(TextNode node, int index, String content,
+ [Attributes? attributes]) {
+ textEdit(node, () => Delta().retain(index).insert(content, attributes));
+ afterSelection = Selection.collapsed(
+ Position(path: node.path, offset: index + content.length));
+ }
+
+ formatText(TextNode node, int index, int length, Attributes attributes) {
+ textEdit(node, () => Delta().retain(index).retain(length, attributes));
+ afterSelection = beforeSelection;
+ }
+
+ deleteText(TextNode node, int index, int length) {
+ textEdit(node, () => Delta().retain(index).delete(length));
+ afterSelection =
+ Selection.collapsed(Position(path: node.path, offset: index));
+ }
+
+ replaceText(TextNode node, int index, int length, String content) {
+ textEdit(
+ node,
+ () => Delta().retain(index).delete(length).insert(content),
+ );
+ afterSelection = Selection.collapsed(
+ Position(
+ path: node.path,
+ offset: index + content.length,
+ ),
+ );
+ }
+
+ add(Operation op) {
+ final Operation? last = operations.isEmpty ? null : operations.last;
+ if (last != null) {
+ if (op is TextEditOperation &&
+ last is TextEditOperation &&
+ pathEquals(op.path, last.path)) {
+ final newOp = TextEditOperation(
+ op.path,
+ last.delta.compose(op.delta),
+ op.inverted.compose(last.inverted),
+ );
+ operations[operations.length - 1] = newOp;
+ return;
+ }
+ }
+ for (var i = 0; i < operations.length; i++) {
+ op = transformOperation(operations[i], op);
+ }
+ operations.add(op);
+ }
+
+ Transaction finish() {
+ return Transaction(
+ operations: UnmodifiableListView(operations),
+ beforeSelection: beforeSelection,
+ afterSelection: afterSelection,
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart
new file mode 100644
index 0000000000..650732f9f9
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart
@@ -0,0 +1,58 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class EditorEntryWidgetBuilder extends NodeWidgetBuilder {
+ @override
+ Widget build(NodeWidgetContext context) {
+ return EditorNodeWidget(
+ key: context.node.key,
+ node: context.node,
+ editorState: context.editorState,
+ );
+ }
+
+ @override
+ NodeValidator get nodeValidator => ((node) {
+ return node.type == 'editor';
+ });
+}
+
+class EditorNodeWidget extends StatelessWidget {
+ const EditorNodeWidget({
+ Key? key,
+ required this.node,
+ required this.editorState,
+ }) : super(key: key);
+
+ final Node node;
+ final EditorState editorState;
+
+ @override
+ Widget build(BuildContext context) {
+ return SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: node.children
+ .map(
+ (child) =>
+ editorState.service.renderPluginService.buildPluginWidget(
+ child is TextNode
+ ? NodeWidgetContext(
+ context: context,
+ node: child,
+ editorState: editorState,
+ )
+ : NodeWidgetContext(
+ context: context,
+ node: child,
+ editorState: editorState,
+ ),
+ ),
+ )
+ .toList(),
+ ),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart
new file mode 100644
index 0000000000..0eae3f22f2
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart
@@ -0,0 +1,73 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder {
+ @override
+ Widget build(NodeWidgetContext context) {
+ return BulletedListTextNodeWidget(
+ key: context.node.key,
+ textNode: context.node,
+ editorState: context.editorState,
+ );
+ }
+
+ @override
+ NodeValidator get nodeValidator => ((node) {
+ return true;
+ });
+}
+
+class BulletedListTextNodeWidget extends StatefulWidget {
+ const BulletedListTextNodeWidget({
+ Key? key,
+ required this.textNode,
+ required this.editorState,
+ }) : super(key: key);
+
+ final TextNode textNode;
+ final EditorState editorState;
+
+ @override
+ State createState() =>
+ _BulletedListTextNodeWidgetState();
+}
+
+// customize
+
+class _BulletedListTextNodeWidgetState extends State
+ with Selectable, DefaultSelectable {
+ final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text');
+ final leftPadding = 20.0;
+
+ @override
+ Selectable get forward =>
+ _richTextKey.currentState as Selectable;
+
+ @override
+ Offset get baseOffset {
+ return Offset(leftPadding, 0);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ FlowySvg(
+ size: Size.square(leftPadding),
+ name: 'point',
+ ),
+ FlowyRichText(
+ key: _richTextKey,
+ textNode: widget.textNode,
+ editorState: widget.editorState,
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart
new file mode 100644
index 0000000000..ba2c5b8712
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart
@@ -0,0 +1,123 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/operation/transaction_builder.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder {
+ @override
+ Widget build(NodeWidgetContext context) {
+ return CheckboxNodeWidget(
+ key: context.node.key,
+ textNode: context.node,
+ editorState: context.editorState,
+ );
+ }
+
+ @override
+ NodeValidator get nodeValidator => ((node) {
+ return node.attributes.containsKey(StyleKey.checkbox);
+ });
+}
+
+class CheckboxNodeWidget extends StatefulWidget {
+ const CheckboxNodeWidget({
+ Key? key,
+ required this.textNode,
+ required this.editorState,
+ }) : super(key: key);
+
+ final TextNode textNode;
+ final EditorState editorState;
+
+ @override
+ State createState() => _CheckboxNodeWidgetState();
+}
+
+class _CheckboxNodeWidgetState extends State
+ with Selectable, DefaultSelectable {
+ final _richTextKey = GlobalKey(debugLabel: 'checkbox_text');
+
+ final leftPadding = 20.0;
+
+ @override
+ Selectable get forward =>
+ _richTextKey.currentState as Selectable;
+
+ @override
+ Offset get baseOffset {
+ return Offset(leftPadding, 0);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (widget.textNode.children.isEmpty) {
+ return _buildWithSingle(context);
+ } else {
+ return _buildWithChildren(context);
+ }
+ }
+
+ Widget _buildWithSingle(BuildContext context) {
+ final check = widget.textNode.attributes.check;
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ GestureDetector(
+ child: FlowySvg(
+ size: Size.square(leftPadding),
+ name: check ? 'check' : 'uncheck',
+ ),
+ onTap: () {
+ debugPrint('[Checkbox] onTap...');
+ TransactionBuilder(widget.editorState)
+ ..updateNode(widget.textNode, {
+ 'checkbox': !check,
+ })
+ ..commit();
+ },
+ ),
+ FlowyRichText(
+ key: _richTextKey,
+ textNode: widget.textNode,
+ editorState: widget.editorState,
+ )
+ ],
+ );
+ }
+
+ Widget _buildWithChildren(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildWithSingle(context),
+ Row(
+ children: [
+ const SizedBox(
+ width: 20,
+ ),
+ Column(
+ children: widget.textNode.children
+ .map(
+ (child) => widget.editorState.service.renderPluginService
+ .buildPluginWidget(
+ NodeWidgetContext(
+ context: context,
+ node: child,
+ editorState: widget.editorState,
+ ),
+ ),
+ )
+ .toList(),
+ )
+ ],
+ )
+ ],
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart
new file mode 100644
index 0000000000..21cc5108f3
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart
@@ -0,0 +1,33 @@
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flutter/material.dart';
+
+mixin DefaultSelectable {
+ Selectable get forward;
+
+ Offset get baseOffset;
+
+ Position getPositionInOffset(Offset start) =>
+ forward.getPositionInOffset(start);
+
+ Rect getCursorRectInPosition(Position position) =>
+ forward.getCursorRectInPosition(position).shift(baseOffset);
+
+ List getRectsInSelection(Selection selection) => forward
+ .getRectsInSelection(selection)
+ .map((rect) => rect.shift(baseOffset))
+ .toList(growable: false);
+
+ Selection getSelectionInRange(Offset start, Offset end) =>
+ forward.getSelectionInRange(start, end);
+
+ Offset localToGlobal(Offset offset) => forward.localToGlobal(offset);
+
+ Selection? getWorldBoundaryInOffset(Offset offset) =>
+ forward.getWorldBoundaryInOffset(offset);
+
+ Position start() => forward.start();
+
+ Position end() => forward.end();
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart
new file mode 100644
index 0000000000..f302fcaba8
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart
@@ -0,0 +1,180 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/document/text_delta.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+class RichTextNodeWidgetBuilder extends NodeWidgetBuilder {
+ @override
+ Widget build(NodeWidgetContext context) {
+ return FlowyRichText(
+ key: context.node.key,
+ textNode: context.node,
+ editorState: context.editorState,
+ );
+ }
+
+ @override
+ NodeValidator get nodeValidator => ((node) {
+ return true;
+ });
+}
+
+typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan);
+
+class FlowyRichText extends StatefulWidget {
+ const FlowyRichText({
+ Key? key,
+ this.cursorHeight,
+ this.cursorWidth = 2.0,
+ this.textSpanDecorator,
+ required this.textNode,
+ required this.editorState,
+ }) : super(key: key);
+
+ final double? cursorHeight;
+ final double cursorWidth;
+ final TextNode textNode;
+ final EditorState editorState;
+ final FlowyTextSpanDecorator? textSpanDecorator;
+
+ @override
+ State createState() => _FlowyRichTextState();
+}
+
+class _FlowyRichTextState extends State with Selectable {
+ final _textKey = GlobalKey();
+
+ RenderParagraph get _renderParagraph =>
+ _textKey.currentContext?.findRenderObject() as RenderParagraph;
+
+ @override
+ Widget build(BuildContext context) {
+ return _buildRichText(context);
+ }
+
+ @override
+ Position start() => Position(path: widget.textNode.path, offset: 0);
+
+ @override
+ Position end() => Position(
+ path: widget.textNode.path, offset: widget.textNode.toRawString().length);
+
+ @override
+ Rect getCursorRectInPosition(Position position) {
+ final textPosition = TextPosition(offset: position.offset);
+ final cursorOffset =
+ _renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
+ final cursorHeight = widget.cursorHeight ??
+ _renderParagraph.getFullHeightForCaret(textPosition) ??
+ 18.0; // default height
+ return Rect.fromLTWH(
+ cursorOffset.dx - (widget.cursorWidth / 2),
+ cursorOffset.dy,
+ widget.cursorWidth,
+ cursorHeight,
+ );
+ }
+
+ @override
+ Position getPositionInOffset(Offset start) {
+ final offset = _renderParagraph.globalToLocal(start);
+ final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
+ return Position(path: widget.textNode.path, offset: baseOffset);
+ }
+
+ @override
+ Selection? getWorldBoundaryInOffset(Offset offset) {
+ final localOffset = _renderParagraph.globalToLocal(offset);
+ final textPosition = _renderParagraph.getPositionForOffset(localOffset);
+ final textRange = _renderParagraph.getWordBoundary(textPosition);
+ final start = Position(path: widget.textNode.path, offset: textRange.start);
+ final end = Position(path: widget.textNode.path, offset: textRange.end);
+ return Selection(start: start, end: end);
+ }
+
+ @override
+ List getRectsInSelection(Selection selection) {
+ assert(pathEquals(selection.start.path, selection.end.path) &&
+ pathEquals(selection.start.path, widget.textNode.path));
+
+ final textSelection = TextSelection(
+ baseOffset: selection.start.offset,
+ extentOffset: selection.end.offset,
+ );
+ return _renderParagraph
+ .getBoxesForSelection(textSelection)
+ .map((box) => box.toRect())
+ .toList();
+ }
+
+ @override
+ Selection getSelectionInRange(Offset start, Offset end) {
+ final localStart = _renderParagraph.globalToLocal(start);
+ final localEnd = _renderParagraph.globalToLocal(end);
+ final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
+ final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
+ return Selection.single(
+ path: widget.textNode.path,
+ startOffset: baseOffset,
+ endOffset: extentOffset,
+ );
+ }
+
+ Widget _buildRichText(BuildContext context) {
+ return _buildSingleRichText(context);
+ }
+
+ Widget _buildSingleRichText(BuildContext context) {
+ final textSpan = _textSpan;
+ return RichText(
+ key: _textKey,
+ text: widget.textSpanDecorator != null
+ ? widget.textSpanDecorator!(textSpan)
+ : textSpan,
+ );
+ }
+
+ // unused now.
+ Widget _buildRichTextWithChildren(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildSingleRichText(context),
+ ...widget.textNode.children
+ .map(
+ (child) => widget.editorState.service.renderPluginService
+ .buildPluginWidget(
+ NodeWidgetContext(
+ context: context,
+ node: child,
+ editorState: widget.editorState,
+ ),
+ ),
+ )
+ .toList()
+ ],
+ );
+ }
+
+ @override
+ Offset localToGlobal(Offset offset) {
+ return _renderParagraph.localToGlobal(offset);
+ }
+
+ TextSpan get _textSpan => TextSpan(
+ children: widget.textNode.delta.operations
+ .whereType()
+ .map((insert) => RichTextStyle(
+ attributes: insert.attributes ?? {},
+ text: insert.content,
+ ).toTextSpan())
+ .toList(growable: false));
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart
new file mode 100644
index 0000000000..4990e90dcf
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart
@@ -0,0 +1,93 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder {
+ @override
+ Widget build(NodeWidgetContext context) {
+ return HeadingTextNodeWidget(
+ key: context.node.key,
+ textNode: context.node,
+ editorState: context.editorState,
+ );
+ }
+
+ @override
+ NodeValidator get nodeValidator => ((node) {
+ return node.attributes.heading != null;
+ });
+}
+
+class HeadingTextNodeWidget extends StatefulWidget {
+ const HeadingTextNodeWidget({
+ Key? key,
+ required this.textNode,
+ required this.editorState,
+ }) : super(key: key);
+
+ final TextNode textNode;
+ final EditorState editorState;
+
+ @override
+ State createState() => _HeadingTextNodeWidgetState();
+}
+
+// customize
+
+class _HeadingTextNodeWidgetState extends State
+ with Selectable, DefaultSelectable {
+ final _richTextKey = GlobalKey(debugLabel: 'heading_text');
+ final topPadding = 5.0;
+ final bottomPadding = 2.0;
+
+ @override
+ Selectable get forward =>
+ _richTextKey.currentState as Selectable;
+
+ @override
+ Offset get baseOffset {
+ return Offset(0, topPadding);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Padding(
+ padding: EdgeInsets.only(
+ top: topPadding,
+ bottom: bottomPadding,
+ ),
+ child: FlowyRichText(
+ key: _richTextKey,
+ textSpanDecorator: _textSpanDecorator,
+ textNode: widget.textNode,
+ editorState: widget.editorState,
+ ),
+ )
+ ],
+ );
+ }
+
+ TextSpan _textSpanDecorator(TextSpan textSpan) {
+ return TextSpan(
+ children: textSpan.children
+ ?.whereType()
+ .map(
+ (span) => TextSpan(
+ text: span.text,
+ style: span.style?.copyWith(
+ fontSize: widget.textNode.attributes.fontSize,
+ ),
+ recognizer: span.recognizer,
+ ),
+ )
+ .toList(),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart
new file mode 100644
index 0000000000..1c52b93d4b
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart
@@ -0,0 +1,74 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder {
+ @override
+ Widget build(NodeWidgetContext context) {
+ return NumberListTextNodeWidget(
+ key: context.node.key,
+ textNode: context.node,
+ editorState: context.editorState,
+ );
+ }
+
+ @override
+ NodeValidator get nodeValidator => ((node) {
+ return node.attributes.number != null;
+ });
+}
+
+class NumberListTextNodeWidget extends StatefulWidget {
+ const NumberListTextNodeWidget({
+ Key? key,
+ required this.textNode,
+ required this.editorState,
+ }) : super(key: key);
+
+ final TextNode textNode;
+ final EditorState editorState;
+
+ @override
+ State createState() =>
+ _NumberListTextNodeWidgetState();
+}
+
+// customize
+
+class _NumberListTextNodeWidgetState extends State
+ with Selectable, DefaultSelectable {
+ final _richTextKey = GlobalKey(debugLabel: 'number_list_text');
+ final leftPadding = 20.0;
+
+ @override
+ Selectable get forward =>
+ _richTextKey.currentState as Selectable;
+
+ @override
+ Offset get baseOffset {
+ return Offset(leftPadding, 0);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ FlowySvg(
+ size: Size.square(leftPadding),
+ number: widget.textNode.attributes.number,
+ ),
+ FlowyRichText(
+ key: _richTextKey,
+ textNode: widget.textNode,
+ editorState: widget.editorState,
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart
new file mode 100644
index 0000000000..41520c560f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart
@@ -0,0 +1,73 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder {
+ @override
+ Widget build(NodeWidgetContext context) {
+ return QuotedTextNodeWidget(
+ key: context.node.key,
+ textNode: context.node,
+ editorState: context.editorState,
+ );
+ }
+
+ @override
+ NodeValidator get nodeValidator => ((node) {
+ return true;
+ });
+}
+
+class QuotedTextNodeWidget extends StatefulWidget {
+ const QuotedTextNodeWidget({
+ Key? key,
+ required this.textNode,
+ required this.editorState,
+ }) : super(key: key);
+
+ final TextNode textNode;
+ final EditorState editorState;
+
+ @override
+ State createState() => _QuotedTextNodeWidgetState();
+}
+
+// customize
+
+class _QuotedTextNodeWidgetState extends State
+ with Selectable, DefaultSelectable {
+ final _richTextKey = GlobalKey(debugLabel: 'quoted_text');
+ final leftPadding = 20.0;
+
+ @override
+ Selectable get forward =>
+ _richTextKey.currentState as Selectable;
+
+ @override
+ Offset get baseOffset {
+ return Offset(leftPadding, 0);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ FlowySvg(
+ size: Size.square(leftPadding),
+ name: 'quote',
+ ),
+ FlowyRichText(
+ key: _richTextKey,
+ textNode: widget.textNode,
+ editorState: widget.editorState,
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart
new file mode 100644
index 0000000000..cc4f6038ac
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart
@@ -0,0 +1,247 @@
+import 'package:flowy_editor/document/attributes.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+
+///
+/// Supported partial rendering types:
+/// bold, italic,
+/// underline, strikethrough,
+/// color, font,
+/// href
+///
+/// Supported global rendering types:
+/// heading: h1, h2, h3, h4, h5, h6, ...
+/// block quote,
+/// list: ordered list, bulleted list,
+/// code block
+///
+class StyleKey {
+ static String bold = 'bold';
+ static String italic = 'italic';
+ static String underline = 'underline';
+ static String strikethrough = 'strikethrough';
+ static String color = 'color';
+ static String highlightColor = 'highlightColor';
+ static String font = 'font';
+ static String href = 'href';
+
+ static String subtype = 'subtype';
+ static String heading = 'heading';
+ static String h1 = 'h1';
+ static String h2 = 'h2';
+ static String h3 = 'h3';
+ static String h4 = 'h4';
+ static String h5 = 'h5';
+ static String h6 = 'h6';
+
+ static String bulletedList = 'bulleted-list';
+ static String numberList = 'number-list';
+
+ static String quote = 'quote';
+ static String checkbox = 'checkbox';
+ static String code = 'code';
+ static String number = 'number';
+
+ static List partialStyleKeys = [
+ StyleKey.bold,
+ StyleKey.italic,
+ StyleKey.underline,
+ StyleKey.strikethrough,
+ ];
+
+ static List globalStyleKeys = [
+ StyleKey.heading,
+ StyleKey.checkbox,
+ StyleKey.bulletedList,
+ StyleKey.numberList,
+ StyleKey.quote,
+ StyleKey.code,
+ ];
+}
+
+double baseFontSize = 16.0;
+// TODO: customize.
+Map headingToFontSize = {
+ StyleKey.h1: baseFontSize + 15,
+ StyleKey.h2: baseFontSize + 12,
+ StyleKey.h3: baseFontSize + 9,
+ StyleKey.h4: baseFontSize + 6,
+ StyleKey.h5: baseFontSize + 3,
+ StyleKey.h6: baseFontSize,
+};
+
+extension NodeAttributesExtensions on Attributes {
+ String? get heading {
+ if (containsKey(StyleKey.heading) && this[StyleKey.heading] is String) {
+ return this[StyleKey.heading];
+ }
+ return null;
+ }
+
+ double get fontSize {
+ if (heading != null) {
+ return headingToFontSize[heading]!;
+ }
+ return baseFontSize;
+ }
+
+ bool get quote {
+ return containsKey(StyleKey.quote);
+ }
+
+ Color? get quoteColor {
+ if (quote) {
+ return Colors.grey;
+ }
+ return null;
+ }
+
+ int? get number {
+ if (containsKey(StyleKey.number) && this[StyleKey.number] is int) {
+ return this[StyleKey.number];
+ }
+ return null;
+ }
+
+ bool get code {
+ if (containsKey(StyleKey.code) && this[StyleKey.code] == true) {
+ return this[StyleKey.code];
+ }
+ return false;
+ }
+
+ bool get check {
+ if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) {
+ return this[StyleKey.checkbox];
+ }
+ return false;
+ }
+}
+
+extension DeltaAttributesExtensions on Attributes {
+ bool get bold {
+ return (containsKey(StyleKey.bold) && this[StyleKey.bold] == true);
+ }
+
+ bool get italic {
+ return (containsKey(StyleKey.italic) && this[StyleKey.italic] == true);
+ }
+
+ bool get underline {
+ return (containsKey(StyleKey.underline) &&
+ this[StyleKey.underline] == true);
+ }
+
+ bool get strikethrough {
+ return (containsKey(StyleKey.strikethrough) &&
+ this[StyleKey.strikethrough] == true);
+ }
+
+ Color? get color {
+ if (containsKey(StyleKey.color) && this[StyleKey.color] is String) {
+ return Color(
+ int.parse(this[StyleKey.color]),
+ );
+ }
+ return null;
+ }
+
+ Color? get hightlightColor {
+ if (containsKey(StyleKey.highlightColor) &&
+ this[StyleKey.highlightColor] is String) {
+ return Color(
+ int.parse(this[StyleKey.highlightColor]),
+ );
+ }
+ return null;
+ }
+
+ String? get font {
+ // TODO: unspport now.
+ return null;
+ }
+
+ String? get href {
+ if (containsKey(StyleKey.href) && this[StyleKey.href] is String) {
+ return this[StyleKey.href];
+ }
+ return null;
+ }
+}
+
+class RichTextStyle {
+ // TODO: customize
+ RichTextStyle({
+ required this.attributes,
+ required this.text,
+ });
+
+ final Attributes attributes;
+ final String text;
+
+ TextSpan toTextSpan() {
+ return TextSpan(
+ text: text,
+ style: TextStyle(
+ fontWeight: fontWeight,
+ fontStyle: fontStyle,
+ fontSize: fontSize,
+ color: textColor,
+ backgroundColor: backgroundColor,
+ decoration: textDecoration,
+ ),
+ recognizer: recognizer,
+ );
+ }
+
+ // bold
+ FontWeight get fontWeight {
+ if (attributes.bold) {
+ return FontWeight.bold;
+ }
+ return FontWeight.normal;
+ }
+
+ // underline or strikethrough
+ TextDecoration get textDecoration {
+ if (attributes.underline || attributes.href != null) {
+ return TextDecoration.underline;
+ } else if (attributes.strikethrough) {
+ return TextDecoration.lineThrough;
+ }
+ return TextDecoration.none;
+ }
+
+ // font
+ FontStyle get fontStyle =>
+ attributes.italic ? FontStyle.italic : FontStyle.normal;
+
+ // text color
+ Color get textColor {
+ if (attributes.href != null) {
+ return Colors.lightBlue;
+ }
+ return attributes.color ?? Colors.black;
+ }
+
+ Color get backgroundColor {
+ return attributes.hightlightColor ?? Colors.transparent;
+ }
+
+ // font size
+ double get fontSize {
+ return baseFontSize;
+ }
+
+ // recognizer
+ GestureRecognizer? get recognizer {
+ final href = attributes.href;
+ if (href != null) {
+ return TapGestureRecognizer()
+ ..onTap = () async {
+ // FIXME: launch the url
+ };
+ }
+ return null;
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart
new file mode 100644
index 0000000000..19da4b55f4
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart
@@ -0,0 +1,77 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+class CursorWidget extends StatefulWidget {
+ const CursorWidget({
+ Key? key,
+ required this.layerLink,
+ required this.rect,
+ required this.color,
+ this.blinkingInterval = 0.5,
+ }) : super(key: key);
+
+ final double blinkingInterval; // milliseconds
+ final Color color;
+ final Rect rect;
+ final LayerLink layerLink;
+
+ @override
+ State createState() => CursorWidgetState();
+}
+
+class CursorWidgetState extends State {
+ bool showCursor = true;
+ late Timer timer;
+
+ @override
+ void initState() {
+ super.initState();
+
+ timer = _initTimer();
+ }
+
+ @override
+ void dispose() {
+ timer.cancel();
+ super.dispose();
+ }
+
+ Timer _initTimer() {
+ return Timer.periodic(
+ Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()),
+ (timer) {
+ setState(() {
+ showCursor = !showCursor;
+ });
+ });
+ }
+
+ /// force the cursor widget to show for a while
+ show() {
+ setState(() {
+ showCursor = true;
+ });
+ timer.cancel();
+ timer = _initTimer();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Positioned.fromRect(
+ rect: widget.rect,
+ child: CompositedTransformFollower(
+ link: widget.layerLink,
+ offset: widget.rect.topCenter,
+ showWhenUnlinked: true,
+ // Ignore the gestures in cursor
+ // to solve the problem that cursor area cannot be selected.
+ child: IgnorePointer(
+ child: Container(
+ color: showCursor ? widget.color : Colors.transparent,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart
new file mode 100644
index 0000000000..bc32706aa0
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart
@@ -0,0 +1,42 @@
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flutter/material.dart';
+
+///
+mixin Selectable on State {
+ /// Returns a [List] of the [Rect] selection surrounded by start and end
+ /// in current widget.
+ ///
+ /// [start] and [end] are the offsets under the global coordinate system.
+ ///
+ /// The return result must be a [List] of the [Rect]
+ /// under the local coordinate system.
+ Selection getSelectionInRange(Offset start, Offset end);
+
+ List getRectsInSelection(Selection selection);
+
+ /// Returns a [Rect] for the offset in current widget.
+ ///
+ /// [start] is the offset of the global coordination system.
+ ///
+ /// The return result must be an offset of the local coordinate system.
+ Position getPositionInOffset(Offset start);
+ Selection? getWorldBoundaryInOffset(Offset start) {
+ return null;
+ }
+
+ Rect getCursorRectInPosition(Position position);
+
+ Offset localToGlobal(Offset offset);
+
+ Position start();
+ Position end();
+
+ /// For [TextNode] only.
+ ///
+ /// Returns a [TextSelection] or [Null].
+ ///
+ /// Only the widget rendered by [TextNode] need to implement the detail,
+ /// and the rest can return null.
+ TextSelection? getTextSelectionInSelection(Selection selection) => null;
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart
new file mode 100644
index 0000000000..e3dea7af34
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+
+class SelectionWidget extends StatefulWidget {
+ const SelectionWidget({
+ Key? key,
+ required this.layerLink,
+ required this.rect,
+ required this.color,
+ }) : super(key: key);
+
+ final Color color;
+ final Rect rect;
+ final LayerLink layerLink;
+
+ @override
+ State createState() => _SelectionWidgetState();
+}
+
+class _SelectionWidgetState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Positioned.fromRect(
+ rect: widget.rect,
+ child: CompositedTransformFollower(
+ link: widget.layerLink,
+ offset: widget.rect.topLeft,
+ showWhenUnlinked: true,
+ // Ignore the gestures in selection overlays
+ // to solve the problem that selection areas cannot overlap.
+ child: IgnorePointer(
+ child: Container(
+ color: widget.color,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart
new file mode 100644
index 0000000000..91659e1d1f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart
@@ -0,0 +1,219 @@
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flutter/material.dart';
+
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart';
+
+typedef ToolbarEventHandler = void Function(EditorState editorState);
+
+typedef ToolbarEventHandlers = Map;
+
+ToolbarEventHandlers defaultToolbarEventHandlers = {
+ 'bold': (editorState) => formatBold(editorState),
+ 'italic': (editorState) => formatItalic(editorState),
+ 'strikethrough': (editorState) => formatStrikethrough(editorState),
+ 'underline': (editorState) => formatUnderline(editorState),
+ 'quote': (editorState) => formatQuote(editorState),
+ 'number_list': (editorState) {},
+ 'bulleted_list': (editorState) => formatBulletedList(editorState),
+ 'Text': (editorState) => formatText(editorState),
+ 'H1': (editorState) => formatHeading(editorState, StyleKey.h1),
+ 'H2': (editorState) => formatHeading(editorState, StyleKey.h2),
+ 'H3': (editorState) => formatHeading(editorState, StyleKey.h3),
+};
+
+List defaultListToolbarEventNames = [
+ 'Text',
+ 'H1',
+ 'H2',
+ 'H3',
+ // 'B-List',
+ // 'N-List',
+];
+
+class ToolbarWidget extends StatefulWidget {
+ const ToolbarWidget({
+ Key? key,
+ required this.editorState,
+ required this.layerLink,
+ required this.offset,
+ required this.handlers,
+ }) : super(key: key);
+
+ final EditorState editorState;
+ final LayerLink layerLink;
+ final Offset offset;
+ final ToolbarEventHandlers handlers;
+
+ @override
+ State createState() => _ToolbarWidgetState();
+}
+
+class _ToolbarWidgetState extends State {
+ final GlobalKey _listToolbarKey = GlobalKey();
+
+ final toolbarHeight = 32.0;
+ final topPadding = 5.0;
+
+ final listToolbarWidth = 60.0;
+ final listToolbarHeight = 120.0;
+
+ final cornerRadius = 8.0;
+
+ OverlayEntry? _listToolbarOverlay;
+
+ @override
+ void initState() {
+ super.initState();
+
+ widget.editorState.service.selectionService.currentSelectedNodes
+ .addListener(_onSelectionChange);
+ }
+
+ @override
+ void dispose() {
+ widget.editorState.service.selectionService.currentSelectedNodes
+ .removeListener(_onSelectionChange);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Positioned(
+ top: widget.offset.dx,
+ left: widget.offset.dy,
+ child: CompositedTransformFollower(
+ link: widget.layerLink,
+ showWhenUnlinked: true,
+ offset: widget.offset,
+ child: _buildToolbar(context),
+ ),
+ );
+ }
+
+ Widget _buildToolbar(BuildContext context) {
+ return Material(
+ borderRadius: BorderRadius.circular(cornerRadius),
+ color: const Color(0xFF333333),
+ child: SizedBox(
+ height: toolbarHeight,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _listToolbar(context),
+ _centerToolbarIcon('divider', width: 10),
+ _centerToolbarIcon('bold'),
+ _centerToolbarIcon('italic'),
+ _centerToolbarIcon('strikethrough'),
+ _centerToolbarIcon('underline'),
+ _centerToolbarIcon('divider', width: 10),
+ _centerToolbarIcon('quote'),
+ _centerToolbarIcon('number_list'),
+ _centerToolbarIcon('bulleted_list'),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _listToolbar(BuildContext context) {
+ return _centerToolbarIcon(
+ 'quote',
+ key: _listToolbarKey,
+ width: listToolbarWidth,
+ onTap: () => _onTapListToolbar(context),
+ );
+ }
+
+ Widget _centerToolbarIcon(String name,
+ {Key? key, double? width, VoidCallback? onTap}) {
+ return Tooltip(
+ key: key,
+ preferBelow: false,
+ message: name,
+ child: GestureDetector(
+ onTap: onTap ?? () => _onTap(name),
+ child: SizedBox.fromSize(
+ size: width != null
+ ? Size(width, toolbarHeight)
+ : Size.square(toolbarHeight),
+ child: Center(
+ child: FlowySvg(
+ name: 'toolbar/$name',
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _onTapListToolbar(BuildContext context) {
+ // TODO: implement more detailed UI.
+ final items = defaultListToolbarEventNames;
+ final renderBox =
+ _listToolbarKey.currentContext?.findRenderObject() as RenderBox;
+ final offset = renderBox
+ .localToGlobal(Offset.zero)
+ .translate(0, toolbarHeight - cornerRadius);
+ final rect = offset & Size(listToolbarWidth, listToolbarHeight);
+
+ _listToolbarOverlay?.remove();
+ _listToolbarOverlay = OverlayEntry(builder: (context) {
+ return Positioned.fromRect(
+ rect: rect,
+ child: Material(
+ borderRadius: BorderRadius.only(
+ bottomLeft: Radius.circular(cornerRadius),
+ bottomRight: Radius.circular(cornerRadius),
+ ),
+ color: const Color(0xFF333333),
+ child: SingleChildScrollView(
+ child: ListView.builder(
+ itemExtent: toolbarHeight,
+ padding: const EdgeInsets.only(bottom: 10.0),
+ shrinkWrap: true,
+ itemCount: items.length,
+ itemBuilder: ((context, index) {
+ return ListTile(
+ contentPadding: const EdgeInsets.only(
+ left: 3.0,
+ right: 3.0,
+ ),
+ minVerticalPadding: 0.0,
+ title: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(
+ items[index],
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ color: Colors.white,
+ ),
+ ),
+ ),
+ onTap: () {
+ _onTap(items[index]);
+ },
+ );
+ }),
+ ),
+ ),
+ ),
+ );
+ });
+ Overlay.of(context)?.insert(_listToolbarOverlay!);
+ }
+
+ void _onTap(String eventName) {
+ if (defaultToolbarEventHandlers.containsKey(eventName)) {
+ defaultToolbarEventHandlers[eventName]!(widget.editorState);
+ return;
+ }
+ assert(false, 'Could not find the event handler for $eventName');
+ }
+
+ void _onSelectionChange() {
+ _listToolbarOverlay?.remove();
+ _listToolbarOverlay = null;
+ }
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart
new file mode 100644
index 0000000000..79e7bfe077
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart
@@ -0,0 +1,152 @@
+import 'package:flowy_editor/document/attributes.dart';
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/extensions/text_node_extensions.dart';
+import 'package:flowy_editor/operation/transaction_builder.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+
+void formatText(EditorState editorState) {
+ formatTextNodes(editorState, {});
+}
+
+void formatHeading(EditorState editorState, String heading) {
+ formatTextNodes(editorState, {
+ StyleKey.subtype: StyleKey.heading,
+ StyleKey.heading: heading,
+ });
+}
+
+void formatQuote(EditorState editorState) {
+ formatTextNodes(editorState, {
+ StyleKey.subtype: StyleKey.quote,
+ });
+}
+
+void formatCheckbox(EditorState editorState) {
+ formatTextNodes(editorState, {
+ StyleKey.subtype: StyleKey.checkbox,
+ StyleKey.checkbox: false,
+ });
+}
+
+void formatBulletedList(EditorState editorState) {
+ formatTextNodes(editorState, {
+ StyleKey.subtype: StyleKey.bulletedList,
+ });
+}
+
+bool formatTextNodes(EditorState editorState, Attributes attributes) {
+ final nodes = editorState.service.selectionService.currentSelectedNodes.value;
+ final textNodes = nodes.whereType().toList();
+
+ if (textNodes.isEmpty) {
+ return false;
+ }
+
+ final builder = TransactionBuilder(editorState);
+
+ for (final textNode in textNodes) {
+ builder.updateNode(
+ textNode,
+ Attributes.fromIterable(
+ StyleKey.globalStyleKeys,
+ value: (_) => null,
+ )..addAll(attributes),
+ );
+ }
+
+ builder.commit();
+ return true;
+}
+
+bool formatBold(EditorState editorState) {
+ return formatRichTextPartialStyle(editorState, StyleKey.bold);
+}
+
+bool formatItalic(EditorState editorState) {
+ return formatRichTextPartialStyle(editorState, StyleKey.italic);
+}
+
+bool formatUnderline(EditorState editorState) {
+ return formatRichTextPartialStyle(editorState, StyleKey.underline);
+}
+
+bool formatStrikethrough(EditorState editorState) {
+ return formatRichTextPartialStyle(editorState, StyleKey.strikethrough);
+}
+
+bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
+ final selection = editorState.service.selectionService.currentSelection;
+ final nodes = editorState.service.selectionService.currentSelectedNodes.value;
+ final textNodes = nodes.whereType().toList(growable: false);
+
+ if (selection == null || textNodes.isEmpty) {
+ return false;
+ }
+
+ bool value = !textNodes.allSatisfyInSelection(styleKey, selection);
+ Attributes attributes = {
+ styleKey: value,
+ };
+ if (styleKey == StyleKey.underline && value) {
+ attributes[StyleKey.strikethrough] = null;
+ } else if (styleKey == StyleKey.strikethrough && value) {
+ attributes[StyleKey.underline] = null;
+ }
+
+ return formatRichTextStyle(editorState, attributes);
+}
+
+bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
+ final selection = editorState.service.selectionService.currentSelection;
+ final nodes = editorState.service.selectionService.currentSelectedNodes.value;
+ final textNodes = nodes.whereType().toList();
+
+ if (selection == null || textNodes.isEmpty) {
+ return false;
+ }
+
+ final builder = TransactionBuilder(editorState);
+
+ // 1. All nodes are text nodes.
+ // 2. The first node is not TextNode.
+ // 3. The last node is not TextNode.
+ if (nodes.length == textNodes.length && textNodes.length == 1) {
+ builder.formatText(
+ textNodes.first,
+ selection.start.offset,
+ selection.end.offset - selection.start.offset,
+ attributes,
+ );
+ } else {
+ for (var i = 0; i < textNodes.length; i++) {
+ final textNode = textNodes[i];
+ if (i == 0 && textNode == nodes.first) {
+ builder.formatText(
+ textNode,
+ selection.start.offset,
+ textNode.toRawString().length - selection.start.offset,
+ attributes,
+ );
+ } else if (i == textNodes.length - 1 && textNode == nodes.last) {
+ builder.formatText(
+ textNode,
+ 0,
+ selection.end.offset,
+ attributes,
+ );
+ } else {
+ builder.formatText(
+ textNode,
+ 0,
+ textNode.toRawString().length,
+ attributes,
+ );
+ }
+ }
+ }
+
+ builder.commit();
+
+ return true;
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart
new file mode 100644
index 0000000000..b62fe1bb15
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart
@@ -0,0 +1,119 @@
+import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart';
+import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
+import 'package:flutter/material.dart';
+
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/render/editor/editor_entry.dart';
+import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart';
+import 'package:flowy_editor/render/rich_text/checkbox_text.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/heading_text.dart';
+import 'package:flowy_editor/render/rich_text/number_list_text.dart';
+import 'package:flowy_editor/render/rich_text/quoted_text.dart';
+import 'package:flowy_editor/service/input_service.dart';
+import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
+import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
+import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
+import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart';
+import 'package:flowy_editor/service/keyboard_service.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flowy_editor/service/selection_service.dart';
+import 'package:flowy_editor/service/toolbar_service.dart';
+
+NodeWidgetBuilders defaultBuilders = {
+ 'editor': EditorEntryWidgetBuilder(),
+ 'text': RichTextNodeWidgetBuilder(),
+ 'text/checkbox': CheckboxNodeWidgetBuilder(),
+ 'text/heading': HeadingTextNodeWidgetBuilder(),
+ 'text/bulleted-list': BulletedListTextNodeWidgetBuilder(),
+ 'text/number-list': NumberListTextNodeWidgetBuilder(),
+ 'text/quote': QuotedTextNodeWidgetBuilder(),
+};
+
+List defaultKeyEventHandler = [
+ deleteTextHandler,
+ slashShortcutHandler,
+ flowyDeleteNodesHandler,
+ arrowKeysHandler,
+ enterInEdgeOfTextNodeHandler,
+ updateTextStyleByCommandXHandler,
+];
+
+class FlowyEditor extends StatefulWidget {
+ const FlowyEditor({
+ Key? key,
+ required this.editorState,
+ this.customBuilders = const {},
+ this.keyEventHandlers = const [],
+ }) : super(key: key);
+
+ final EditorState editorState;
+
+ /// Render plugins.
+ final NodeWidgetBuilders customBuilders;
+
+ /// Keyboard event handlers.
+ final List keyEventHandlers;
+
+ @override
+ State createState() => _FlowyEditorState();
+}
+
+class _FlowyEditorState extends State {
+ EditorState get editorState => widget.editorState;
+
+ @override
+ void initState() {
+ super.initState();
+
+ editorState.service.renderPluginService = _createRenderPlugin();
+ }
+
+ @override
+ void didUpdateWidget(covariant FlowyEditor oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (editorState.service != oldWidget.editorState.service) {
+ editorState.service.renderPluginService = _createRenderPlugin();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return FlowySelection(
+ key: editorState.service.selectionServiceKey,
+ editorState: editorState,
+ child: FlowyInput(
+ key: editorState.service.inputServiceKey,
+ editorState: editorState,
+ child: FlowyKeyboard(
+ key: editorState.service.keyboardServiceKey,
+ handlers: [
+ ...defaultKeyEventHandler,
+ ...widget.keyEventHandlers,
+ ],
+ editorState: editorState,
+ child: FlowyToolbar(
+ key: editorState.service.toolbarServiceKey,
+ editorState: editorState,
+ child: editorState.service.renderPluginService.buildPluginWidget(
+ NodeWidgetContext(
+ context: context,
+ node: editorState.document.root,
+ editorState: editorState,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin(
+ editorState: editorState,
+ builders: {
+ ...defaultBuilders,
+ ...widget.customBuilders,
+ },
+ );
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart
new file mode 100644
index 0000000000..ee570d902a
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart
@@ -0,0 +1,236 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/operation/transaction_builder.dart';
+
+mixin FlowyInputService {
+ void attach(TextEditingValue textEditingValue);
+ void setTextEditingValue(TextEditingValue textEditingValue);
+ void apply(List deltas);
+ void close();
+}
+
+/// process input
+class FlowyInput extends StatefulWidget {
+ const FlowyInput({
+ Key? key,
+ required this.editorState,
+ required this.child,
+ }) : super(key: key);
+
+ final EditorState editorState;
+ final Widget child;
+
+ @override
+ State