diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 24cd55c6b6..48a347456b 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -6,7 +6,24 @@ # status after issuing an appropriate message if it wants to stop the # commit. The hook is allowed to edit the commit message file. -echo "Running the AppFlowy commit-msg hook." +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + +printMessage "Running the AppFlowy commit-msg hook." # This example catches duplicate Signed-off-by lines. @@ -16,11 +33,19 @@ test "" = "$(grep '^Signed-off-by: ' "$1" | exit 1 } -npx --no -- commitlint --edit $1 +.githooks/gitlint \ + --msg-file=$1 \ + --subject-regex="^(build|chore|ci|docs|feat|feature|fix|perf|refactor|revert|style|test)(.*)?:\s?.*" \ + --subject-maxlen=100 \ + --subject-minlen=10 \ + --body-regex=".*" \ + --body-maxlen=200 \ + --max-parents=1 if [ $? -ne 0 ] then - echo "Please fix your commit message to match AppFlowy coding standards" + printError "Please fix your commit message to match AppFlowy coding standards" + printError "https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides" exit 1 fi diff --git a/.githooks/pre-commit b/.githooks/pre-commit index d27345fe71..be42c93834 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,6 +1,23 @@ #!/usr/bin/env bash -echo "Running local AppFlowy pre-commit hook." +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + +printMessage "Running local AppFlowy pre-commit hook." #flutter format . ##https://gist.github.com/benmccallum/28e4f216d9d72f5965133e6c43aaff6e diff --git a/.githooks/pre-push b/.githooks/pre-push index ad7d19a16e..ee7c8cb79c 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,26 +1,36 @@ #!/usr/bin/env bash -echo "Running local AppFlowy pre-push hook." +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + +printMessage "Running local AppFlowy pre-push hook." if [[ `git status --porcelain` ]]; then - printf "\e[31;1m%s\e[0m\n" 'This script needs to run against committed code only. Please commit or stash you changes.' + printError "This script needs to run against committed code only. Please commit or stash you changes." exit 1 fi -printf "\e[33;1m%s\e[0m\n" 'Running the Flutter analyzer' -flutter analyze - -if [ $? -ne 0 ]; then - printf "\e[31;1m%s\e[0m\n" 'Flutter analyzer error' - exit 1 -fi - -printf "\e[33;1m%s\e[0m\n" 'Finished running the Flutter analyzer' -printf "\e[33;1m%s\e[0m\n" 'Running unit tests' - -#flutter test +# +#printMessage "Running the Flutter analyzer" +#flutter analyze +# #if [ $? -ne 0 ]; then -# printf "\e[31;1m%s\e[0m\n" 'Unit tests error' +# printError "Flutter analyzer error" # exit 1 #fi -#printf "\e[33;1m%s\e[0m\n" 'Finished running unit tests' +# +#printMessage "Finished running the Flutter analyzer" 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/.github/workflows/rust_lint.yml b/.github/workflows/rust_lint.yml index 4f364a616a..83ecd62280 100644 --- a/.github/workflows/rust_lint.yml +++ b/.github/workflows/rust_lint.yml @@ -17,9 +17,13 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 - with: - toolchain: 'stable-2022-01-20' - override: true + with: + toolchain: 'stable-2022-01-20' + override: true + - uses: subosito/flutter-action@v1 + with: + flutter-version: '3.0.0' + channel: "stable" - name: Rust Deps working-directory: frontend @@ -40,7 +44,7 @@ jobs: - run: rustup component add clippy - working-directory: frontend/rust-lib + working-directory: frontend/rust-lib - name: clippy run: cargo clippy --no-default-features working-directory: frontend/rust-lib diff --git a/.github/workflows/translation_notify.yml b/.github/workflows/translation_notify.yml index 8c12ccb0b8..00f1576b6f 100644 --- a/.github/workflows/translation_notify.yml +++ b/.github/workflows/translation_notify.yml @@ -4,15 +4,12 @@ on: branches: [ main ] paths: - "frontend/app_flowy/assets/translations/en.json" - pull_request: - branches: [ main ] - paths: - - "frontend/app_flowy/assets/translations/en.json" + jobs: Discord-Notify: runs-on: ubuntu-latest steps: - - uses: Ilshidur/action-discord@0.3.2 + - uses: Ilshidur/action-discord@master env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} with: diff --git a/.gitignore b/.gitignore index c956a0ad13..3d544c712e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ frontend/.vscode/* # Commit the highest level pubspec.lock, but ignore the others pubspec.lock !frontend/app_flowy/pubspec.lock + +# ignore tool used for commit linting +.githooks/gitlint +.githooks/gitlint.exe diff --git a/README.md b/README.md index 9aa15e40ae..e441496c96 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,15 @@ Please view the [documentation](https://appflowy.gitbook.io/docs/essential-docum ## Stay Up-to-Date -

AppFlowy Github

+

AppFlowy Github - how to star the repo

## Getting Started with development Please view the [documentation](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy) for OS specific development instructions ## Roadmap -[AppFlowy Roadmap](https://trello.com/b/NCyXCXXh/appflowy-roadmap) +- [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap) +- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) If you'd like to propose a feature, submit an issue [here](https://github.com/AppFlowy-IO/appflowy/issues). diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000000..53ac484771 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,3 @@ +## Our [roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) is where you can learn about the features we’re working on, their status, when we expect to release them, and how you can help us. + +## Find more information about how to use our official AppFlowy public roadmap on [Gitbook](https://appflowy.gitbook.io/docs/essential-documentation/roadmap). diff --git a/commitlint.config.js b/commitlint.config.js index c59a378e48..1e1af55752 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -22,3 +22,4 @@ module.exports = { 'footer-max-line-length': [2, 'always', 100] }, }; + diff --git a/doc/roadmap.md b/doc/roadmap.md index b5d7e368c9..54bef7f99b 100644 --- a/doc/roadmap.md +++ b/doc/roadmap.md @@ -1 +1,3 @@ -https://trello.com/b/NCyXCXXh/appflowy-roadmap +[AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap) + +[AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) 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/Makefile.toml b/frontend/Makefile.toml index 7449bfeef3..337b9efd76 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -8,7 +8,6 @@ extend = [ { path = "scripts/makefile/env.toml" }, { path = "scripts/makefile/flutter.toml" }, { path = "scripts/makefile/tool.toml" }, - { path = "scripts/makefile/githooks.toml" }, ] [config] diff --git a/frontend/app_flowy/.gitignore b/frontend/app_flowy/.gitignore index 505fd9136b..e43862d70e 100644 --- a/frontend/app_flowy/.gitignore +++ b/frontend/app_flowy/.gitignore @@ -64,4 +64,5 @@ windows/flutter/dart_ffi/ **/**/*.dll **/**/*.so **/**/Brewfile.lock.json -**/.sandbox \ No newline at end of file +**/.sandbox +**/.vscode/ \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index a6f9b4d3ae..2dbb970611 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -141,6 +141,7 @@ "menu": { "appearance": "Appearance", "language": "Language", + "user": "User", "open": "Open Settings" }, "appearance": { @@ -172,8 +173,8 @@ "includeTime": " Include time", "dateFormatFriendly": "Month Day,Year", "dateFormatISO": "Year-Month-Day", - "dateFormatLocal": "Month/Month/Day", - "dateFormatUS": "Month/Month/Day", + "dateFormatLocal": "Year/Month/Day", + "dateFormatUS": "Year/Month/Day", "timeFormat": " Time format", "invalidTimeFormat": "Invalid format", "timeFormatTwelveHour": "12 hour", @@ -214,4 +215,4 @@ "timeHintTextInTwentyFourHour": "12:00" } } -} +} \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/es-VE.json b/frontend/app_flowy/assets/translations/es-VE.json index 4aabe23674..d3740db8ec 100644 --- a/frontend/app_flowy/assets/translations/es-VE.json +++ b/frontend/app_flowy/assets/translations/es-VE.json @@ -96,6 +96,12 @@ "lightMode": "Cambiar a modo Claro", "darkMode": "Cambiar a modo Oscuro" }, + "notifications": { + "export": { + "markdown": "Nota exportada a Markdown", + "path": "Documentos/flowy" + } + }, "contactsPage": { "title": "Contactos", "whatsHappening": "¿Qué está pasando esta semana?", @@ -120,13 +126,13 @@ "oAuth": { "err": { "failedTitle": "Imposible conectarse con sus credenciales.", - "failedMsg": "Por favor asegurese haber completado el proceso de ingreso en su buscador." + "failedMsg": "Por favor asegurese haber completado el proceso de ingreso en su navegador." }, "google": { "title": "Ingresar con Google", - "instruction1": "Para importar sus contactos de Google, debe autorizar esta aplicación usando su buscador web.", + "instruction1": "Para importar sus contactos de Google, debe autorizar esta aplicación usando su navegador web.", "instruction2": "Copie este código al presionar el icono o al seleccionar el texto:", - "instruction3": "Navege al siguiente enlace en su buscador web, e ingrese el código anterior:", + "instruction3": "Navege al siguiente enlace en su navegador web, e ingrese el código anterior:", "instruction4": "Presione el botón de abajo cuando haya completado su registro:" } }, @@ -141,5 +147,71 @@ "lightLabel": "Modo Claro", "darkLabel": "Modo Oscuro" } + }, + "grid": { + "settings": { + "filter": "Filtrar", + "sortBy": "Ordenar por", + "Properties": "Propiedades" + }, + "field": { + "hide": "Ocultar", + "insertLeft": "Insertar a la Izquierda", + "insertRight": "Insertar a la Derecha", + "duplicate": "Duplicar", + "delete": "Eliminar", + "textFieldName": "Texto", + "checkboxFieldName": "Casilla de verificación", + "dateFieldName": "Fecha", + "numberFieldName": "Números", + "singleSelectFieldName": "Seleccionar", + "multiSelectFieldName": "Selección múltiple", + "urlFieldName": "URL", + "numberFormat": " Formato numérico", + "dateFormat": " Formato de fecha", + "includeTime": " Incluir tiempo", + "dateFormatFriendly": "Mes Día, Año", + "dateFormatISO": "Año-Mes-Día", + "dateFormatLocal": "Año/Mes/Día", + "dateFormatUS": "Año/Mes/Día", + "timeFormat": " Time format", + "invalidTimeFormat": "Formato de tiempo", + "timeFormatTwelveHour": "12 horas", + "timeFormatTwentyFourHour": "24 horas", + "addSelectOption": "Añadir una opción", + "optionTitle": "Opciones", + "addOption": "Añadir opción", + "editProperty": "Editar propiedad" + }, + "row": { + "duplicate": "Duplicar", + "delete": "Eliminar", + "textPlaceholder": "Vacío", + "copyProperty": "Propiedad copiada al portapapeles" + }, + "selectOption": { + "create": "Crear", + "purpleColor": "Morado", + "pinkColor": "Rosa", + "lightPinkColor": "Rosa Claro", + "orangeColor": "Naranja", + "yellowColor": "Amarillo", + "limeColor": "Lima", + "greenColor": "Verde", + "aquaColor": "Agua", + "blueColor": "Azul", + "deleteTag": "Borrar etiqueta", + "colorPannelTitle": "Colores", + "pannelTitle": "Selecciona una opción o crea una", + "searchOption": "Buscar una opción" + }, + "menuName": "Grid" + }, + "document": { + "menuName": "Doc", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } } } diff --git a/frontend/app_flowy/assets/translations/fr-FR.json b/frontend/app_flowy/assets/translations/fr-FR.json index c05c9aba27..ed1d56bce8 100644 --- a/frontend/app_flowy/assets/translations/fr-FR.json +++ b/frontend/app_flowy/assets/translations/fr-FR.json @@ -68,7 +68,7 @@ "help": "Aide et Support", "debug": { "name": "Informations de Débogage", - "success": "Informations de Débogage copiées dans le presse-papiers!", + "success": "Informations de Débogage copiées dans le presse-papiers !", "fail": "Impossible de copier les informations de Débogage dans le presse-papiers" } }, diff --git a/frontend/app_flowy/assets/translations/id-ID.json b/frontend/app_flowy/assets/translations/id-ID.json new file mode 100644 index 0000000000..cbe721c2b8 --- /dev/null +++ b/frontend/app_flowy/assets/translations/id-ID.json @@ -0,0 +1,218 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Saya", + "welcomeText": "Selamat datang di @:appName", + "githubStarText": "Bintangi GitHub", + "subscribeNewsletterText": "Berlangganan buletin", + "letsGoButtonText": "Ayo", + "title": "Judul", + "signUp": { + "buttonText": "Daftar", + "title": "Daftar ke @:appName", + "getStartedText": "Mulai", + "emptyPasswordError": "Sandi tidak boleh kosong", + "repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong", + "unmatchedPasswordError": "Sandi ulang tidak sama dengan sandi", + "alreadyHaveAnAccount": "Sudah punya akun?", + "emailHint": "Email", + "passwordHint": "Sandi", + "repeatPasswordHint": "Sandi ulang" + }, + "signIn": { + "loginTitle": "Masuk ke @:appName", + "loginButtonText": "Masuk", + "buttonText": "Masuk", + "forgotPassword": "Lupa Sandi?", + "emailHint": "Email", + "passwordHint": "Sandi", + "dontHaveAnAccount": "Belum punya akun?", + "repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong", + "unmatchedPasswordError": "Sandi ulang tidak sama dengan sandi" + }, + "workspace": { + "create": "Buat workspace", + "hint": "workspace", + "notFoundError": "Workspace tidak ditemukan" + }, + "shareAction": { + "buttonText": "Bagikan", + "workInProgress": "Segera", + "markdown": "Markdown", + "copyLink": "Salin tautan" + }, + "disclosureAction": { + "rename": "Ganti nama", + "delete": "Hapus", + "duplicate": "Duplikat" + }, + "blankPageTitle": "Halaman kosong", + "newPageText": "Halaman baru", + "trash": { + "text": "Sampah", + "restoreAll": "Pulihkan Semua", + "deleteAll": "Hapus semua", + "pageHeader": { + "fileName": "Nama file", + "lastModified": "Terakhir diubah", + "created": "Dibuat" + } + }, + "deletePagePrompt": { + "text": "Halaman ini di tempat sampah", + "restore": "Pulihkan halaman", + "deletePermanent": "Hapus secara permanen" + }, + "dialogCreatePageNameHint": "Nama halaman", + "questionBubble": { + "whatsNew": "Apa yang baru?", + "help": "Bantuan & Dukungan", + "debug": { + "name": "Info debug", + "success": "Info debug disalin ke papan klip!", + "fail": "Tidak dapat menyalin info debug ke papan klip" + } + }, + "menuAppHeader": { + "addPageTooltip": "Menambahkan halaman di dalam dengan cepat", + "defaultNewPageName": "Tanpa Judul", + "renameDialog": "Ganti nama" + }, + "toolbar": { + "undo": "Undo", + "redo": "Redo", + "bold": "Tebal", + "italic": "Miring", + "underline": "Garis bawah", + "strike": "Dicoret", + "numList": "Daftar bernomor", + "bulletList": "Daftar berpoin", + "checkList": "Daftar periksa", + "inlineCode": "Kode sebaris", + "quote": "Blok kutipan", + "header": "Tajuk", + "highlight": "Sorotan" + }, + "tooltip": { + "lightMode": "Ganti mode terang", + "darkMode": "Ganti mode gelap" + }, + "notifications": { + "export": { + "markdown": "Mengekspor Catatan ke Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "Kontak", + "whatsHappening": "Apa yang terjadi minggu ini?", + "addContact": "Tambahkan Kontak", + "editContact": "Ubah Kontak" + }, + "button": { + "OK": "Ya", + "Cancel": "Batal", + "signIn": "Masuk", + "signOut": "Keluar", + "complete": "Selesai", + "save": "Simpan" + }, + "label": { + "welcome": "Selamat datang!", + "firstName": "Nama Depan", + "middleName": "Nama Tengah", + "lastName": "Nama Akhir", + "stepX": "Langkah {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Tidak dapat terhubung ke akun anda", + "failedMsg": "Mohon pastikan anda menyelesaikan proses pendaftaran pada browser anda." + }, + "google": { + "title": "MASUK GOOGLE", + "instruction1": "Untuk mengimpor kontak Google Contacts anda, anda harus mengizinkan aplikasi ini menggunakan browser web anda.", + "instruction2": "Salin kode ini ke papan klip anda dengan cara mengklik ikon atau memilih teks:", + "instruction3": "Arahkan ke tautan berikut di browser web Anda, dan masukkan kode di atas:", + "instruction4": "Tekan tombol di bawah ini setelah Anda menyelesaikan pendaftaran:" + } + }, + "settings": { + "title": "Pengaturan", + "menu": { + "appearance": "Tampilan", + "language": "Bahasa", + "user": "Pengguna", + "open": "Buka Pengaturan" + }, + "appearance": { + "lightLabel": "Mode Terang", + "darkLabel": "Mode Gelap" + } + }, + "grid": { + "settings": { + "filter": "Filter", + "sortBy": "Sortir dengan", + "Properties": "Properti" + }, + "field": { + "hide": "Sembunyikan", + "insertLeft": "Sisipkan Kiri", + "insertRight": "Sisipkan Kanan", + "duplicate": "Duplikasi", + "delete": "Hapus", + "textFieldName": "Teks", + "checkboxFieldName": "Kotak Centang", + "dateFieldName": "Tanggal", + "numberFieldName": "Angka", + "singleSelectFieldName": "seleksi", + "multiSelectFieldName": "Multi seleksi", + "urlFieldName": "URL", + "numberFormat": " Format angka", + "dateFormat": " Format tanggal", + "includeTime": " Sertakan waktu", + "dateFormatFriendly": "Bulan Hari,Tahun", + "dateFormatISO": "Tahun-Bulan-Hari", + "dateFormatLocal": "Tahun/Bulan/Hari", + "dateFormatUS": "Tahun/Bulan/Hari", + "timeFormat": " Format waktu", + "invalidTimeFormat": "Format yang tidak valid", + "timeFormatTwelveHour": "12 jam", + "timeFormatTwentyFourHour": "24 jam", + "addSelectOption": "Tambahkan opsi", + "optionTitle": "Opsi", + "addOption": "Tambahkan opsi", + "editProperty": "Ubah properti" + }, + "row": { + "duplicate": "Duplikasi", + "delete": "Hapus", + "textPlaceholder": "Kosong", + "copyProperty": "Salin properti ke papan klip" + }, + "selectOption": { + "create": "Buat", + "purpleColor": "Ungu", + "pinkColor": "Merah Jambu", + "lightPinkColor": "Merah Jambu Muda", + "orangeColor": "Oranye", + "yellowColor": "Kuning", + "limeColor": "Limau", + "greenColor": "Hijau", + "aquaColor": "Air", + "blueColor": "Biru", + "deleteTag": "Hapus tag", + "colorPannelTitle": "Warna", + "pannelTitle": "Pilih opsi atau buat baru", + "searchOption": "Cari opsi" + }, + "menuName": "Grid" + }, + "document": { + "menuName": "Doc", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } + } +} \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/pl-PL.json b/frontend/app_flowy/assets/translations/pl-PL.json new file mode 100644 index 0000000000..0105e7aec7 --- /dev/null +++ b/frontend/app_flowy/assets/translations/pl-PL.json @@ -0,0 +1,145 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Ja", + "welcomeText": "Witaj w @:appName", + "githubStarText": "Gwiazdka na GitHub-ie", + "subscribeNewsletterText": "Zapisz się do naszego Newslettera", + "letsGoButtonText": "Start!", + "title": "Tytuł", + "signUp": { + "buttonText": "Zarejestruj", + "title": "Zarejestruj się w @:appName", + "getStartedText": "Zaczynamy", + "emptyPasswordError": "Hasło nie moze być puste", + "repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste", + "unmatchedPasswordError": "Hasła nie są takie same", + "alreadyHaveAnAccount": "Masz juz konto?", + "emailHint": "Email", + "passwordHint": "Hasło", + "repeatPasswordHint": "Powtórz hasło" + }, + "signIn": { + "loginTitle": "Zaloguj do @:appName", + "loginButtonText": "Logowanie", + "buttonText": "Zaloguj", + "forgotPassword": "Zapomniałem hasła?", + "emailHint": "Email", + "passwordHint": "Password", + "dontHaveAnAccount": "Nie masz konta?", + "repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste", + "unmatchedPasswordError": "Hasła nie są takie same" + }, + "workspace": { + "create": "Utwórz przestrzeń", + "hint": "przestrzeń robocza", + "notFoundError": "Przestrzeni nie znaleziono" + }, + "shareAction": { + "buttonText": "Udostępnij", + "workInProgress": "Wkrótce", + "markdown": "Markdown", + "copyLink": "Skopiuj link" + }, + "disclosureAction": { + "rename": "Zmień nazwę", + "delete": "Usuń", + "duplicate": "Duplikuj" + }, + "blankPageTitle": "Pusta strona", + "newPageText": "Nowa strona", + "trash": { + "text": "Kosz", + "restoreAll": "Przywróć Wszystko", + "deleteAll": "Usuń Wszystko", + "pageHeader": { + "fileName": "Nazwa Pliku", + "lastModified": "Ostatnio Zmodyfikowano", + "created": "Utworzono" + } + }, + "deletePagePrompt": { + "text": "Ta strona jest w Koszu", + "restore": "Przywróć strone", + "deletePermanent": "Usuń bezpowrotnie" + }, + "dialogCreatePageNameHint": "Nazwa Strony", + "questionBubble": { + "whatsNew": "What's new?", + "help": "Pomoc & Wsparcie", + "debug": { + "name": "Informacje Debugowania", + "success": "Skopiowano informacje debugowania do schowka!", + "fail": "Nie mozna skopiować informacji debugowania do schowka" + } + }, + "menuAppHeader": { + "addPageTooltip": "Szybko dodaj stronę do środka", + "defaultNewPageName": "Brak tytułu", + "renameDialog": "Zmień nazwę" + }, + "toolbar": { + "undo": "Cofnij", + "redo": "Powtórz", + "bold": "Pogrubiony", + "italic": "Kursywa", + "underline": "Podkreśl", + "strike": "Przekreśl", + "numList": "Lista Numerowana", + "bulletList": "Lista Punktowana", + "checkList": "Lista Kontrolna", + "inlineCode": "Kod Wbudowany", + "quote": "Blok cytat", + "header": "Nagłówek", + "highlight": "Podświetl" + }, + "tooltip": { + "lightMode": "Przełącz w Tryb Jasny", + "darkMode": "Przełącz w Tryb Ciemny" + }, + "contactsPage": { + "title": "Kontakty", + "whatsHappening": "Co się dzieje w tym tygodniu?", + "addContact": "Dodaj Kontakt", + "editContact": "Edytuj Kontakt" + }, + "button": { + "OK": "OK", + "Cancel": "Anuluj", + "signIn": "Zaloguj", + "signOut": "Wyloguj", + "complete": "Zakończono", + "save": "Zapisz" + }, + "label": { + "welcome": "Witaj!", + "firstName": "Imię Pierwsze", + "middleName": "Imię Drugie", + "lastName": "Nazwisko", + "stepX": "Krok {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Nie można połączyć się z Twoim kontem.", + "failedMsg": "Upewnij się, że zakończyłeś proces logowania w przeglądarce." + }, + "google": { + "title": "LOGOWANIE GOOGLE", + "instruction1": "Aby zaimportować Kontakty Google, musisz autoryzować tę aplikację za pomocą przeglądarki internetowej.", + "instruction2": "Skopiuj ten kod do schowka, klikając ikonę lub zaznaczając tekst:", + "instruction3": "Przejdź do następującego linku w przeglądarce internetowej i wprowadź powyższy kod:", + "instruction4": "Naciśnij poniższy przycisk po zakończeniu rejestracji:" + } + }, + "settings": { + "title": "Ustawienia", + "menu": { + "appearance": "Wygląd", + "language": "Język", + "open": "Otwórz Ustawienia" + }, + "appearance": { + "lightLabel": "Tryb Jasny", + "darkLabel": "Tryb Ciemny" + } + } +} diff --git a/frontend/app_flowy/assets/translations/zh-TW.json b/frontend/app_flowy/assets/translations/zh-TW.json new file mode 100644 index 0000000000..9f72b90512 --- /dev/null +++ b/frontend/app_flowy/assets/translations/zh-TW.json @@ -0,0 +1,218 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "我", + "welcomeText": "歡迎使用 @:appName", + "githubStarText": "在 GitHub 點星", + "subscribeNewsletterText": "訂閱電子報", + "letsGoButtonText": "出發吧", + "title": "標題", + "signUp": { + "buttonText": "註冊", + "title": "註冊 @:appName", + "getStartedText": "開始使用", + "emptyPasswordError": "密碼不能為空", + "repeatPasswordEmptyError": "確認密碼不能為空", + "unmatchedPasswordError": "確認密碼與密碼不符", + "alreadyHaveAnAccount": "已經有帳號了嗎?", + "emailHint": "電子郵件地址", + "passwordHint": "密碼", + "repeatPasswordHint": "確認密碼" + }, + "signIn": { + "loginTitle": "登入 @:appName", + "loginButtonText": "登入", + "buttonText": "登入", + "forgotPassword": "忘記密碼?", + "emailHint": "電子郵件地址", + "passwordHint": "密碼", + "dontHaveAnAccount": "沒有帳號?", + "repeatPasswordEmptyError": "確認密碼不能為空", + "unmatchedPasswordError": "確認密碼與密碼不符" + }, + "workspace": { + "create": "建立工作區", + "hint": "工作區", + "notFoundError": "找不到工作區" + }, + "shareAction": { + "buttonText": "分享", + "workInProgress": "即將推出", + "markdown": "Markdown", + "copyLink": "複製連結" + }, + "disclosureAction": { + "rename": "重新命名", + "delete": "刪除", + "duplicate": "複製" + }, + "blankPageTitle": "空白頁面", + "newPageText": "新頁面", + "trash": { + "text": "垃圾筒", + "restoreAll": "全部復原", + "deleteAll": "全部刪除", + "pageHeader": { + "fileName": "檔案名稱", + "lastModified": "最後修改時間", + "created": "建立時間" + } + }, + "deletePagePrompt": { + "text": "此頁面在垃圾筒中", + "restore": "復原頁面", + "deletePermanent": "永久刪除" + }, + "dialogCreatePageNameHint": "頁面名稱", + "questionBubble": { + "whatsNew": "新功能", + "help": "幫助 & 支援", + "debug": { + "name": "除錯資訊", + "success": "已將除錯資訊複製至剪貼簿!", + "fail": "無法將除錯資訊複製至剪貼簿" + } + }, + "menuAppHeader": { + "addPageTooltip": "快速新增頁面", + "defaultNewPageName": "未命名", + "renameDialog": "重新命名" + }, + "toolbar": { + "undo": "復原", + "redo": "取消復原", + "bold": "粗體", + "italic": "斜體", + "underline": "底線", + "strike": "刪除線", + "numList": "有序清單", + "bulletList": "無序清單", + "checkList": "核取清單", + "inlineCode": "程式碼", + "quote": "區塊引言", + "header": "標題", + "highlight": "反白" + }, + "tooltip": { + "lightMode": "切換至亮色模式", + "darkMode": "切換至暗色模式" + }, + "notifications": { + "export": { + "markdown": "已將筆記匯出成 Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "聯絡人", + "whatsHappening": "這周有甚麼新鮮事?", + "addContact": "新增聯絡人", + "editContact": "編輯聯絡人" + }, + "button": { + "OK": "OK", + "Cancel": "取消", + "signIn": "登入", + "signOut": "登出", + "complete": "完成", + "save": "儲存" + }, + "label": { + "welcome": "歡迎!", + "firstName": "名", + "middleName": "中間名", + "lastName": "姓", + "stepX": "步驟 {X}" + }, + "oAuth": { + "err": { + "failedTitle": "無法連接至您的帳號。", + "failedMsg": "請確認您已在瀏覽器中完成登入程序:" + }, + "google": { + "title": "GOOGLE 登入", + "instruction1": "若要匯入您的 Google 聯絡人,您必須透過瀏覽器授權此應用程式:", + "instruction2": "點擊圖示或選取文字以複製代碼:", + "instruction3": "前往下列網址,並輸入上述代碼:", + "instruction4": "完成註冊後,請點擊下方按鈕:" + } + }, + "settings": { + "title": "設定", + "menu": { + "appearance": "外觀", + "language": "語言", + "user": "使用者", + "open": "開啟設定" + }, + "appearance": { + "lightLabel": "亮色模式", + "darkLabel": "暗色模式" + } + }, + "grid": { + "settings": { + "filter": "篩選", + "sortBy": "排序方式", + "Properties": "內容" + }, + "field": { + "hide": "隱藏", + "insertLeft": "插入左方欄", + "insertRight": "插入右方欄", + "duplicate": "複製", + "delete": "刪除", + "textFieldName": "文字", + "checkboxFieldName": "核取方塊", + "dateFieldName": "日期", + "numberFieldName": "數字", + "singleSelectFieldName": "單選", + "multiSelectFieldName": "多選", + "urlFieldName": "網址", + "numberFormat": " 數字格式", + "dateFormat": " 日期格式", + "includeTime": " 包含時間", + "dateFormatFriendly": "月 日,年", + "dateFormatISO": "年-月-日", + "dateFormatLocal": "年/月/日", + "dateFormatUS": "年/月/日", + "timeFormat": " 時間格式", + "invalidTimeFormat": "格式無效", + "timeFormatTwelveHour": "12 小時", + "timeFormatTwentyFourHour": "24 小時", + "addSelectOption": "新增選項", + "optionTitle": "選項", + "addOption": "新增選項", + "editProperty": "編輯內容" + }, + "row": { + "duplicate": "複製", + "delete": "刪除", + "textPlaceholder": "空", + "copyProperty": "已將內容複製至剪貼簿" + }, + "selectOption": { + "create": "建立", + "purpleColor": "紫色", + "pinkColor": "粉色", + "lightPinkColor": "淡粉色", + "orangeColor": "橘色", + "yellowColor": "黃色", + "limeColor": "萊姆色", + "greenColor": "綠色", + "aquaColor": "水藍色", + "blueColor": "藍色", + "deleteTag": "刪除標籤", + "colorPannelTitle": "顏色", + "pannelTitle": "搜尋或建立選項", + "searchOption": "搜尋選項" + }, + "menuName": "網格" + }, + "document": { + "menuName": "檔案", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } + } +} \ No newline at end of file diff --git a/frontend/app_flowy/lib/startup/deps_resolver.dart b/frontend/app_flowy/lib/startup/deps_resolver.dart index 1864a2aa62..e0ab40eea7 100644 --- a/frontend/app_flowy/lib/startup/deps_resolver.dart +++ b/frontend/app_flowy/lib/startup/deps_resolver.dart @@ -5,10 +5,12 @@ import 'package:app_flowy/workspace/application/app/prelude.dart'; import 'package:app_flowy/workspace/application/doc/prelude.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart'; import 'package:app_flowy/workspace/application/trash/prelude.dart'; +import 'package:app_flowy/workspace/application/user/prelude.dart'; import 'package:app_flowy/workspace/application/workspace/prelude.dart'; import 'package:app_flowy/workspace/application/edit_pannel/edit_pannel_bloc.dart'; import 'package:app_flowy/workspace/application/view/prelude.dart'; import 'package:app_flowy/workspace/application/menu/prelude.dart'; +import 'package:app_flowy/workspace/application/settings/prelude.dart'; import 'package:app_flowy/user/application/prelude.dart'; import 'package:app_flowy/user/presentation/router.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; @@ -101,6 +103,16 @@ void _resolveFolderDeps(GetIt getIt) { (user, _) => MenuUserBloc(user), ); + //Settings + getIt.registerFactoryParam( + (user, _) => SettingsDialogBloc(user), + ); + + //User + getIt.registerFactoryParam( + (user, _) => SettingsUserViewBloc(user), + ); + // AppPB getIt.registerFactoryParam( (app, _) => AppBloc( diff --git a/frontend/app_flowy/lib/startup/tasks/app_widget.dart b/frontend/app_flowy/lib/startup/tasks/app_widget.dart index c8addc9d6f..6dae04a1c0 100644 --- a/frontend/app_flowy/lib/startup/tasks/app_widget.dart +++ b/frontend/app_flowy/lib/startup/tasks/app_widget.dart @@ -36,8 +36,10 @@ class InitAppWidgetTask extends LaunchTask { Locale('fr', 'FR'), Locale('fr', 'CA'), Locale('hu', 'HU'), + Locale('id', 'ID'), Locale('it', 'IT'), Locale('ja', 'JP'), + Locale('pl', 'PL'), Locale('pt', 'BR'), Locale('ru', 'RU'), Locale('tr', 'TR'), diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart index a38a771158..71927bae14 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart @@ -58,8 +58,8 @@ class DateCellDataPersistence implements IGridCellDataPersistence } } -GridCellIdentifierPayloadPB _makeCellIdPayload(GridCellIdentifier cellId) { - return GridCellIdentifierPayloadPB.create() +GridCellIdPB _makeCellIdPayload(GridCellIdentifier cellId) { + return GridCellIdPB.create() ..gridId = cellId.gridId ..fieldId = cellId.fieldId ..rowId = cellId.rowId; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart index 3e8746c20a..47cd67a55f 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart @@ -46,7 +46,7 @@ class CellService { Future> getCell({ required GridCellIdentifier cellId, }) { - final payload = GridCellIdentifierPayloadPB.create() + final payload = GridCellIdPB.create() ..gridId = cellId.gridId ..fieldId = cellId.fieldId ..rowId = cellId.rowId; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart index 54ac384267..b6966a3e4e 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart @@ -19,7 +19,7 @@ class SelectOptionService { (result) { return result.fold( (option) { - final cellIdentifier = GridCellIdentifierPayloadPB.create() + final cellIdentifier = GridCellIdPB.create() ..gridId = gridId ..fieldId = fieldId ..rowId = rowId; @@ -54,7 +54,7 @@ class SelectOptionService { } Future> getOpitonContext() { - final payload = GridCellIdentifierPayloadPB.create() + final payload = GridCellIdPB.create() ..gridId = gridId ..fieldId = fieldId ..rowId = rowId; @@ -76,8 +76,8 @@ class SelectOptionService { return GridEventUpdateSelectOptionCell(payload).send(); } - GridCellIdentifierPayloadPB _cellIdentifier() { - return GridCellIdentifierPayloadPB.create() + GridCellIdPB _cellIdentifier() { + return GridCellIdPB.create() ..gridId = gridId ..fieldId = fieldId ..rowId = rowId; diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart b/frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart index a0c6fc7d21..9274770b21 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart @@ -103,7 +103,7 @@ class FieldService { } Future> deleteField() { - final payload = GridFieldIdentifierPayloadPB.create() + final payload = DeleteFieldPayloadPB.create() ..gridId = gridId ..fieldId = fieldId; @@ -111,7 +111,7 @@ class FieldService { } Future> duplicateField() { - final payload = GridFieldIdentifierPayloadPB.create() + final payload = DuplicateFieldPayloadPB.create() ..gridId = gridId ..fieldId = fieldId; @@ -121,7 +121,7 @@ class FieldService { Future> getFieldTypeOptionData({ required FieldType fieldType, }) { - final payload = EditFieldPayloadPB.create() + final payload = GridFieldTypeOptionIdPB.create() ..gridId = gridId ..fieldId = fieldId ..fieldType = fieldType; @@ -165,7 +165,7 @@ class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader { @override Future> load() { - final payload = EditFieldPayloadPB.create() + final payload = CreateFieldPayloadPB.create() ..gridId = gridId ..fieldType = FieldType.RichText; @@ -185,7 +185,7 @@ class FieldTypeOptionLoader extends IFieldTypeOptionLoader { @override Future> load() { - final payload = EditFieldPayloadPB.create() + final payload = GridFieldTypeOptionIdPB.create() ..gridId = gridId ..fieldId = field.id ..fieldType = field.fieldType; diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart index b45cc6f869..fca6995b3f 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart @@ -21,13 +21,10 @@ class TypeOptionService { Future> newOption({ required String name, }) { - final fieldIdentifier = GridFieldIdentifierPayloadPB.create() - ..gridId = gridId - ..fieldId = fieldId; - final payload = CreateSelectOptionPayloadPB.create() ..optionName = name - ..fieldIdentifier = fieldIdentifier; + ..gridId = gridId + ..fieldId = fieldId; return GridEventNewSelectOption(payload).send(); } diff --git a/frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart b/frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart index f8f9c7ee3c..893ebb719b 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart @@ -191,7 +191,7 @@ class GridRowCache { } Future _loadRow(String rowId) async { - final payload = GridRowIdPayloadPB.create() + final payload = GridRowIdPB.create() ..gridId = gridId ..blockId = block.id ..rowId = rowId; @@ -297,7 +297,7 @@ class RowService { } Future> getRow() { - final payload = GridRowIdPayloadPB.create() + final payload = GridRowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; @@ -306,7 +306,7 @@ class RowService { } Future> deleteRow() { - final payload = GridRowIdPayloadPB.create() + final payload = GridRowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; @@ -315,7 +315,7 @@ class RowService { } Future> duplicateRow() { - final payload = GridRowIdPayloadPB.create() + final payload = GridRowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; diff --git a/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart b/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart index a4ddef0945..af637ea1c7 100644 --- a/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart @@ -16,42 +16,48 @@ class HomeBloc extends Bloc { HomeBloc(UserProfilePB user, CurrentWorkspaceSettingPB workspaceSetting) : _listener = UserWorkspaceListener(userProfile: user), super(HomeState.initial(workspaceSetting)) { - on((event, emit) async { - await event.map( - initial: (_Initial value) { - _listener.start( - onAuthChanged: (result) => _authDidChanged(result), - onSettingUpdated: (result) { - result.fold( - (setting) => add(HomeEvent.didReceiveWorkspaceSetting(setting)), - (r) => Log.error(r), - ); - }, - ); - }, - showLoading: (e) async { - emit(state.copyWith(isLoading: e.isLoading)); - }, - setEditPannel: (e) async { - emit(state.copyWith(pannelContext: some(e.editContext))); - }, - dismissEditPannel: (value) async { - emit(state.copyWith(pannelContext: none())); - }, - forceCollapse: (e) async { - emit(state.copyWith(forceCollapse: e.forceCollapse)); - }, - didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { - emit(state.copyWith(workspaceSetting: value.setting)); - }, - unauthorized: (_Unauthorized value) { - emit(state.copyWith(unauthorized: true)); - }, - collapseMenu: (e) { - emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed)); - }, - ); - }); + on( + (event, emit) async { + await event.map( + initial: (_Initial value) { + _listener.start( + onAuthChanged: (result) => _authDidChanged(result), + onSettingUpdated: (result) { + result.fold( + (setting) => add(HomeEvent.didReceiveWorkspaceSetting(setting)), + (r) => Log.error(r), + ); + }, + ); + }, + showLoading: (e) async { + emit(state.copyWith(isLoading: e.isLoading)); + }, + setEditPannel: (e) async { + emit(state.copyWith(pannelContext: some(e.editContext))); + }, + dismissEditPannel: (value) async { + emit(state.copyWith(pannelContext: none())); + }, + forceCollapse: (e) async { + emit(state.copyWith(forceCollapse: e.forceCollapse)); + }, + didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { + emit(state.copyWith(workspaceSetting: value.setting)); + }, + unauthorized: (_Unauthorized value) { + emit(state.copyWith(unauthorized: true)); + }, + collapseMenu: (e) { + emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed)); + }, + editPannelResized: (e) { + final newOffset = (state.resizeOffset + e.offset).clamp(-50, 200).toDouble(); + emit(state.copyWith(resizeOffset: newOffset)); + }, + ); + }, + ); } @override @@ -79,6 +85,7 @@ class HomeEvent with _$HomeEvent { const factory HomeEvent.didReceiveWorkspaceSetting(CurrentWorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting; const factory HomeEvent.unauthorized(String msg) = _Unauthorized; const factory HomeEvent.collapseMenu() = _CollapseMenu; + const factory HomeEvent.editPannelResized(double offset) = _EditPannelResized; } @freezed @@ -90,6 +97,7 @@ class HomeState with _$HomeState { required CurrentWorkspaceSettingPB workspaceSetting, required bool unauthorized, required bool isMenuCollapsed, + required double resizeOffset, }) = _HomeState; factory HomeState.initial(CurrentWorkspaceSettingPB workspaceSetting) => HomeState( @@ -99,5 +107,6 @@ class HomeState with _$HomeState { workspaceSetting: workspaceSetting, unauthorized: false, isMenuCollapsed: false, + resizeOffset: 0, ); } diff --git a/frontend/app_flowy/lib/workspace/application/settings/prelude.dart b/frontend/app_flowy/lib/workspace/application/settings/prelude.dart new file mode 100644 index 0000000000..3917b54aaf --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/settings/prelude.dart @@ -0,0 +1 @@ +export 'settings_dialog_bloc.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart new file mode 100644 index 0000000000..3c40f767b1 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -0,0 +1,67 @@ +import 'package:app_flowy/user/application/user_listener.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +part 'settings_dialog_bloc.freezed.dart'; + +class SettingsDialogBloc extends Bloc { + final UserListener _userListener; + final UserProfilePB userProfile; + + SettingsDialogBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + super(SettingsDialogState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + setViewIndex: (int viewIndex) { + emit(state.copyWith(viewIndex: viewIndex)); + }, + ); + }); + } + + @override + Future close() async { + await _userListener.stop(); + super.close(); + } + + void _profileUpdated(Either userProfileOrFailed) { + userProfileOrFailed.fold( + (newUserProfile) => add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); + } +} + +@freezed +class SettingsDialogEvent with _$SettingsDialogEvent { + const factory SettingsDialogEvent.initial() = _Initial; + const factory SettingsDialogEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; + const factory SettingsDialogEvent.setViewIndex(int index) = _SetViewIndex; +} + +@freezed +class SettingsDialogState with _$SettingsDialogState { + const factory SettingsDialogState({ + required UserProfilePB userProfile, + required Either successOrFailure, + required int viewIndex, + }) = _SettingsDialogState; + + factory SettingsDialogState.initial(UserProfilePB userProfile) => SettingsDialogState( + userProfile: userProfile, + successOrFailure: left(unit), + viewIndex: 0, + ); +} diff --git a/frontend/app_flowy/lib/workspace/application/user/prelude.dart b/frontend/app_flowy/lib/workspace/application/user/prelude.dart new file mode 100644 index 0000000000..f698497db9 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/user/prelude.dart @@ -0,0 +1 @@ +export 'settings_user_bloc.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart new file mode 100644 index 0000000000..7435778471 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart @@ -0,0 +1,79 @@ +import 'package:app_flowy/user/application/user_listener.dart'; +import 'package:app_flowy/user/application/user_service.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +part 'settings_user_bloc.freezed.dart'; + +class SettingsUserViewBloc extends Bloc { + final UserService _userService; + final UserListener _userListener; + final UserProfilePB userProfile; + + SettingsUserViewBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + _userService = UserService(userId: userProfile.id), + super(SettingsUserState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + await _initUser(); + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + updateUserName: (String name) { + _userService.updateUserProfile(name: name).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + ); + }); + } + + @override + Future close() async { + await _userListener.stop(); + super.close(); + } + + Future _initUser() async { + final result = await _userService.initUser(); + result.fold((l) => null, (error) => Log.error(error)); + } + + void _profileUpdated(Either userProfileOrFailed) { + userProfileOrFailed.fold( + (newUserProfile) => add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); + } +} + +@freezed +class SettingsUserEvent with _$SettingsUserEvent { + const factory SettingsUserEvent.initial() = _Initial; + const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; + const factory SettingsUserEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; +} + +@freezed +class SettingsUserState with _$SettingsUserState { + const factory SettingsUserState({ + required UserProfilePB userProfile, + required Either successOrFailure, + }) = _SettingsUserState; + + factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( + userProfile: userProfile, + successOrFailure: left(unit), + ); +} diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart index 0405279851..e7e02b9767 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart @@ -27,6 +27,8 @@ class HomeLayout { menuWidth = Sizes.sideBarLg; } + menuWidth += homeBlocState.resizeOffset; + if (forceCollapse) { showMenu = false; } else { diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart index d1d5321848..abe89c14cb 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart @@ -7,6 +7,7 @@ import 'package:flowy_sdk/log.dart'; import 'package:flowy_infra_ui/style_widget/container.dart'; import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -87,6 +88,7 @@ class _HomeScreenState extends State { context: context, state: state, ); + final homeMenuResizer = _buildHomeMenuResizer(context: context); final editPannel = _buildEditPannel( homeState: state, layout: layout, @@ -99,6 +101,7 @@ class _HomeScreenState extends State { homeMenu: menu, editPannel: editPannel, bubble: bubble, + homeMenuResizer: homeMenuResizer, ); }, ); @@ -150,12 +153,31 @@ class _HomeScreenState extends State { ); } + Widget _buildHomeMenuResizer({ + required BuildContext context, + }) { + return MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: GestureDetector( + dragStartBehavior: DragStartBehavior.down, + onPanUpdate: ((details) { + context.read().add(HomeEvent.editPannelResized(details.delta.dx)); + }), + behavior: HitTestBehavior.translucent, + child: SizedBox( + width: 10, + height: MediaQuery.of(context).size.height, + )), + ); + } + Widget _layoutWidgets({ required HomeLayout layout, required Widget homeMenu, required Widget homeStack, required Widget editPannel, required Widget bubble, + required Widget homeMenuResizer, }) { return Stack( children: [ @@ -170,6 +192,7 @@ class _HomeScreenState extends State { .constrained(minWidth: 500) .positioned(left: layout.homePageLOffset, right: layout.homePageROffset, bottom: 0, top: 0, animate: true) .animate(layout.animDuration, Curves.easeOut), + homeMenuResizer.positioned(left: layout.homePageLOffset - 5), bubble .positioned( right: 20, diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index 6c9ffdece2..d7b8dce4af 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -67,6 +67,7 @@ class MenuUser extends StatelessWidget { Widget _renderSettingsButton(BuildContext context) { final theme = context.watch(); + final userProfile = context.read().state.userProfile; return Tooltip( message: LocaleKeys.settings_menu_open.tr(), child: IconButton( @@ -74,7 +75,7 @@ class MenuUser extends StatelessWidget { showDialog( context: context, builder: (context) { - return const SettingsDialog(); + return SettingsDialog(userProfile); }, ); }, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart index 82b13867a4..4aae99acbe 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart @@ -1,9 +1,11 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/grid_bloc.dart'; import 'package:app_flowy/workspace/application/grid/row/row_service.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -250,6 +252,8 @@ class _GridFooter extends StatelessWidget { @override Widget build(BuildContext context) { + final rowCount = context.watch().state.rowInfos.length; + final theme = context.watch(); return SliverPadding( padding: const EdgeInsets.only(bottom: 200), sliver: SliverToBoxAdapter( @@ -260,7 +264,14 @@ class _GridFooter extends StatelessWidget { child: Row( children: [ SizedBox(width: GridSize.leadingHeaderPadding), - const SizedBox(width: 120, child: GridAddRowButton()), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 120, child: GridAddRowButton()), + const SizedBox(height: 30), + _rowCountTextWidget(theme: theme,count: rowCount) + ], + ), ], ), ), @@ -268,4 +279,19 @@ class _GridFooter extends StatelessWidget { ), ); } + + Widget _rowCountTextWidget({required AppTheme theme, required int count}){ + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + FlowyText.regular('Count : ', + fontSize: 13, + color: theme.shader3, + ), + FlowyText.regular(count.toString(), + fontSize: 13, + ), + ], + ); + } } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart index 61e8e0151c..0a137b6d46 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart @@ -5,7 +5,7 @@ class GridSize { static double get scrollBarSize => 12 * scale; static double get headerHeight => 40 * scale; - static double get footerHeight => 40 * scale; + static double get footerHeight => 100 * scale; static double get leadingHeaderPadding => 50 * scale; static double get trailHeaderPadding => 140 * scale; static double get headerContainerPadding => 0 * scale; diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart index 8c7bb8f494..eaf09d770f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,70 +1,75 @@ +import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/application/appearance.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_menu.dart'; +import 'package:app_flowy/workspace/application/settings/settings_dialog_bloc.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; -class SettingsDialog extends StatefulWidget { - const SettingsDialog({Key? key}) : super(key: key); +class SettingsDialog extends StatelessWidget { + final UserProfilePB user; + SettingsDialog(this.user, {Key? key}) : super(key: ValueKey(user.id)); - @override - State createState() => _SettingsDialogState(); -} - -class _SettingsDialogState extends State { - int _selectedViewIndex = 0; - - final List settingsViews = const [ - SettingsAppearanceView(), - SettingsLanguageView(), - ]; + Widget getSettingsView(int index, UserProfilePB user) { + final List settingsViews = [ + const SettingsAppearanceView(), + const SettingsLanguageView(), + SettingsUserView(user), + ]; + return settingsViews[index]; + } @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - title: Text( - LocaleKeys.settings_title.tr(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - content: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 600, - minWidth: 600, - maxWidth: 1000, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: SettingsMenu( - changeSelectedIndex: (index) { - setState(() { - _selectedViewIndex = index; - }); - }, - currentIndex: _selectedViewIndex, - ), - ), - const VerticalDivider(), - const SizedBox(width: 10), - Expanded( - child: settingsViews[_selectedViewIndex], - ) - ], - ), - ), - ), - ); + return BlocProvider( + create: (context) => getIt(param1: user)..add(const SettingsDialogEvent.initial()), + child: BlocBuilder( + builder: (context, state) => ChangeNotifierProvider.value( + value: Provider.of(context, listen: true), + child: AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + title: Text( + LocaleKeys.settings_title.tr(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 600, + minWidth: 600, + maxWidth: 1000, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: SettingsMenu( + changeSelectedIndex: (index) { + context.read().add(SettingsDialogEvent.setViewIndex(index)); + }, + currentIndex: context.read().state.viewIndex, + ), + ), + const VerticalDivider(), + const SizedBox(width: 10), + Expanded( + child: getSettingsView(context.read().state.viewIndex, + context.read().state.userProfile), + ) + ], + ), + ), + ), + ))); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart index e03f1fbc3b..85b78ae3b0 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart @@ -1,10 +1,13 @@ import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/application/appearance.dart'; +import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../widgets/toggle/toggle.dart'; + class SettingsAppearanceView extends StatelessWidget { const SettingsAppearanceView({Key? key}) : super(key: key); @@ -25,11 +28,12 @@ class SettingsAppearanceView extends StatelessWidget { fontWeight: FontWeight.w500, ), ), - Switch( + Toggle( value: theme.isDark, onChanged: (val) { context.read().swapTheme(); }, + style: ToggleStyle.big(theme), ), Text( LocaleKeys.settings_appearance_darkLabel.tr(), diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart index 5617750a77..e95e6e83ab 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart @@ -1,18 +1,37 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/application/appearance.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flowy_infra/language.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; class SettingsLanguageView extends StatelessWidget { const SettingsLanguageView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [LanguageSelectorDropdown()], + context.watch(); + return ChangeNotifierProvider.value( + value: Provider.of(context, listen: true), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + LocaleKeys.settings_menu_language.tr(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const LanguageSelectorDropdown(), + ], + ), + ], + ), ), ); } @@ -28,22 +47,66 @@ class LanguageSelectorDropdown extends StatefulWidget { } class _LanguageSelectorDropdownState extends State { + Color currHoverColor = Colors.white.withOpacity(0.0); + late Color themedHoverColor; + void hoverExitLanguage() { + setState(() { + currHoverColor = Colors.white.withOpacity(0.0); + }); + } + + void hoverEnterLanguage() { + setState(() { + currHoverColor = themedHoverColor; + }); + } + @override Widget build(BuildContext context) { - return DropdownButton( - value: context.read().locale, - onChanged: (val) { - setState(() { - context.read().setLocale(context, val!); - }); - }, - autofocus: true, - items: EasyLocalization.of(context)!.supportedLocales.map((locale) { - return DropdownMenuItem( - value: locale, - child: Text(languageFromLocale(locale)), - ); - }).toList(), + final theme = context.watch(); + themedHoverColor = theme.main2; + + return MouseRegion( + onEnter: (event) => {hoverEnterLanguage()}, + onExit: (event) => {hoverExitLanguage()}, + child: Container( + margin: const EdgeInsets.only(left: 8, right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: currHoverColor, + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: context.read().locale, + onChanged: (val) { + setState(() { + context.read().setLocale(context, val!); + }); + }, + icon: const Visibility( + child: (Icon(Icons.arrow_downward)), + visible: false, + ), + borderRadius: BorderRadius.circular(8), + items: EasyLocalization.of(context)!.supportedLocales.map((locale) { + return DropdownMenuItem( + value: locale, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + languageFromLocale(locale), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.textColor, + ), + ), + ), + ); + }).toList(), + ), + ), + ), ); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart index 241c337705..a27d9861c4 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -34,6 +34,16 @@ class SettingsMenu extends StatelessWidget { icon: Icons.translate, changeSelectedIndex: changeSelectedIndex, ), + const SizedBox( + height: 10, + ), + SettingsMenuElement( + index: 2, + currentIndex: currentIndex, + label: LocaleKeys.settings_menu_user.tr(), + icon: Icons.account_box_outlined, + changeSelectedIndex: changeSelectedIndex, + ), ], ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart new file mode 100644 index 0000000000..f8f094d1b0 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -0,0 +1,50 @@ +import 'package:app_flowy/startup/startup.dart'; +import 'package:flutter/material.dart'; +import 'package:app_flowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; + +class SettingsUserView extends StatelessWidget { + final UserProfilePB user; + SettingsUserView(this.user, {Key? key}) : super(key: ValueKey(user.id)); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(param1: user)..add(const SettingsUserEvent.initial()), + child: BlocBuilder( + builder: (context, state) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [_renderUserNameInput(context)], + ), + ), + ), + ); + } + + Widget _renderUserNameInput(BuildContext context) { + String name = context.read().state.userProfile.name; + return _UserNameInput(name); + } +} + +class _UserNameInput extends StatelessWidget { + final String name; + const _UserNameInput( + this.name, { + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextField( + controller: TextEditingController()..text = name, + decoration: const InputDecoration( + labelText: 'Name', + ), + onSubmitted: (val) { + context.read().add(SettingsUserEvent.updateUserName(val)); + }); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle.dart new file mode 100644 index 0000000000..4de1c7d376 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle.dart @@ -0,0 +1,52 @@ +import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:flutter/widgets.dart'; + +class Toggle extends StatelessWidget { + final ToggleStyle style; + final bool value; + final void Function(bool) onChanged; + final EdgeInsets padding; + + const Toggle({ + Key? key, + required this.value, + required this.onChanged, + required this.style, + this.padding = const EdgeInsets.all(8.0), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: (() => onChanged(value)), + child: Padding( + padding: padding, + child: Stack( + children: [ + Container( + height: style.height, + width: style.width, + decoration: BoxDecoration( + color: value ? style.activeBackgroundColor : style.inactiveBackgroundColor, + borderRadius: BorderRadius.circular(style.height / 2), + ), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 150), + top: (style.height - style.thumbRadius) / 2, + left: value ? style.width - style.thumbRadius - 1 : 1, + child: Container( + height: style.thumbRadius, + width: style.thumbRadius, + decoration: BoxDecoration( + color: style.thumbColor, + borderRadius: BorderRadius.circular(style.thumbRadius / 2), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle_style.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle_style.dart new file mode 100644 index 0000000000..683abfab5f --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle_style.dart @@ -0,0 +1,38 @@ +import 'package:flowy_infra/theme.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter/widgets.dart'; + +class ToggleStyle { + final double height; + final double width; + + final double thumbRadius; + final Color thumbColor; + final Color activeBackgroundColor; + final Color inactiveBackgroundColor; + + ToggleStyle({ + required this.height, + required this.width, + required this.thumbRadius, + required this.thumbColor, + required this.activeBackgroundColor, + required this.inactiveBackgroundColor, + }); + + ToggleStyle.big(AppTheme theme) + : height = 16, + width = 27, + thumbRadius = 14, + activeBackgroundColor = theme.main1, + inactiveBackgroundColor = theme.shader5, + thumbColor = theme.surface; + + ToggleStyle.small(AppTheme theme) + : height = 10, + width = 16, + thumbRadius = 8, + activeBackgroundColor = theme.main1, + inactiveBackgroundColor = theme.shader5, + thumbColor = theme.surface; +} 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..417d1ce11c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -0,0 +1,111 @@ +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) { + return Position(path: node.path, offset: 0); + } + + @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..00fd3bc03f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin"); + rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar); + 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..0342e3868a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + rich_clipboard_linux + 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..0dc858f3c7 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import rich_clipboard_macos +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin")) + 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..93389ef3ec --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/macos/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - FlutterMacOS (1.0.0) + - rich_clipboard_macos (0.0.1): + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + rich_clipboard_macos: + :path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c + 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..8a0f4ea223 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -0,0 +1,383 @@ +# 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" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" + 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" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + 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" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.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" + rich_clipboard: + dependency: transitive + description: + name: rich_clipboard + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_android: + dependency: transitive + description: + name: rich_clipboard_android + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_ios: + dependency: transitive + description: + name: rich_clipboard_ios + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_linux: + dependency: transitive + description: + name: rich_clipboard_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_macos: + dependency: transitive + description: + name: rich_clipboard_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + rich_clipboard_platform_interface: + dependency: transitive + description: + name: rich_clipboard_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_web: + dependency: transitive + description: + name: rich_clipboard_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + rich_clipboard_windows: + dependency: transitive + description: + name: rich_clipboard_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.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" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.1" + 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: ">=3.0.0" 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..c3f75c5c9c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -0,0 +1,225 @@ +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 Delta delta, + LinkedList? children, + Attributes? attributes, + }) : _delta = delta, + super(children: children ?? LinkedList(), attributes: attributes ?? {}); + + 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/node_iterator.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart new file mode 100644 index 0000000000..1f321e937a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart @@ -0,0 +1,74 @@ +import 'package:flowy_editor/document/node.dart'; + +import './state_tree.dart'; +import './node.dart'; + +/// [NodeIterator] is used to traverse the nodes in visual order. +class NodeIterator implements Iterator { + final StateTree stateTree; + final Node _startNode; + final Node? _endNode; + Node? _currentNode; + bool _began = false; + + NodeIterator(this.stateTree, Node startNode, [Node? endNode]) + : _startNode = startNode, + _endNode = endNode; + + @override + bool moveNext() { + if (!_began) { + _currentNode = _startNode; + _began = true; + return true; + } + + final node = _currentNode; + if (node == null) { + return false; + } + + if (_endNode != null && _endNode == node) { + _currentNode = null; + return false; + } + + if (node.children.isNotEmpty) { + _currentNode = _findLeadingChild(node); + } else if (node.next != null) { + _currentNode = node.next!; + } else { + final parent = node.parent!; + final nextOfParent = parent.next; + if (nextOfParent == null) { + _currentNode = null; + } else { + _currentNode = _findLeadingChild(node); + } + } + + return _currentNode != null; + } + + Node _findLeadingChild(Node node) { + while (node.children.isNotEmpty) { + node = node.children.first; + } + return node; + } + + @override + Node get current { + return _currentNode!; + } + + List toList() { + final result = []; + + while (moveNext()) { + result.add(current); + } + + return result; + } +} 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 = + '$number.'; + return SizedBox.fromSize( + size: size, + child: SvgPicture.string(numberText), + ); + } + return Container(); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart new file mode 100644 index 0000000000..9c7872c901 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -0,0 +1,201 @@ +import 'dart:collection'; + +import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flutter/foundation.dart'; +import 'package:html/parser.dart' show parse; +import 'package:html/dom.dart' as html; + +const String tagH1 = "h1"; +const String tagH2 = "h2"; +const String tagH3 = "h3"; +const String tagUnorderedList = "ul"; +const String tagList = "li"; +const String tagParagraph = "p"; +const String tagImage = "img"; +const String tagAnchor = "a"; +const String tagBold = "b"; +const String tagStrong = "strong"; +const String tagSpan = "span"; + +class HTMLConverter { + final html.Document _document; + + HTMLConverter(String htmlString) : _document = parse(htmlString); + + List toNodes() { + final result = []; + final delta = Delta(); + + final childNodes = _document.body?.nodes.toList() ?? []; + for (final child in childNodes) { + if (child is html.Element) { + if (child.localName == tagAnchor || + child.localName == tagSpan || + child.localName == tagStrong || + child.localName == tagBold) { + _handleRichTextElement(delta, child); + } else { + _handleElement(result, child); + } + } else { + delta.insert(child.text ?? ""); + } + } + + if (delta.operations.isNotEmpty) { + result.add(TextNode(type: "text", delta: delta)); + } + + return result; + } + + _handleElement(List nodes, html.Element element) { + if (element.localName == tagH1) { + _handleHeadingElement(nodes, element, tagH1); + } else if (element.localName == tagH2) { + _handleHeadingElement(nodes, element, tagH2); + } else if (element.localName == tagH3) { + _handleHeadingElement(nodes, element, tagH3); + } else if (element.localName == tagUnorderedList) { + _handleUnorderedList(nodes, element); + } else if (element.localName == tagList) { + _handleListElement(nodes, element); + } else if (element.localName == tagParagraph) { + _handleParagraph(nodes, element); + } else { + final delta = Delta(); + delta.insert(element.text); + if (delta.operations.isNotEmpty) { + nodes.add(TextNode(type: "text", delta: delta)); + } + } + } + + _handleParagraph(List nodes, html.Element element) { + _handleRichText(nodes, element); + } + + Attributes? _getDeltaAttributesFromHtmlAttributes( + LinkedHashMap htmlAttributes) { + final attrs = {}; + final styleString = htmlAttributes["style"]; + if (styleString != null) { + final entries = styleString.split(";"); + for (final entry in entries) { + final tuples = entry.split(":"); + if (tuples.length < 2) { + continue; + } + if (tuples[0] == "font-weight") { + int? weight = int.tryParse(tuples[1]); + if (weight != null && weight > 500) { + attrs["bold"] = true; + } + } + } + } + + return attrs.isEmpty ? null : attrs; + } + + _handleRichTextElement(Delta delta, html.Element element) { + if (element.localName == tagSpan) { + delta.insert(element.text, + _getDeltaAttributesFromHtmlAttributes(element.attributes)); + } else if (element.localName == tagAnchor) { + final hyperLink = element.attributes["href"]; + Map? attributes; + if (hyperLink != null) { + attributes = {"href": hyperLink}; + } + delta.insert(element.text, attributes); + } else if (element.localName == tagStrong || element.localName == tagBold) { + delta.insert(element.text, {"bold": true}); + } else { + delta.insert(element.text); + } + } + + _handleRichText(List nodes, html.Element element) { + final image = element.querySelector(tagImage); + if (image != null) { + _handleImage(nodes, image); + return; + } + + var delta = Delta(); + + for (final child in element.nodes.toList()) { + if (child is html.Element) { + _handleRichTextElement(delta, child); + } else { + delta.insert(child.text ?? ""); + } + } + + if (delta.operations.isNotEmpty) { + nodes.add(TextNode(type: "text", delta: delta)); + } + } + + _handleImage(List nodes, html.Element element) { + final src = element.attributes["src"]; + final attributes = {}; + if (src != null) { + attributes["image_src"] = src; + } + debugPrint("insert image: $src"); + nodes.add( + Node(type: "image", attributes: attributes, children: LinkedList())); + } + + _handleUnorderedList(List nodes, html.Element element) { + element.children.forEach((child) { + _handleListElement(nodes, child); + }); + } + + _handleHeadingElement( + List nodes, + html.Element element, + String headingStyle, + ) { + final delta = Delta(); + delta.insert(element.text); + if (delta.operations.isNotEmpty) { + nodes.add(TextNode( + type: "text", + attributes: {"subtype": "heading", "heading": headingStyle}, + delta: delta)); + } + } + + _handleListElement(List nodes, html.Element element) { + final childNodes = element.nodes.toList(); + for (final child in childNodes) { + if (child is html.Element) { + _handleRichText(nodes, child); + } + } + } +} + +String deltaToHtml(Delta delta) { + var result = "

"; + + for (final op in delta.operations) { + if (op is TextInsert) { + final attributes = op.attributes; + if (attributes != null && attributes["bold"] == true) { + result += '${op.content}'; + } else { + result += op.content; + } + } + } + + result += "

"; + return result; +} 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..8fa67687c2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -0,0 +1,165 @@ +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)); + } + + setAfterSelection(Selection sel) { + afterSelection = sel; + } + + 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..39382d3a9c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -0,0 +1,121 @@ +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/service/input_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_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/selection_service.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/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, + copyPasteKeysHandler, + 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 createState() => _FlowyInputState(); +} + +class _FlowyInputState extends State + with FlowyInputService + implements DeltaTextInputClient { + TextInputConnection? _textInputConnection; + + EditorState get _editorState => widget.editorState; + + @override + void initState() { + super.initState(); + + _editorState.service.selectionService.currentSelectedNodes + .addListener(_onSelectedNodesChange); + } + + @override + void dispose() { + _editorState.service.selectionService.currentSelectedNodes + .removeListener(_onSelectedNodesChange); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + child: widget.child, + ); + } + + @override + void attach(TextEditingValue textEditingValue) { + if (_textInputConnection != null) { + return; + } + + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + // TODO: customize + enableDeltaModel: true, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + ), + ); + + _textInputConnection + ?..show() + ..setEditingState(textEditingValue); + } + + @override + void setTextEditingValue(TextEditingValue textEditingValue) { + assert(_textInputConnection != null, + 'Must call `attach` before set textEditingValue'); + if (_textInputConnection != null) { + _textInputConnection?.setEditingState(textEditingValue); + } + } + + @override + void apply(List deltas) { + // TODO: implement the detail + for (final delta in deltas) { + if (delta is TextEditingDeltaInsertion) { + _applyInsert(delta); + } else if (delta is TextEditingDeltaDeletion) { + } else if (delta is TextEditingDeltaReplacement) { + _applyReplacement(delta); + } else if (delta is TextEditingDeltaNonTextUpdate) { + // We don't need to care the [TextEditingDeltaNonTextUpdate]. + // Do nothing. + } + } + } + + void _applyInsert(TextEditingDeltaInsertion delta) { + final selectionService = _editorState.service.selectionService; + final currentSelection = selectionService.currentSelection; + if (currentSelection == null) { + return; + } + if (currentSelection.isSingle) { + final textNode = + selectionService.currentSelectedNodes.value.first as TextNode; + TransactionBuilder(_editorState) + ..insertText( + textNode, + delta.insertionOffset, + delta.textInserted, + ) + ..commit(); + } else { + // TODO: implement + } + } + + void _applyReplacement(TextEditingDeltaReplacement delta) { + final selectionService = _editorState.service.selectionService; + final currentSelection = selectionService.currentSelection; + if (currentSelection == null) { + return; + } + if (currentSelection.isSingle) { + final textNode = + selectionService.currentSelectedNodes.value.first as TextNode; + final length = delta.replacedRange.end - delta.replacedRange.start; + TransactionBuilder(_editorState) + ..replaceText( + textNode, delta.replacedRange.start, length, delta.replacementText) + ..commit(); + } else { + // TODO: implement + } + } + + @override + void close() { + _textInputConnection?.close(); + _textInputConnection = null; + } + + @override + void connectionClosed() { + // TODO: implement connectionClosed + } + + @override + // TODO: implement currentAutofillScope + AutofillScope? get currentAutofillScope => throw UnimplementedError(); + + @override + // TODO: implement currentTextEditingValue + TextEditingValue? get currentTextEditingValue => throw UnimplementedError(); + + @override + void insertTextPlaceholder(Size size) { + // TODO: implement insertTextPlaceholder + } + + @override + void performAction(TextInputAction action) { + // TODO: implement performAction + } + + @override + void performPrivateCommand(String action, Map data) { + // TODO: implement performPrivateCommand + } + + @override + void removeTextPlaceholder() { + // TODO: implement removeTextPlaceholder + } + + @override + void showAutocorrectionPromptRect(int start, int end) { + // TODO: implement showAutocorrectionPromptRect + } + + @override + void showToolbar() { + // TODO: implement showToolbar + } + + @override + void updateEditingValue(TextEditingValue value) { + // TODO: implement updateEditingValue + } + + @override + void updateEditingValueWithDeltas(List textEditingDeltas) { + debugPrint(textEditingDeltas.map((delta) => delta.toString()).toString()); + + apply(textEditingDeltas); + } + + @override + void updateFloatingCursor(RawFloatingCursorPoint point) { + // TODO: implement updateFloatingCursor + } + + void _onSelectedNodesChange() { + final nodes = + _editorState.service.selectionService.currentSelectedNodes.value; + final selection = _editorState.service.selectionService.currentSelection; + // FIXME: upward. + if (nodes.isNotEmpty && selection != null) { + final textNodes = nodes.whereType(); + final text = textNodes.fold( + '', (sum, textNode) => '$sum${textNode.toRawString()}\n'); + attach( + TextEditingValue( + text: text, + selection: TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset, + ), + ), + ); + } else { + close(); + } + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart new file mode 100644 index 0000000000..7fbdf669b5 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -0,0 +1,143 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +int _endOffsetOfNode(Node node) { + if (node is TextNode) { + return node.delta.length; + } + return 0; +} + +extension on Position { + Position? goLeft(EditorState editorState) { + if (offset == 0) { + final node = editorState.document.nodeAtPath(path)!; + final prevNode = node.previous; + if (prevNode != null) { + return Position( + path: prevNode.path, offset: _endOffsetOfNode(prevNode)); + } + return null; + } + + return Position(path: path, offset: offset - 1); + } + + Position? goRight(EditorState editorState) { + final node = editorState.document.nodeAtPath(path)!; + final lengthOfNode = _endOffsetOfNode(node); + if (offset >= lengthOfNode) { + final nextNode = node.next; + if (nextNode != null) { + return Position(path: nextNode.path, offset: 0); + } + return null; + } + + return Position(path: path, offset: offset + 1); + } +} + +Position? _goUp(EditorState editorState) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return null; + } + final first = rects.first; + final firstOffset = Offset(first.left, first.top); + final hitOffset = firstOffset - Offset(0, first.height * 0.5); + return editorState.service.selectionService.hitTest(hitOffset); +} + +Position? _goDown(EditorState editorState) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return null; + } + final first = rects.last; + final firstOffset = Offset(first.right, first.bottom); + final hitOffset = firstOffset + Offset(0, first.height * 0.5); + return editorState.service.selectionService.hitTest(hitOffset); +} + +KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { + final currentSelection = editorState.cursorSelection; + if (currentSelection == null) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + final leftPosition = currentSelection.end.goLeft(editorState); + editorState.updateCursorSelection(leftPosition == null + ? null + : Selection(start: currentSelection.start, end: leftPosition)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + final rightPosition = currentSelection.start.goRight(editorState); + editorState.updateCursorSelection(rightPosition == null + ? null + : Selection(start: rightPosition, end: currentSelection.end)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final position = _goUp(editorState); + editorState.updateCursorSelection(position == null + ? null + : Selection(start: position, end: currentSelection.end)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final position = _goDown(editorState); + editorState.updateCursorSelection(position == null + ? null + : Selection(start: currentSelection.start, end: position)); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +} + +FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { + if (event.isShiftPressed) { + return _handleShiftKey(editorState, event); + } + + final currentSelection = editorState.cursorSelection; + if (currentSelection == null) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + if (currentSelection.isCollapsed) { + final leftPosition = currentSelection.start.goLeft(editorState); + if (leftPosition != null) { + editorState.updateCursorSelection(Selection.collapsed(leftPosition)); + } + } else { + editorState + .updateCursorSelection(currentSelection.collapse(atStart: true)); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + if (currentSelection.isCollapsed) { + final rightPosition = currentSelection.end.goRight(editorState); + if (rightPosition != null) { + editorState.updateCursorSelection(Selection.collapsed(rightPosition)); + } + } else { + editorState.updateCursorSelection(currentSelection.collapse()); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final position = _goUp(editorState); + editorState.updateCursorSelection( + position == null ? null : Selection.collapsed(position)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final position = _goDown(editorState); + editorState.updateCursorSelection( + position == null ? null : Selection.collapsed(position)); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart new file mode 100644 index 0000000000..cde1c4e122 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -0,0 +1,233 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/infra/html_converter.dart'; +import 'package:flowy_editor/document/node_iterator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:rich_clipboard/rich_clipboard.dart'; + +_handleCopy(EditorState editorState) async { + final selection = editorState.cursorSelection; + if (selection == null || selection.isCollapsed) { + return; + } + if (pathEquals(selection.start.path, selection.end.path)) { + final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!; + if (nodeAtPath.type == "text") { + final textNode = nodeAtPath as TextNode; + final delta = + textNode.delta.slice(selection.start.offset, selection.end.offset); + + final htmlString = deltaToHtml(delta); + debugPrint('copy html: $htmlString'); + RichClipboard.setData(RichClipboardData(html: htmlString)); + } else { + debugPrint("unimplemented: copy non-text"); + } + return; + } + + final beginNode = editorState.document.nodeAtPath(selection.start.path)!; + final endNode = editorState.document.nodeAtPath(selection.end.path)!; + final traverser = NodeIterator(editorState.document, beginNode, endNode); + + var copyString = ""; + while (traverser.moveNext()) { + final node = traverser.current; + if (node.type == "text") { + final textNode = node as TextNode; + if (node == beginNode) { + final htmlString = + deltaToHtml(textNode.delta.slice(selection.start.offset)); + copyString += htmlString; + } else if (node == endNode) { + final htmlString = + deltaToHtml(textNode.delta.slice(0, selection.end.offset)); + copyString += htmlString; + } else { + final htmlString = deltaToHtml(textNode.delta); + copyString += htmlString; + } + } + // TODO: handle image and other blocks + + } + debugPrint('copy html: $copyString'); + RichClipboard.setData(RichClipboardData(html: copyString)); +} + +_pasteHTML(EditorState editorState, String html) { + final selection = editorState.cursorSelection; + if (selection == null) { + return; + } + + final path = [...selection.end.path]; + if (path.isEmpty) { + return; + } + + debugPrint('paste html: $html'); + final converter = HTMLConverter(html); + final nodes = converter.toNodes(); + + if (nodes.isEmpty) { + return; + } else if (nodes.length == 1) { + final firstNode = nodes[0]; + final nodeAtPath = editorState.document.nodeAtPath(path)!; + final tb = TransactionBuilder(editorState); + final startOffset = selection.start.offset; + if (nodeAtPath.type == "text" && firstNode.type == "text") { + final textNodeAtPath = nodeAtPath as TextNode; + final firstTextNode = firstNode as TextNode; + tb.textEdit(textNodeAtPath, + () => Delta().retain(startOffset).concat(firstTextNode.delta)); + tb.setAfterSelection(Selection.collapsed(Position( + path: path, offset: startOffset + firstTextNode.delta.length))); + tb.commit(); + return; + } + } + + _pasteMultipleLinesInText(editorState, path, selection.start.offset, nodes); +} + +_pasteMultipleLinesInText( + EditorState editorState, List path, int offset, List nodes) { + final tb = TransactionBuilder(editorState); + + final firstNode = nodes[0]; + final nodeAtPath = editorState.document.nodeAtPath(path)!; + + if (nodeAtPath.type == "text" && firstNode.type == "text") { + // split and merge + final textNodeAtPath = nodeAtPath as TextNode; + final firstTextNode = firstNode as TextNode; + final remain = textNodeAtPath.delta.slice(offset); + + tb.textEdit( + textNodeAtPath, + () => Delta() + .retain(offset) + .delete(remain.length) + .concat(firstTextNode.delta)); + + final tailNodes = nodes.sublist(1); + path[path.length - 1]++; + if (tailNodes.isNotEmpty) { + if (tailNodes.last.type == "text") { + final tailTextNode = tailNodes.last as TextNode; + tailTextNode.delta = tailTextNode.delta.concat(remain); + } else if (remain.length > 0) { + tailNodes.add(TextNode(type: "text", delta: remain)); + } + } else { + tailNodes.add(TextNode(type: "text", delta: remain)); + } + + tb.insertNodes(path, tailNodes); + tb.commit(); + return; + } + + path[path.length - 1]++; + tb.insertNodes(path, nodes); + tb.commit(); +} + +_handlePaste(EditorState editorState) async { + final data = await RichClipboard.getData(); + if (data.html != null) { + _pasteHTML(editorState, data.html!); + return; + } + if (data.text != null) { + _handlePastePlainText(editorState, data.text!); + return; + } +} + +_handlePastePlainText(EditorState editorState, String plainText) { + final selection = editorState.cursorSelection; + if (selection == null) { + return; + } + + final lines = plainText + .split("\n") + .map((e) => e.replaceAll(RegExp(r'\r'), "")) + .toList(); + + if (lines.isEmpty) { + return; + } else if (lines.length == 1) { + final node = + editorState.document.nodeAtPath(selection.end.path)! as TextNode; + final beginOffset = selection.end.offset; + TransactionBuilder(editorState) + ..textEdit(node, () => Delta().retain(beginOffset).insert(lines[0])) + ..setAfterSelection(Selection.collapsed(Position( + path: selection.end.path, offset: beginOffset + lines[0].length))) + ..commit(); + } else { + final firstLine = lines[0]; + final beginOffset = selection.end.offset; + final remains = lines.sublist(1); + + final path = [...selection.end.path]; + if (path.isEmpty) { + return; + } + + final node = + editorState.document.nodeAtPath(selection.end.path)! as TextNode; + final insertedLineSuffix = node.delta.slice(beginOffset); + + path[path.length - 1]++; + var index = 0; + final tb = TransactionBuilder(editorState); + final nodes = remains.map((e) { + if (index++ == remains.length - 1) { + return TextNode( + type: "text", + delta: Delta().insert(e).addAll(insertedLineSuffix.operations)); + } + return TextNode(type: "text", delta: Delta().insert(e)); + }).toList(); + // insert first line + tb.textEdit( + node, + () => Delta() + .retain(beginOffset) + .insert(firstLine) + .delete(node.delta.length - beginOffset)); + // insert remains + tb.insertNodes(path, nodes); + tb.commit(); + + // fixme: don't set the cursor manually + editorState.updateCursorSelection(Selection.collapsed( + Position(path: nodes.last.path, offset: lines.last.length))); + } +} + +_handleCut() { + debugPrint('cut'); +} + +FlowyKeyEventHandler copyPasteKeysHandler = (editorState, event) { + if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyC) { + _handleCopy(editorState); + return KeyEventResult.handled; + } + if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV) { + _handlePaste(editorState); + return KeyEventResult.handled; + } + if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyX) { + _handleCut(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart new file mode 100644 index 0000000000..dda52612e9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart @@ -0,0 +1,21 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; + +FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { + // Handle delete nodes. + final nodes = editorState.selectedNodes; + if (nodes.length <= 1) { + return KeyEventResult.ignored; + } + + debugPrint('delete nodes = $nodes'); + + nodes + .fold( + TransactionBuilder(editorState), + (previousValue, node) => previousValue..deleteNode(node), + ) + .commit(); + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart new file mode 100644 index 0000000000..498fd845b2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart @@ -0,0 +1,82 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Handle delete text. +FlowyKeyEventHandler deleteTextHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.backspace) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection; + if (selection == null) { + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + // make sure all nodes is [TextNode]. + final textNodes = nodes.whereType().toList(); + if (textNodes.length != nodes.length) { + return KeyEventResult.ignored; + } + + TransactionBuilder transactionBuilder = TransactionBuilder(editorState); + if (textNodes.length == 1) { + final textNode = textNodes.first; + final index = selection.start.offset - 1; + if (index < 0) { + // 1. style + if (textNode.subtype != null) { + transactionBuilder.updateNode(textNode, { + 'subtype': null, + }); + } else { + // 2. non-style + // find previous text node. + while (textNode.previous != null) { + if (textNode.previous is TextNode) { + final previous = textNode.previous as TextNode; + transactionBuilder + ..deleteNode(textNode) + ..mergeText(previous, textNode); + break; + } + } + } + } else { + if (selection.isCollapsed) { + transactionBuilder.deleteText( + textNode, + selection.start.offset - 1, + 1, + ); + } else { + transactionBuilder.deleteText( + textNode, + selection.start.offset, + selection.end.offset - selection.start.offset, + ); + } + } + } else { + final first = textNodes.first; + final last = textNodes.last; + var content = textNodes.last.toRawString(); + content = content.substring(selection.end.offset, content.length); + // Merge the fist and the last text node content, + // and delete the all nodes expect for the first. + transactionBuilder + ..deleteNodes(textNodes.sublist(1)) + ..mergeText( + first, + last, + firstOffset: selection.start.offset, + secondOffset: selection.end.offset, + ); + } + + transactionBuilder.commit(); + + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart new file mode 100644 index 0000000000..ccdfcad5dc --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart @@ -0,0 +1,94 @@ +import 'dart:collection'; + +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/document/text_delta.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flowy_editor/extensions/path_extensions.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; + +FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.enter) { + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final selection = editorState.service.selectionService.currentSelection; + if (selection == null || + nodes.length != 1 || + nodes.first is! TextNode || + !selection.isCollapsed) { + return KeyEventResult.ignored; + } + + final textNode = nodes.first as TextNode; + if (textNode.selectable!.end() == selection.end) { + if (textNode.subtype != null && textNode.delta.length == 0) { + TransactionBuilder(editorState) + ..deleteNode(textNode) + ..insertNode( + textNode.path, + textNode.copyWith( + children: LinkedList(), + delta: Delta([TextInsert('')]), + attributes: {}, + ), + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: 0, + ), + ) + ..commit(); + } else { + final needCopyAttributes = StyleKey.globalStyleKeys + .where((key) => key != StyleKey.heading) + .contains(textNode.subtype); + TransactionBuilder(editorState) + ..insertNode( + textNode.path.next, + textNode.copyWith( + children: LinkedList(), + delta: Delta([TextInsert('')]), + attributes: needCopyAttributes ? textNode.attributes : {}, + ), + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path.next, + offset: 0, + ), + ) + ..commit(); + } + + return KeyEventResult.handled; + } else if (textNode.selectable!.start() == selection.start) { + TransactionBuilder(editorState) + ..insertNode( + textNode.path, + textNode.copyWith( + children: LinkedList(), + delta: Delta([TextInsert('')]), + attributes: {}, + ), + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path.next, + offset: 0, + ), + ) + ..commit(); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart new file mode 100644 index 0000000000..f424bcf314 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart @@ -0,0 +1,12 @@ +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// type '/' to trigger shortcut widget +FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.slash) { + return KeyEventResult.ignored; + } + + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart new file mode 100644 index 0000000000..b062480cf2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; + +FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { + if (!event.isMetaPressed || event.character == null) { + return KeyEventResult.ignored; + } + + 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 KeyEventResult.ignored; + } + + switch (event.character!) { + // bold + case 'B': + case 'b': + formatBold(editorState); + return KeyEventResult.handled; + default: + break; + } + + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart new file mode 100644 index 0000000000..ebd66894a7 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart @@ -0,0 +1,65 @@ +import 'package:flutter/services.dart'; + +import '../editor_state.dart'; +import 'package:flutter/material.dart'; + +typedef FlowyKeyEventHandler = KeyEventResult Function( + EditorState editorState, + RawKeyEvent event, +); + +/// Process keyboard events +class FlowyKeyboard extends StatefulWidget { + const FlowyKeyboard({ + Key? key, + required this.handlers, + required this.editorState, + required this.child, + }) : super(key: key); + + final EditorState editorState; + final Widget child; + final List handlers; + + @override + State createState() => _FlowyKeyboardState(); +} + +class _FlowyKeyboardState extends State { + final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service'); + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: focusNode, + autofocus: true, + onKey: _onKey, + child: widget.child, + ); + } + + KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + debugPrint('on keyboard event $event'); + + if (event is! RawKeyDownEvent) { + return KeyEventResult.ignored; + } + + for (final handler in widget.handlers) { + // debugPrint('handle keyboard event $event by $handler'); + + KeyEventResult result = handler(widget.editorState, event); + + switch (result) { + case KeyEventResult.handled: + return KeyEventResult.handled; + case KeyEventResult.skipRemainingHandlers: + return KeyEventResult.skipRemainingHandlers; + case KeyEventResult.ignored: + continue; + } + } + + return KeyEventResult.ignored; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart new file mode 100644 index 0000000000..8ac32ac66c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart @@ -0,0 +1,145 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +typedef NodeValidator = bool Function(T node); + +abstract class NodeWidgetBuilder { + NodeValidator get nodeValidator; + + Widget build(NodeWidgetContext context); +} + +typedef NodeWidgetBuilders = Map; + +abstract class FlowyRenderPluginService { + /// Register render plugin with specified [name]. + /// + /// [name] should be [Node].type + /// or [Node].type + '/' + [Node].attributes['subtype']. + /// + /// e.g. 'text', 'text/checkbox', or 'text/heading' + /// + /// [name] could be empty. + void register(String name, NodeWidgetBuilder builder); + void registerAll(Map builders); + + /// UnRegister plugin with specified [name]. + void unRegister(String name); + + Widget buildPluginWidget(NodeWidgetContext context); +} + +class NodeWidgetContext { + final BuildContext context; + final T node; + final EditorState editorState; + + NodeWidgetContext({ + required this.context, + required this.node, + required this.editorState, + }); + + NodeWidgetContext copyWith({ + BuildContext? context, + T? node, + EditorState? editorState, + }) { + return NodeWidgetContext( + context: context ?? this.context, + node: node ?? this.node, + editorState: editorState ?? this.editorState, + ); + } +} + +class FlowyRenderPlugin extends FlowyRenderPluginService { + FlowyRenderPlugin({ + required this.editorState, + required NodeWidgetBuilders builders, + }) { + registerAll(builders); + } + + final NodeWidgetBuilders _builders = {}; + final EditorState editorState; + + @override + Widget buildPluginWidget(NodeWidgetContext context) { + final node = context.node; + final name = + node.subtype == null ? node.type : '${node.type}/${node.subtype!}'; + final builder = _builders[name]; + if (builder != null && builder.nodeValidator(node)) { + final key = GlobalKey(debugLabel: name); + node.key = key; + return _autoUpdateNodeWidget(builder, context); + } else { + assert(false, 'Could not query the builder with this $name'); + // TODO: return a placeholder widget with tips. + return Container(); + } + } + + @override + void register(String name, NodeWidgetBuilder builder) { + debugPrint('[Plugins] registering $name...'); + _validatePlugin(name); + _builders[name] = builder; + } + + @override + void registerAll(Map builders) { + builders.forEach(register); + } + + @override + void unRegister(String name) { + _validatePlugin(name); + _builders.remove(name); + } + + Widget _autoUpdateNodeWidget( + NodeWidgetBuilder builder, NodeWidgetContext context) { + Widget notifier; + if (context.node is TextNode) { + notifier = ChangeNotifierProvider.value( + value: context.node as TextNode, + builder: (_, child) { + return Consumer( + builder: ((_, value, child) { + debugPrint('Text Node is rebuilding...'); + return builder.build(context); + }), + ); + }); + } else { + notifier = ChangeNotifierProvider.value( + value: context.node, + builder: (_, child) { + return Consumer( + builder: ((_, value, child) { + debugPrint('Node is rebuilding...'); + return builder.build(context); + }), + ); + }); + } + return CompositedTransformTarget( + link: context.node.layerLink, + child: notifier, + ); + } + + void _validatePlugin(String name) { + final paths = name.split('/'); + if (paths.length > 2) { + throw Exception('Plugin name must contain at most one or zero slash'); + } + if (_builders.containsKey(name)) { + throw Exception('Plugin name($name) already exists.'); + } + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart new file mode 100644 index 0000000000..55b08f9279 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -0,0 +1,582 @@ +import 'dart:async'; + +import 'package:flowy_editor/document/node_iterator.dart'; +import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.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/extensions/node_extensions.dart'; +import 'package:flowy_editor/render/selection/cursor_widget.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/render/selection/selection_widget.dart'; + +/// Process selection and cursor +mixin FlowySelectionService on State { + /// Returns the currently selected [Node]s. + /// + /// The order of the return is determined according to the selected order. + ValueNotifier> get currentSelectedNodes; + Selection? get currentSelection; + + /// ------------------ Selection ------------------------ + + /// + void updateSelection(Selection selection); + + /// + void clearSelection(); + + List rects(); + + Position? hitTest(Offset? offset); + + /// + List getNodesInSelection(Selection selection); + + /// ------------------ Selection ------------------------ + + /// ------------------ Offset ------------------------ + + /// Returns selected [Node]s. Empty list would be returned + /// if no nodes are being selected. + /// + /// + /// [start] and [end] are the offsets under the global coordinate system. + /// + /// If end is not null, it means multiple selection, + /// otherwise single selection. + List getNodesInRange(Offset start, [Offset? end]); + + /// Return the [Node] or [Null] in single selection. + /// + /// [start] is the offset under the global coordinate system. + Node? computeNodeInOffset(Node node, Offset offset); + + /// Return the [Node]s in multiple selection. Empty list would be returned + /// if no nodes are in range. + /// + /// [start] is the offset under the global coordinate system. + List computeNodesInRange( + Node node, + Offset start, + Offset end, + ); + + /// Return [bool] to identify the [Node] is in Range or not. + /// + /// [start] and [end] are the offsets under the global coordinate system. + bool isNodeInRange( + Node node, + Offset start, + Offset end, + ); + + /// Return [bool] to identify the [Node] contains [Offset] or not. + /// + /// [start] is the offset under the global coordinate system. + bool isNodeInOffset(Node node, Offset offset); + + /// ------------------ Offset ------------------------ +} + +class FlowySelection extends StatefulWidget { + const FlowySelection({ + Key? key, + this.cursorColor = Colors.black, + this.selectionColor = const Color.fromARGB(60, 61, 61, 213), + required this.editorState, + required this.child, + }) : super(key: key); + + final EditorState editorState; + final Widget child; + final Color cursorColor; + final Color selectionColor; + + @override + State createState() => _FlowySelectionState(); +} + +/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer] +/// for a while. So we need to implement our own GestureDetector. +@immutable +class _SelectionGestureDetector extends StatefulWidget { + const _SelectionGestureDetector( + {Key? key, + this.child, + this.onTapDown, + this.onDoubleTapDown, + this.onPanStart, + this.onPanUpdate, + this.onPanEnd}) + : super(key: key); + + @override + State<_SelectionGestureDetector> createState() => + _SelectionGestureDetectorState(); + + final Widget? child; + + final GestureTapDownCallback? onTapDown; + final GestureTapDownCallback? onDoubleTapDown; + final GestureDragStartCallback? onPanStart; + final GestureDragUpdateCallback? onPanUpdate; + final GestureDragEndCallback? onPanEnd; +} + +class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> { + bool _isDoubleTap = false; + Timer? _doubleTapTimer; + @override + Widget build(BuildContext context) { + return RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + PanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer(), + (recognizer) { + recognizer + ..onStart = widget.onPanStart + ..onUpdate = widget.onPanUpdate + ..onEnd = widget.onPanEnd; + }, + ), + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (recognizer) { + recognizer.onTapDown = _tapDownDelegate; + }, + ), + }, + child: widget.child, + ); + } + + _tapDownDelegate(TapDownDetails tapDownDetails) { + if (_isDoubleTap) { + _isDoubleTap = false; + _doubleTapTimer?.cancel(); + _doubleTapTimer = null; + if (widget.onDoubleTapDown != null) { + widget.onDoubleTapDown!(tapDownDetails); + } + } else { + if (widget.onTapDown != null) { + widget.onTapDown!(tapDownDetails); + } + + _isDoubleTap = true; + _doubleTapTimer?.cancel(); + _doubleTapTimer = Timer(kDoubleTapTimeout, () { + _isDoubleTap = false; + _doubleTapTimer = null; + }); + } + } + + @override + void dispose() { + _doubleTapTimer?.cancel(); + super.dispose(); + } +} + +class _FlowySelectionState extends State + with FlowySelectionService, WidgetsBindingObserver { + final _cursorKey = GlobalKey(debugLabel: 'cursor'); + + final List _selectionOverlays = []; + final List _cursorOverlays = []; + + /// [Pan] and [Tap] must be mutually exclusive. + /// Pan + Offset? panStartOffset; + Offset? panEndOffset; + + /// Tap + Offset? tapOffset; + + final List _rects = []; + + EditorState get editorState => widget.editorState; + + @override + Selection? currentSelection; + + @override + ValueNotifier> currentSelectedNodes = ValueNotifier([]); + + @override + List getNodesInSelection(Selection selection) => + _selectedNodesInSelection(editorState.document, selection); + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + + // Need to refresh the selection when the metrics changed. + if (currentSelection != null) { + updateSelection(currentSelection!); + } + } + + @override + void dispose() { + clearSelection(); + WidgetsBinding.instance.removeObserver(this); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _SelectionGestureDetector( + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + onTapDown: _onTapDown, + onDoubleTapDown: _onDoubleTapDown, + child: widget.child, + ); + } + + @override + List rects() { + return _rects; + } + + @override + void updateSelection(Selection selection) { + _rects.clear(); + _clearSelection(); + + // cursor + if (selection.isCollapsed) { + debugPrint('Update cursor'); + _updateCursor(selection.start); + } else { + debugPrint('Update selection'); + _updateSelection(selection); + } + } + + @override + void clearSelection() { + _clearSelection(); + } + + @override + List getNodesInRange(Offset start, [Offset? end]) { + if (end != null) { + return computeNodesInRange(editorState.document.root, start, end); + } else { + final result = computeNodeInOffset(editorState.document.root, start); + if (result != null) { + return [result]; + } + } + return []; + } + + @override + Node? computeNodeInOffset(Node node, Offset offset) { + for (final child in node.children) { + final result = computeNodeInOffset(child, offset); + if (result != null) { + return result; + } + } + if (node.parent != null && node.key != null) { + if (isNodeInOffset(node, offset)) { + return node; + } + } + return null; + } + + @override + List computeNodesInRange(Node node, Offset start, Offset end) { + final result = _computeNodesInRange(node, start, end); + if (start.dy <= end.dy) { + // downward + return result; + } else { + // upward + return result.reversed.toList(growable: false); + } + } + + List _computeNodesInRange(Node node, Offset start, Offset end) { + List result = []; + if (node.parent != null && node.key != null) { + if (isNodeInRange(node, start, end)) { + result.add(node); + } + } + for (final child in node.children) { + result.addAll(computeNodesInRange(child, start, end)); + } + return result; + } + + @override + bool isNodeInOffset(Node node, Offset offset) { + final renderBox = node.renderBox; + if (renderBox != null) { + final boxOffset = renderBox.localToGlobal(Offset.zero); + final boxRect = boxOffset & renderBox.size; + return boxRect.contains(offset); + } + return false; + } + + @override + bool isNodeInRange(Node node, Offset start, Offset end) { + final renderBox = node.renderBox; + if (renderBox != null) { + final rect = Rect.fromPoints(start, end); + final boxOffset = renderBox.localToGlobal(Offset.zero); + final boxRect = boxOffset & renderBox.size; + return rect.overlaps(boxRect); + } + return false; + } + + void _onDoubleTapDown(TapDownDetails details) { + final offset = details.globalPosition; + final nodes = getNodesInRange(offset); + if (nodes.isEmpty) { + editorState.updateCursorSelection(null); + return; + } + final selectable = nodes.first.selectable; + if (selectable == null) { + editorState.updateCursorSelection(null); + return; + } + editorState + .updateCursorSelection(selectable.getWorldBoundaryInOffset(offset)); + } + + void _onTapDown(TapDownDetails details) { + // clear old state. + panStartOffset = null; + panEndOffset = null; + + tapOffset = details.globalPosition; + + final position = hitTest(tapOffset); + if (position == null) { + return; + } + final selection = Selection.collapsed(position); + editorState.updateCursorSelection(selection); + } + + @override + Position? hitTest(Offset? offset) { + if (offset == null) { + editorState.updateCursorSelection(null); + return null; + } + final nodes = getNodesInRange(offset); + if (nodes.isEmpty) { + editorState.updateCursorSelection(null); + return null; + } + assert(nodes.length == 1); + final selectable = nodes.first.selectable; + if (selectable == null) { + editorState.updateCursorSelection(null); + return null; + } + return selectable.getPositionInOffset(offset); + } + + void _onPanStart(DragStartDetails details) { + // clear old state. + panEndOffset = null; + tapOffset = null; + clearSelection(); + + panStartOffset = details.globalPosition; + } + + void _onPanUpdate(DragUpdateDetails details) { + panEndOffset = details.globalPosition; + + final nodes = getNodesInRange(panStartOffset!, panEndOffset!); + if (nodes.isEmpty) { + return; + } + final first = nodes.first.selectable; + final last = nodes.last.selectable; + + // compute the selection in range. + if (first != null && last != null) { + bool isDownward; + if (first == last) { + isDownward = panStartOffset!.dx < panEndOffset!.dx; + } else { + isDownward = panStartOffset!.dy < panEndOffset!.dy; + } + final start = + first.getSelectionInRange(panStartOffset!, panEndOffset!).start; + final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; + final selection = Selection( + start: isDownward ? start : end, end: isDownward ? end : start); + debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); + editorState.updateCursorSelection(selection); + } + } + + void _onPanEnd(DragEndDetails details) { + // do nothing + } + + void _clearSelection() { + currentSelection = null; + currentSelectedNodes.value = []; + + // clear selection + _selectionOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + // clear cursors + _cursorOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + // clear toolbar + editorState.service.toolbarService?.hide(); + } + + void _updateSelection(Selection selection) { + final nodes = _selectedNodesInSelection(editorState.document, selection); + + currentSelection = selection; + currentSelectedNodes.value = nodes; + + Rect? topmostRect; + LayerLink? layerLink; + + var index = 0; + for (final node in nodes) { + final selectable = node.selectable; + if (selectable == null) { + continue; + } + + var newSelection = selection.copy(); + // In the case of multiple selections, + // we need to return a new selection for each selected node individually. + if (!selection.isSingle) { + // <> means selected. + // text: abcdopqr + if (index == 0) { + if (selection.isDownward) { + newSelection = selection.copyWith(end: selectable.end()); + } else { + newSelection = selection.copyWith(start: selectable.start()); + } + } else if (index == nodes.length - 1) { + if (selection.isDownward) { + newSelection = selection.copyWith(start: selectable.start()); + } else { + newSelection = selection.copyWith(end: selectable.end()); + } + } else { + newSelection = selection.copyWith( + start: selectable.start(), + end: selectable.end(), + ); + } + } + + final rects = selectable.getRectsInSelection(newSelection); + + for (final rect in rects) { + // FIXME: Need to compute more precise location. + topmostRect ??= rect; + layerLink ??= node.layerLink; + + _rects.add(_transformRectToGlobal(selectable, rect)); + final overlay = OverlayEntry( + builder: (context) => SelectionWidget( + color: widget.selectionColor, + layerLink: node.layerLink, + rect: rect, + ), + ); + _selectionOverlays.add(overlay); + } + index += 1; + } + Overlay.of(context)?.insertAll(_selectionOverlays); + + if (topmostRect != null && layerLink != null) { + editorState.service.toolbarService + ?.showInOffset(topmostRect.topLeft, layerLink); + } + } + + Rect _transformRectToGlobal(Selectable selectable, Rect r) { + final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top)); + return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height); + } + + void _updateCursor(Position position) { + final node = editorState.document.root.childAtPath(position.path); + + assert(node != null); + if (node == null) { + return; + } + + currentSelection = Selection.collapsed(position); + currentSelectedNodes.value = [node]; + + final selectable = node.selectable; + final rect = selectable?.getCursorRectInPosition(position); + if (rect != null) { + _rects.add(_transformRectToGlobal(selectable!, rect)); + final cursor = OverlayEntry( + builder: ((context) => CursorWidget( + key: _cursorKey, + rect: rect, + color: widget.cursorColor, + layerLink: node.layerLink, + )), + ); + _cursorOverlays.add(cursor); + Overlay.of(context)?.insertAll(_cursorOverlays); + _forceShowCursor(); + } + } + + _forceShowCursor() { + final currentState = _cursorKey.currentState as CursorWidgetState?; + currentState?.show(); + } + + List _selectedNodesInSelection( + StateTree stateTree, Selection selection) { + final startNode = stateTree.nodeAtPath(selection.start.path)!; + final endNode = stateTree.nodeAtPath(selection.end.path)!; + return NodeIterator(stateTree, startNode, endNode).toList(); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart new file mode 100644 index 0000000000..937a16044a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -0,0 +1,33 @@ +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; +import 'package:flowy_editor/service/selection_service.dart'; +import 'package:flutter/material.dart'; + +class FlowyService { + // selection service + final selectionServiceKey = GlobalKey(debugLabel: 'flowy_selection_service'); + FlowySelectionService get selectionService { + assert(selectionServiceKey.currentState != null && + selectionServiceKey.currentState is FlowySelectionService); + return selectionServiceKey.currentState! as FlowySelectionService; + } + + // keyboard service + final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service'); + + // input service + final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service'); + + // render plugin service + late FlowyRenderPlugin renderPluginService; + + // toolbar service + final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); + ToolbarService? get toolbarService { + if (toolbarServiceKey.currentState != null && + toolbarServiceKey.currentState is ToolbarService) { + return toolbarServiceKey.currentState! as ToolbarService; + } + return null; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart new file mode 100644 index 0000000000..feb293aad4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart @@ -0,0 +1,56 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/render/selection/toolbar_widget.dart'; +import 'package:flutter/material.dart'; + +mixin ToolbarService { + /// Show the toolbar widget beside the offset. + void showInOffset(Offset offset, LayerLink layerLink); + + /// Hide the toolbar widget. + void hide(); +} + +class FlowyToolbar extends StatefulWidget { + const FlowyToolbar({ + Key? key, + required this.editorState, + required this.child, + }) : super(key: key); + + final EditorState editorState; + final Widget child; + + @override + State createState() => _FlowyToolbarState(); +} + +class _FlowyToolbarState extends State with ToolbarService { + OverlayEntry? _toolbarOverlay; + + @override + void showInOffset(Offset offset, LayerLink layerLink) { + _toolbarOverlay?.remove(); + _toolbarOverlay = OverlayEntry( + builder: (context) => ToolbarWidget( + editorState: widget.editorState, + layerLink: layerLink, + offset: offset.translate(0, -37.0), + handlers: const {}, + ), + ); + Overlay.of(context)?.insert(_toolbarOverlay!); + } + + @override + void hide() { + _toolbarOverlay?.remove(); + _toolbarOverlay = null; + } + + @override + Widget build(BuildContext context) { + return Container( + child: widget.child, + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart new file mode 100644 index 0000000000..5b543f03a1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart @@ -0,0 +1,145 @@ +import 'dart:collection'; + +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/operation/transaction.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flutter/foundation.dart'; + +/// A [HistoryItem] contains list of operations committed by users. +/// If a [HistoryItem] is not sealed, operations can be added sequentially. +/// Otherwise, the operations should be added to a new [HistoryItem]. +class HistoryItem extends LinkedListEntry { + final List operations = []; + Selection? beforeSelection; + Selection? afterSelection; + bool _sealed = false; + + HistoryItem(); + + seal() { + _sealed = true; + } + + bool get sealed => _sealed; + + add(Operation op) { + operations.add(op); + } + + addAll(Iterable iterable) { + operations.addAll(iterable); + } + + Transaction toTransaction(EditorState state) { + final builder = TransactionBuilder(state); + for (var i = operations.length - 1; i >= 0; i--) { + final operation = operations[i]; + final inverted = operation.invert(); + builder.add(inverted); + } + builder.afterSelection = beforeSelection; + builder.beforeSelection = afterSelection; + return builder.finish(); + } +} + +class FixedSizeStack { + final _list = LinkedList(); + final int maxSize; + + FixedSizeStack(this.maxSize); + + push(HistoryItem stackItem) { + if (_list.length >= maxSize) { + _list.remove(_list.first); + } + _list.add(stackItem); + } + + HistoryItem? pop() { + if (_list.isEmpty) { + return null; + } + final last = _list.last; + + _list.remove(last); + + return last; + } + + clear() { + _list.clear(); + } + + HistoryItem get last => _list.last; + + bool get isEmpty => _list.isEmpty; + + bool get isNonEmpty => _list.isNotEmpty; +} + +class UndoManager { + final FixedSizeStack undoStack; + final FixedSizeStack redoStack; + EditorState? state; + + UndoManager([int stackSize = 20]) + : undoStack = FixedSizeStack(stackSize), + redoStack = FixedSizeStack(stackSize); + + HistoryItem getUndoHistoryItem() { + if (undoStack.isEmpty) { + final item = HistoryItem(); + undoStack.push(item); + return item; + } + final last = undoStack.last; + if (last.sealed) { + redoStack.clear(); + final item = HistoryItem(); + undoStack.push(item); + return item; + } + return last; + } + + undo() { + debugPrint('undo'); + final s = state; + if (s == null) { + return; + } + final historyItem = undoStack.pop(); + if (historyItem == null) { + return; + } + final transaction = historyItem.toTransaction(s); + s.apply( + transaction, + const ApplyOptions( + recordUndo: false, + recordRedo: true, + )); + } + + redo() { + debugPrint('redo'); + final s = state; + if (s == null) { + return; + } + final historyItem = redoStack.pop(); + if (historyItem == null) { + return; + } + final transaction = historyItem.toTransaction(s); + s.apply( + transaction, + const ApplyOptions( + recordUndo: true, + recordRedo: false, + )); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml new file mode 100644 index 0000000000..e3a6aab187 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -0,0 +1,61 @@ +name: flowy_editor +description: A new Flutter package project. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.17.0 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + rich_clipboard: ^1.0.0 + html: ^0.15.0 + flutter_svg: ^1.1.1+1 + provider: ^6.0.3 + +dev_dependencies: + flutter_test: + sdk: flutter + 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: + # To add assets to your package, add an assets section, like this: + assets: + - assets/images/toolbar/ + - assets/images/ + - assets/document.json + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, 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 in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart new file mode 100644 index 0000000000..9a914888d4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -0,0 +1,233 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flowy_editor/document/text_delta.dart'; + +void main() { + group('compose', () { + test('test delta', () { + final delta = Delta([ + TextInsert('Gandalf', { + 'bold': true, + }), + TextInsert(' the '), + TextInsert('Grey', { + 'color': '#ccc', + }) + ]); + + final death = Delta().retain(12).insert("White", { + 'color': '#fff', + }).delete(4); + + final restores = delta.compose(death); + expect(restores.operations, [ + TextInsert('Gandalf', {'bold': true}), + TextInsert(' the '), + TextInsert('White', {'color': '#fff'}), + ]); + }); + test('compose()', () { + final a = Delta().insert('A'); + final b = Delta().insert('B'); + final expected = Delta().insert('B').insert('A'); + expect(a.compose(b), expected); + }); + test('insert + retain', () { + final a = Delta().insert('A'); + final b = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta().insert('A', { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('insert + delete', () { + final a = Delta().insert('A'); + final b = Delta().delete(1); + final expected = Delta(); + expect(a.compose(b), expected); + }); + test('delete + insert', () { + final a = Delta().delete(1); + final b = Delta().insert('B'); + final expected = Delta().insert('B').delete(1); + expect(a.compose(b), expected); + }); + test('delete + retain', () { + final a = Delta().delete(1); + final b = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta().delete(1).retain(1, { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('delete + delete', () { + final a = Delta().delete(1); + final b = Delta().delete(1); + final expected = Delta().delete(2); + expect(a.compose(b), expected); + }); + test('retain + insert', () { + final a = Delta().retain(1, {'color': 'blue'}); + final b = Delta().insert('B'); + final expected = Delta().insert('B').retain(1, { + 'color': 'blue', + }); + expect(a.compose(b), expected); + }); + test('retain + retain', () { + final a = Delta().retain(1, { + 'color': 'blue', + }); + final b = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta().retain(1, { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('retain + delete', () { + final a = Delta().retain(1, { + 'color': 'blue', + }); + final b = Delta().delete(1); + final expected = Delta().delete(1); + expect(a.compose(b), expected); + }); + test('insert in middle of text', () { + final a = Delta().insert('Hello'); + final b = Delta().retain(3).insert('X'); + final expected = Delta().insert('HelXlo'); + expect(a.compose(b), expected); + }); + test('insert and delete ordering', () { + final a = Delta().insert('Hello'); + final b = Delta().insert('Hello'); + final insertFirst = Delta().retain(3).insert('X').delete(1); + final deleteFirst = Delta().retain(3).delete(1).insert('X'); + final expected = Delta().insert('HelXo'); + expect(a.compose(insertFirst), expected); + expect(b.compose(deleteFirst), expected); + }); + test('delete entire text', () { + final a = Delta().retain(4).insert('Hello'); + final b = Delta().delete(9); + final expected = Delta().delete(4); + expect(a.compose(b), expected); + }); + test('retain more than length of text', () { + final a = Delta().insert('Hello'); + final b = Delta().retain(10); + final expected = Delta().insert('Hello'); + expect(a.compose(b), expected); + }); + test('retain start optimization', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .delete(1); + final b = Delta().retain(3).insert('D'); + final expected = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .insert('D') + .delete(1); + expect(a.compose(b), expected); + }); + test('retain end optimization', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}); + final b = Delta().delete(1); + final expected = Delta().insert('B').insert('C', {'bold': true}); + expect(a.compose(b), expected); + }); + test('retain end optimization join', () { + final a = Delta() + .insert('A', {'bold': true}) + .insert('B') + .insert('C', {'bold': true}) + .insert('D') + .insert('E', {'bold': true}) + .insert('F'); + final b = Delta().retain(1).delete(1); + final expected = Delta() + .insert('AC', {'bold': true}) + .insert('D') + .insert('E', {'bold': true}) + .insert('F'); + expect(a.compose(b), expected); + }); + }); + group('invert', () { + test('insert', () { + final delta = Delta().retain(2).insert('A'); + final base = Delta().insert('12346'); + final expected = Delta().retain(2).delete(1); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); + test('delete', () { + final delta = Delta().retain(2).delete(3); + final base = Delta().insert('123456'); + final expected = Delta().retain(2).insert('345'); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); + // test('retain', () { + // final delta = Delta().retain(2).retain(3, {'bold': true}); + // final base = Delta().insert('123456'); + // final expected = Delta().retain(2).retain(3, {'bold': null}); + // final inverted = delta.invert(base); + // expect(expected, inverted); + // expect(base.compose(delta).compose(inverted), base); + // }); + }); + group('json', () { + test('toJson()', () { + final delta = Delta().retain(2).insert('A').delete(3); + expect(delta.toJson(), [ + {'retain': 2}, + {'insert': 'A'}, + {'delete': 3} + ]); + }); + test('attributes', () { + final delta = + Delta().retain(2, {'bold': true}).insert('A', {'italic': true}); + expect(delta.toJson(), [ + { + 'retain': 2, + 'attributes': {'bold': true}, + }, + { + 'insert': 'A', + 'attributes': {'italic': true}, + }, + ]); + }); + test('fromJson()', () { + final delta = Delta.fromJson([ + {'retain': 2}, + {'insert': 'A'}, + {'delete': 3}, + ]); + final expected = Delta().retain(2).insert('A').delete(3); + expect(delta, expected); + }); + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart new file mode 100644 index 0000000000..49d0fd00f5 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -0,0 +1,143 @@ +import 'dart:convert'; + +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('create state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + expect(stateTree.root.type, 'root'); + expect(stateTree.root.toJson(), data['document']); + }); + + test('search node by Path in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final checkBoxNode = stateTree.root.childAtPath([1, 0]); + expect(checkBoxNode != null, true); + final textType = checkBoxNode!.attributes['text-type']; + expect(textType != null, true); + }); + + test('search node by Self in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final checkBoxNode = stateTree.root.childAtPath([1, 0]); + expect(checkBoxNode != null, true); + final textType = checkBoxNode!.attributes['text-type']; + expect(textType != null, true); + final path = checkBoxNode.path; + expect(pathEquals(path, [1, 0]), true); + }); + + test('insert node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final insertNode = Node.fromJson({ + 'type': 'text', + }); + bool result = stateTree.insert([1, 1], [insertNode]); + expect(result, true); + expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true); + }); + + test('delete node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + stateTree.delete([1, 1], 1); + final node = stateTree.nodeAtPath([1, 1]); + expect(node != null, true); + expect(node!.attributes['tag'], '**'); + }); + + test('update node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final attributes = stateTree.update([1, 1], {'text-type': 'heading1'}); + expect(attributes != null, true); + expect(attributes!['text-type'], 'checkbox'); + final updatedNode = stateTree.nodeAtPath([1, 1]); + expect(updatedNode != null, true); + expect(updatedNode!.attributes['text-type'], 'heading1'); + }); + + test('test path utils 1', () { + final path1 = [1]; + final path2 = [1]; + expect(pathEquals(path1, path2), true); + + expect(hashList(path1), hashList(path2)); + }); + + test('test path utils 2', () { + final path1 = [1]; + final path2 = [2]; + expect(pathEquals(path1, path2), false); + + expect(hashList(path1) != hashList(path2), true); + }); + + test('test position comparator', () { + final pos1 = Position(path: [1], offset: 0); + final pos2 = Position(path: [1], offset: 0); + expect(pos1 == pos2, true); + expect(pos1.hashCode == pos2.hashCode, true); + }); + + test('test position comparator with offset', () { + final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100); + final pos2 = Position(path: [1, 1, 1, 1, 1], offset: 100); + expect(pos1, pos2); + expect(pos1.hashCode, pos2.hashCode); + }); + + test('test position comparator false', () { + final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100); + final pos2 = Position(path: [1, 1, 2, 1, 1], offset: 100); + expect(pos1 == pos2, false); + expect(pos1.hashCode == pos2.hashCode, false); + }); + + test('test position comparator with offset false', () { + final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100); + final pos2 = Position(path: [1, 1, 1, 1, 1], offset: 101); + expect(pos1 == pos2, false); + expect(pos1.hashCode == pos2.hashCode, false); + }); + + test('test selection comparator', () { + final pos = Position(path: [0], offset: 0); + final sel = Selection.collapsed(pos); + expect(sel.start, sel.end); + expect(sel.isCollapsed, true); + }); + + test('test selection collapse', () { + final start = Position(path: [0], offset: 0); + final end = Position(path: [0], offset: 10); + final sel = Selection(start: start, end: end); + + final collapsedSelAtStart = sel.collapse(atStart: true); + expect(collapsedSelAtStart.start, start); + expect(collapsedSelAtStart.end, start); + + final collapsedSelAtEnd = sel.collapse(); + expect(collapsedSelAtEnd.start, end); + expect(collapsedSelAtEnd.end, end); + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart new file mode 100644 index 0000000000..339807cea4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -0,0 +1,118 @@ +import 'dart:collection'; + +import 'package:flowy_editor/document/node.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/document/state_tree.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('transform path', () { + test('transform path changed', () { + expect(transformPath([0, 1], [0, 1]), [0, 2]); + expect(transformPath([0, 1], [0, 2]), [0, 3]); + expect(transformPath([0, 1], [0, 2, 7, 8, 9]), [0, 3, 7, 8, 9]); + expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]); + }); + test("transform path not changed", () { + expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]); + expect(transformPath([0, 1, 2], [0, 1]), [0, 1]); + expect(transformPath([1, 1], [1, 0]), [1, 0]); + }); + test("transform path delta", () { + expect(transformPath([0, 1], [0, 1], 5), [0, 6]); + }); + }); + group('transform operation', () { + test('insert + insert', () { + final t = transformOperation( + InsertOperation([0, 1], + [Node(type: "node", attributes: {}, children: LinkedList())]), + InsertOperation([0, 1], + [Node(type: "node", attributes: {}, children: LinkedList())])); + expect(t.path, [0, 2]); + }); + test('delete + delete', () { + final t = transformOperation( + DeleteOperation([0, 1], + [Node(type: "node", attributes: {}, children: LinkedList())]), + DeleteOperation([0, 2], + [Node(type: "node", attributes: {}, children: LinkedList())])); + expect(t.path, [0, 1]); + }); + }); + test('transform transaction builder', () { + final item1 = Node(type: "node", attributes: {}, children: LinkedList()); + final item2 = Node(type: "node", attributes: {}, children: LinkedList()); + final item3 = Node(type: "node", attributes: {}, children: LinkedList()); + final root = Node( + type: "root", + attributes: {}, + children: LinkedList() + ..addAll([ + item1, + item2, + item3, + ])); + final state = EditorState(document: StateTree(root: root)); + + expect(item1.path, [0]); + expect(item2.path, [1]); + expect(item3.path, [2]); + + final tb = TransactionBuilder(state); + tb.deleteNode(item1); + tb.deleteNode(item2); + tb.deleteNode(item3); + final transaction = tb.finish(); + expect(transaction.operations[0].path, [0]); + expect(transaction.operations[1].path, [0]); + expect(transaction.operations[2].path, [0]); + }); + group("toJson", () { + test("insert", () { + final root = Node(type: "root", attributes: {}, children: LinkedList()); + final state = EditorState(document: StateTree(root: root)); + + final item1 = Node(type: "node", attributes: {}, children: LinkedList()); + final tb = TransactionBuilder(state); + tb.insertNode([0], item1); + + final transaction = tb.finish(); + expect(transaction.toJson(), { + "operations": [ + { + "type": "insert-operation", + "path": [0], + "nodes": [item1.toJson()], + } + ] + }); + }); + test("delete", () { + final item1 = Node(type: "node", attributes: {}, children: LinkedList()); + final root = Node( + type: "root", + attributes: {}, + children: LinkedList() + ..addAll([ + item1, + ])); + final state = EditorState(document: StateTree(root: root)); + final tb = TransactionBuilder(state); + tb.deleteNode(item1); + final transaction = tb.finish(); + expect(transaction.toJson(), { + "operations": [ + { + "type": "delete-operation", + "path": [0], + "nodes": [item1.toJson()], + } + ], + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/flowy_infra/lib/language.dart b/frontend/app_flowy/packages/flowy_infra/lib/language.dart index 2b98263cfb..752956b33e 100644 --- a/frontend/app_flowy/packages/flowy_infra/lib/language.dart +++ b/frontend/app_flowy/packages/flowy_infra/lib/language.dart @@ -26,10 +26,14 @@ String languageFromLocale(Locale locale) { } case "hu": return "Magyar"; + case "id": + return "Bahasa"; case "it": return "Italiano"; case "ja": return "日本語"; + case "pl": + return "Polski"; case "pt": return "Português"; case "ru": diff --git a/frontend/app_flowy/packages/flowy_infra/pubspec.lock b/frontend/app_flowy/packages/flowy_infra/pubspec.lock new file mode 100644 index 0000000000..fc492e7344 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_infra/pubspec.lock @@ -0,0 +1,231 @@ +# 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" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + 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: "1.0.4" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "0.22.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + 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" + 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: "0.5.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.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" + textstyle_extensions: + dependency: "direct main" + description: + name: textstyle_extensions + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0-nullsafety" + time: + dependency: "direct main" + description: + name: time + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + uuid: + dependency: "direct main" + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + 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: "5.2.0" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=1.24.0-7.0" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugin_registrant.cc index d0195b3a44..28d352c9d0 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,10 @@ #include "generated_plugin_registrant.h" -#include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flowy_infra_ui_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlowyInfraUIPlugin"); - flowy_infra_ui_plugin_register_with_registrar(flowy_infra_ui_registrar); + flowy_infra_u_i_plugin_register_with_registrar(flowy_infra_ui_registrar); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugins.cmake index 8ba3e19a28..98453e694d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/linux/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flowy_infra_ui ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock new file mode 100644 index 0000000000..eaaa5db680 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock @@ -0,0 +1,341 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + animations: + dependency: transitive + description: + name: animations + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + 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" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + dartz: + dependency: transitive + description: + name: dartz + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0-nullsafety.2" + equatable: + dependency: transitive + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + flowy_infra: + dependency: transitive + description: + path: "../../flowy_infra" + relative: true + source: path + version: "0.0.1" + flowy_infra_ui: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + flowy_infra_ui_platform_interface: + dependency: transitive + description: + path: "../flowy_infra_ui_platform_interface" + relative: true + source: path + version: "0.0.1" + flowy_infra_ui_web: + dependency: transitive + description: + path: "../flowy_infra_ui_web" + 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: "1.0.4" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "0.22.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + lint: + dependency: transitive + description: + name: lint + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.3" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + loading_indicator: + dependency: transitive + description: + name: loading_indicator + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + 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: "0.5.1+1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + provider: + dependency: transitive + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" + 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" + styled_widget: + dependency: transitive + description: + name: styled_widget + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+2" + 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" + textstyle_extensions: + dependency: transitive + description: + name: textstyle_extensions + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0-nullsafety" + time: + dependency: transitive + description: + name: time + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + 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: "5.3.1" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=2.0.0" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/flowy_infra_ui/example/windows/flutter/generated_plugins.cmake index c82a15ca3f..8571d27085 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/windows/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flowy_infra_ui ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock new file mode 100644 index 0000000000..7309cce94a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock @@ -0,0 +1,168 @@ +# 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" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + 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: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + 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" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + plugin_platform_interface: + dependency: "direct main" + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + 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" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=1.17.0" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock new file mode 100644 index 0000000000..bdc4a1ae65 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock @@ -0,0 +1,187 @@ +# 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" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + flowy_infra_ui_platform_interface: + dependency: "direct main" + description: + path: "../flowy_infra_ui_platform_interface" + relative: true + source: path + version: "0.0.1" + flutter: + dependency: transitive + 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: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: "direct main" + 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: "1.0.1" + 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" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + 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" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=1.17.0" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart index 16d3b55f47..0b247267eb 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart @@ -41,20 +41,22 @@ class ListOverlay extends StatelessWidget { return OverlayContainer( constraints: BoxConstraints.tight(Size(width, totalHeight)), padding: padding, - child: Column( - children: [ - ListView.builder( - shrinkWrap: true, - itemBuilder: itemBuilder, - itemCount: itemCount, - controller: controller, - ), - if (footer != null) - Padding( - padding: footer!.padding, - child: footer!.widget, + child: SingleChildScrollView( + child: Column( + children: [ + ListView.builder( + shrinkWrap: true, + itemBuilder: itemBuilder, + itemCount: itemCount, + controller: controller, ), - ], + if (footer != null) + Padding( + padding: footer!.padding, + child: footer!.widget, + ), + ], + ), ), ); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart index 85de2c71a7..96b566b308 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart @@ -18,7 +18,7 @@ class OverlayContainer extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = context.watch(); + final theme = context.watch() ?? AppTheme.fromType(ThemeType.light); return Material( type: MaterialType.transparency, child: Container( diff --git a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock new file mode 100644 index 0000000000..c24e86d22d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock @@ -0,0 +1,327 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + animations: + dependency: "direct main" + description: + name: animations + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + 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" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + dartz: + dependency: "direct main" + description: + name: dartz + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0-nullsafety.2" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + flowy_infra: + dependency: "direct main" + description: + path: "../flowy_infra" + relative: true + source: path + version: "0.0.1" + flowy_infra_ui_platform_interface: + dependency: "direct main" + description: + path: flowy_infra_ui_platform_interface + relative: true + source: path + version: "0.0.1" + flowy_infra_ui_web: + dependency: "direct main" + description: + path: flowy_infra_ui_web + 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: "1.0.4" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "0.22.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + lint: + dependency: transitive + description: + name: lint + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.3" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + loading_indicator: + dependency: "direct main" + description: + name: loading_indicator + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + 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: "0.5.1+1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" + 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" + styled_widget: + dependency: "direct main" + description: + name: styled_widget + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+2" + 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" + textstyle_extensions: + dependency: "direct main" + description: + name: textstyle_extensions + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0-nullsafety" + time: + dependency: transitive + description: + name: time + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + 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: "5.3.1" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=2.0.0" diff --git a/frontend/app_flowy/packages/flowy_sdk/example/pubspec.lock b/frontend/app_flowy/packages/flowy_sdk/example/pubspec.lock new file mode 100644 index 0000000000..ef6b2235e4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_sdk/example/pubspec.lock @@ -0,0 +1,309 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.11" + 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" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + dartz: + dependency: transitive + description: + name: dartz + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0-nullsafety.2" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flowy_sdk: + 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_driver: + dependency: transitive + 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: "1.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.1" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + isolates: + dependency: transitive + description: + name: isolates + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.3+8" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + url: "https://pub.dartlang.org" + source: hosted + version: "1.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" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.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" + sync_http: + dependency: transitive + description: + name: sync_http + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "8.2.2" + webdriver: + dependency: transitive + description: + name: webdriver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=1.17.0" diff --git a/frontend/app_flowy/packages/flowy_sdk/pubspec.lock b/frontend/app_flowy/packages/flowy_sdk/pubspec.lock new file mode 100644 index 0000000000..0539950c64 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_sdk/pubspec.lock @@ -0,0 +1,504 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "20.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + 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" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.7" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.10" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.12.2" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.12" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.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" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + dartz: + dependency: "direct main" + description: + name: dartz + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0-nullsafety.2" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + ffi: + dependency: "direct main" + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + 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: "1.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.1+2" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.1" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.4" + isolates: + dependency: "direct main" + description: + name: isolates + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.3+8" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + logger: + dependency: "direct main" + description: + name: logger + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + 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" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + protobuf: + dependency: "direct main" + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.9" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + 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" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.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" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+3" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" +sdks: + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=1.17.0" diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 958debd9dd..505280115f 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "38.0.0" + version: "42.0.0" analyzer: - dependency: transitive + dependency: "direct overridden" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "3.4.1" + version: "4.3.0" animations: dependency: transitive description: diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 7e9c38cf5b..5e112a26c2 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -86,6 +86,9 @@ dev_dependencies: freezed: bloc_test: ^9.0.2 +dependency_overrides: + analyzer: ">=4.2.0 <5.0.0" + # 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 diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index d452c1a025..643237bfe6 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -754,9 +754,9 @@ dependencies = [ [[package]] name = "faccess" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e039175679baf763ddddf4f76900b92d4dae9411ee88cf42d2f11b976b09e07c" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" dependencies = [ "bitflags", "libc", diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 356ff15897..df070d90f3 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -215,7 +215,7 @@ impl DefaultFolderBuilder { for app in workspace_rev.apps.iter() { for (index, view) in app.belongings.iter().enumerate() { let view_data = if index == 0 { - initial_read_me().to_delta_str() + initial_read_me().json_str() } else { initial_quill_delta_string() }; diff --git a/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs b/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs index b69b303fa1..de461379e1 100644 --- a/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs +++ b/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs @@ -10,7 +10,7 @@ use flowy_sync::{ entities::{revision::Revision, ws_data::ServerRevisionWSData}, }; use lib_infra::future::FutureResult; -use lib_ot::core::PlainTextAttributes; +use lib_ot::core::PhantomAttributes; use parking_lot::RwLock; use std::sync::Arc; @@ -80,7 +80,7 @@ impl FolderEditor { pub(crate) fn apply_change(&self, change: FolderChange) -> FlowyResult<()> { let FolderChange { delta, md5 } = change; let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); - let delta_data = delta.to_delta_bytes(); + let delta_data = delta.json_bytes(); let revision = Revision::new( &self.rev_manager.object_id, base_rev_id, @@ -132,7 +132,7 @@ impl FolderEditor { pub struct FolderRevisionCompactor(); impl RevisionCompactor for FolderRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; - Ok(delta.to_delta_bytes()) + let delta = make_delta_from_revisions::(revisions)?; + Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs index 5676db54da..ffa5219aa8 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs @@ -13,7 +13,7 @@ use flowy_folder_data_model::revision::{AppRevision, TrashRevision, ViewRevision use flowy_revision::disk::{RevisionRecord, RevisionState}; use flowy_revision::mk_text_block_revision_disk_cache; use flowy_sync::{client_folder::FolderPad, entities::revision::Revision}; -use lib_ot::core::PlainTextDeltaBuilder; +use lib_ot::core::TextDeltaBuilder; use std::sync::Arc; use tokio::sync::RwLock; pub use version_1::{app_sql::*, trash_sql::*, v1_impl::V1Transaction, view_sql::*, workspace_sql::*}; @@ -110,7 +110,7 @@ impl FolderPersistence { pub async fn save_folder(&self, user_id: &str, folder_id: &FolderId, folder: FolderPad) -> FlowyResult<()> { let pool = self.database.db_pool()?; let json = folder.to_json()?; - let delta_data = PlainTextDeltaBuilder::new().insert(&json).build().to_delta_bytes(); + let delta_data = TextDeltaBuilder::new().insert(&json).build().json_bytes(); let revision = Revision::initial_revision(user_id, folder_id.as_ref(), delta_data); let record = RevisionRecord { revision, diff --git a/frontend/rust-lib/flowy-folder/src/services/web_socket.rs b/frontend/rust-lib/flowy-folder/src/services/web_socket.rs index 75db905d63..993e556f8a 100644 --- a/frontend/rust-lib/flowy-folder/src/services/web_socket.rs +++ b/frontend/rust-lib/flowy-folder/src/services/web_socket.rs @@ -10,7 +10,7 @@ use flowy_sync::{ }, }; use lib_infra::future::{BoxResultFuture, FutureResult}; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta}; use parking_lot::RwLock; use std::{sync::Arc, time::Duration}; @@ -24,7 +24,7 @@ pub(crate) async fn make_folder_ws_manager( ) -> Arc { let ws_data_provider = Arc::new(WSDataProvider::new(folder_id, Arc::new(rev_manager.clone()))); let resolver = Arc::new(FolderConflictResolver { folder_pad }); - let conflict_controller = ConflictController::::new( + let conflict_controller = ConflictController::::new( user_id, resolver, Arc::new(ws_data_provider.clone()), @@ -55,8 +55,8 @@ struct FolderConflictResolver { folder_pad: Arc>, } -impl ConflictResolver for FolderConflictResolver { - fn compose_delta(&self, delta: PlainTextDelta) -> BoxResultFuture { +impl ConflictResolver for FolderConflictResolver { + fn compose_delta(&self, delta: TextDelta) -> BoxResultFuture { let folder_pad = self.folder_pad.clone(); Box::pin(async move { let md5 = folder_pad.write().compose_remote_delta(delta)?; @@ -64,15 +64,12 @@ impl ConflictResolver for FolderConflictResolver { }) } - fn transform_delta( - &self, - delta: PlainTextDelta, - ) -> BoxResultFuture, FlowyError> { + fn transform_delta(&self, delta: TextDelta) -> BoxResultFuture, FlowyError> { let folder_pad = self.folder_pad.clone(); Box::pin(async move { let read_guard = folder_pad.read(); - let mut server_prime: Option = None; - let client_prime: PlainTextDelta; + let mut server_prime: Option = None; + let client_prime: TextDelta; if read_guard.is_empty() { // Do nothing client_prime = delta; @@ -89,7 +86,7 @@ impl ConflictResolver for FolderConflictResolver { }) } - fn reset_delta(&self, delta: PlainTextDelta) -> BoxResultFuture { + fn reset_delta(&self, delta: TextDelta) -> BoxResultFuture { let folder_pad = self.folder_pad.clone(); Box::pin(async move { let md5 = folder_pad.write().reset_folder(delta)?; diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index df2192c77d..f778186903 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -4,6 +4,13 @@ use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::RowRevision; use std::sync::Arc; +/// [GridBlockPB] contains list of row ids. The rows here does not contain any data, just the id +/// of the row. Check out [GridRowPB] for more details. +/// +/// +/// A grid can have many rows. Rows are therefore grouped into Blocks in order to make +/// things more efficient. +/// | #[derive(Debug, Clone, Default, ProtoBuf)] pub struct GridBlockPB { #[pb(index = 1)] @@ -22,6 +29,7 @@ impl GridBlockPB { } } +/// [GridRowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row. #[derive(Debug, Default, Clone, ProtoBuf)] pub struct GridRowPB { #[pb(index = 1)] @@ -81,6 +89,8 @@ impl std::convert::From> for RepeatedRowPB { Self { items } } } + +/// [RepeatedGridBlockPB] contains list of [GridBlockPB] #[derive(Debug, Default, ProtoBuf)] pub struct RepeatedGridBlockPB { #[pb(index = 1)] @@ -194,6 +204,8 @@ impl GridBlockChangesetPB { } } +/// [QueryGridBlocksPayloadPB] is used to query the data of the block that belongs to the grid whose +/// id is grid_id. #[derive(ProtoBuf, Default)] pub struct QueryGridBlocksPayloadPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs index 11cbdeb88e..1b86eb1e65 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs @@ -1,4 +1,3 @@ -use crate::entities::{FieldIdentifierParams, GridFieldIdentifierPayloadPB}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -8,40 +7,38 @@ use std::collections::HashMap; #[derive(ProtoBuf, Default)] pub struct CreateSelectOptionPayloadPB { #[pb(index = 1)] - pub field_identifier: GridFieldIdentifierPayloadPB, + pub field_id: String, #[pb(index = 2)] + pub grid_id: String, + + #[pb(index = 3)] pub option_name: String, } pub struct CreateSelectOptionParams { - pub field_identifier: FieldIdentifierParams, + pub field_id: String, + pub grid_id: String, pub option_name: String, } -impl std::ops::Deref for CreateSelectOptionParams { - type Target = FieldIdentifierParams; - - fn deref(&self) -> &Self::Target { - &self.field_identifier - } -} - impl TryInto for CreateSelectOptionPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { let option_name = NotEmptyStr::parse(self.option_name).map_err(|_| ErrorCode::SelectOptionNameIsEmpty)?; - let field_identifier = self.field_identifier.try_into()?; + let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; Ok(CreateSelectOptionParams { - field_identifier, + field_id: field_id.0, option_name: option_name.0, + grid_id: grid_id.0, }) } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct GridCellIdentifierPayloadPB { +pub struct GridCellIdPB { #[pb(index = 1)] pub grid_id: String, @@ -52,20 +49,20 @@ pub struct GridCellIdentifierPayloadPB { pub row_id: String, } -pub struct CellIdentifierParams { +pub struct GridCellIdParams { pub grid_id: String, pub field_id: String, pub row_id: String, } -impl TryInto for GridCellIdentifierPayloadPB { +impl TryInto for GridCellIdPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - Ok(CellIdentifierParams { + Ok(GridCellIdParams { grid_id: grid_id.0, field_id: field_id.0, row_id: row_id.0, @@ -122,6 +119,7 @@ impl std::convert::From> for RepeatedCellPB { } } +/// #[derive(Debug, Clone, Default, ProtoBuf)] pub struct CellChangesetPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs index c769b4f08b..dce77787c3 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use strum_macros::{Display, EnumCount as EnumCountMacro, EnumIter, EnumString}; +/// [GridFieldPB] defines a Field's attributes. Such as the name, field_type, and width. etc. #[derive(Debug, Clone, Default, ProtoBuf)] pub struct GridFieldPB { #[pb(index = 1)] @@ -56,6 +57,8 @@ impl std::convert::From> for GridFieldPB { GridFieldPB::from(field_rev) } } + +/// [GridFieldIdPB] id of the [Field] #[derive(Debug, Clone, Default, ProtoBuf)] pub struct GridFieldIdPB { #[pb(index = 1)] @@ -155,6 +158,41 @@ pub struct GetEditFieldContextPayloadPB { pub field_type: FieldType, } +#[derive(Debug, Default, ProtoBuf)] +pub struct CreateFieldPayloadPB { + #[pb(index = 1)] + pub grid_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] + pub create_if_not_exist: bool, +} + +pub struct CreateFieldParams { + pub grid_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +impl TryInto for CreateFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(CreateFieldParams { + grid_id: grid_id.0, + field_id: field_id.0, + field_type: self.field_type, + }) + } +} + #[derive(Debug, Default, ProtoBuf)] pub struct EditFieldPayloadPB { #[pb(index = 1)] @@ -190,24 +228,45 @@ impl TryInto for EditFieldPayloadPB { } } -pub struct CreateFieldParams { +#[derive(Debug, Default, ProtoBuf)] +pub struct GridFieldTypeOptionIdPB { + #[pb(index = 1)] pub grid_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] pub field_type: FieldType, } -impl TryInto for EditFieldPayloadPB { +pub struct GridFieldTypeOptionIdParams { + pub grid_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +impl TryInto for GridFieldTypeOptionIdPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; - - Ok(CreateFieldParams { + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(GridFieldTypeOptionIdParams { grid_id: grid_id.0, + field_id: field_id.0, field_type: self.field_type, }) } } +/// Certain field types have user-defined options such as color, date format, number format, +/// or a list of values for a multi-select list. These options are defined within a specialization +/// of the FieldTypeOption class. +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid#fieldtype) +/// for more information. +/// #[derive(Debug, Default, ProtoBuf)] pub struct FieldTypeOptionDataPB { #[pb(index = 1)] @@ -220,6 +279,7 @@ pub struct FieldTypeOptionDataPB { pub type_option_data: Vec, } +/// Collection of the [GridFieldPB] #[derive(Debug, Default, ProtoBuf)] pub struct RepeatedGridFieldPB { #[pb(index = 1)] @@ -315,6 +375,7 @@ impl TryInto for InsertFieldPayloadPB { } } +/// [UpdateFieldTypeOptionPayloadPB] is used to update the type option data. #[derive(ProtoBuf, Default)] pub struct UpdateFieldTypeOptionPayloadPB { #[pb(index = 1)] @@ -323,6 +384,7 @@ pub struct UpdateFieldTypeOptionPayloadPB { #[pb(index = 2)] pub field_id: String, + /// Check out [FieldTypeOptionDataPB] for more details. #[pb(index = 3)] pub type_option_data: Vec, } @@ -375,6 +437,12 @@ impl TryInto for QueryFieldPayloadPB { } } +/// [FieldChangesetPayloadPB] is used to modify the corresponding field. It defines which properties of +/// the field can be modified. +/// +/// Pass in None if you don't want to modify a property +/// Pass in Some(Value) if you want to modify a property +/// #[derive(Debug, Clone, Default, ProtoBuf)] pub struct FieldChangesetPayloadPB { #[pb(index = 1)] @@ -556,6 +624,15 @@ impl std::convert::From for FieldType { } } } +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DuplicateFieldPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub grid_id: String, +} + #[derive(Debug, Clone, Default, ProtoBuf)] pub struct GridFieldIdentifierPayloadPB { #[pb(index = 1)] @@ -565,20 +642,42 @@ pub struct GridFieldIdentifierPayloadPB { pub grid_id: String, } -pub struct FieldIdentifierParams { - pub field_id: String, - pub grid_id: String, -} - -impl TryInto for GridFieldIdentifierPayloadPB { +impl TryInto for DuplicateFieldPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; - Ok(FieldIdentifierParams { + Ok(GridFieldIdParams { grid_id: grid_id.0, field_id: field_id.0, }) } } + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DeleteFieldPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub grid_id: String, +} + +impl TryInto for DeleteFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(GridFieldIdParams { + grid_id: grid_id.0, + field_id: field_id.0, + }) + } +} + +pub struct GridFieldIdParams { + pub field_id: String, + pub grid_id: String, +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs index 1be0412503..49278afc54 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs @@ -2,6 +2,8 @@ use crate::entities::{GridBlockPB, GridFieldIdPB}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; + +/// [GridPB] describes how many fields and blocks the grid has #[derive(Debug, Clone, Default, ProtoBuf)] pub struct GridPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs index f66e1c1d06..745a5dc368 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs @@ -2,18 +2,6 @@ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -#[derive(ProtoBuf, Default)] -pub struct GridRowIdPayloadPB { - #[pb(index = 1)] - pub grid_id: String, - - #[pb(index = 2)] - pub block_id: String, - - #[pb(index = 3)] - pub row_id: String, -} - #[derive(Debug, Default, Clone, ProtoBuf)] pub struct GridRowIdPB { #[pb(index = 1)] @@ -26,15 +14,21 @@ pub struct GridRowIdPB { pub row_id: String, } -impl TryInto for GridRowIdPayloadPB { +pub struct GridRowIdParams { + pub grid_id: String, + pub block_id: String, + pub row_id: String, +} + +impl TryInto for GridRowIdPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let block_id = NotEmptyStr::parse(self.block_id).map_err(|_| ErrorCode::BlockIdIsEmpty)?; let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - Ok(GridRowIdPB { + Ok(GridRowIdParams { grid_id: grid_id.0, block_id: block_id.0, row_id: row_id.0, diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index 9272cfe46d..3564886c4a 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -12,6 +12,7 @@ use std::convert::TryInto; use strum::IntoEnumIterator; use strum_macros::EnumIter; +/// [GridSettingPB] defines the setting options for the grid. Such as the filter, group, and sort. #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct GridSettingPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 4c4bfe5559..b0ef43f5ad 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -113,10 +113,10 @@ pub(crate) async fn update_field_type_option_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn delete_field_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: FieldIdentifierParams = data.into_inner().try_into()?; + let params: GridFieldIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.delete_field(¶ms.field_id).await?; Ok(()) @@ -151,10 +151,10 @@ pub(crate) async fn switch_to_field_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn duplicate_field_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: FieldIdentifierParams = data.into_inner().try_into()?; + let params: GridFieldIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.duplicate_field(¶ms.field_id).await?; Ok(()) @@ -163,10 +163,10 @@ pub(crate) async fn duplicate_field_handler( /// Return the FieldTypeOptionData if the Field exists otherwise return record not found error. #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn get_field_type_option_data_handler( - data: Data, + data: Data, manager: AppData>, ) -> DataResult { - let params: EditFieldParams = data.into_inner().try_into()?; + let params: GridFieldTypeOptionIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_field_rev(¶ms.field_id).await { None => Err(FlowyError::record_not_found()), @@ -186,7 +186,7 @@ pub(crate) async fn get_field_type_option_data_handler( /// Create FieldMeta and save it. Return the FieldTypeOptionData. #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn create_field_type_option_data_handler( - data: Data, + data: Data, manager: AppData>, ) -> DataResult { let params: CreateFieldParams = data.into_inner().try_into()?; @@ -227,10 +227,10 @@ async fn get_type_option_data(field_rev: &FieldRevision, field_type: &FieldType) #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn get_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> DataResult { - let params: GridRowIdPB = data.into_inner().try_into()?; + let params: GridRowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let row = editor .get_row_rev(¶ms.row_id) @@ -242,10 +242,10 @@ pub(crate) async fn get_row_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn delete_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: GridRowIdPB = data.into_inner().try_into()?; + let params: GridRowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.delete_row(¶ms.row_id).await?; Ok(()) @@ -253,10 +253,10 @@ pub(crate) async fn delete_row_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn duplicate_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: GridRowIdPB = data.into_inner().try_into()?; + let params: GridRowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.duplicate_row(¶ms.row_id).await?; Ok(()) @@ -275,10 +275,10 @@ pub(crate) async fn create_row_handler( // #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_cell_handler( - data: Data, + data: Data, manager: AppData>, ) -> DataResult { - let params: CellIdentifierParams = data.into_inner().try_into()?; + let params: GridCellIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_cell(¶ms).await { None => data_result(GridCellPB::empty(¶ms.field_id)), @@ -357,10 +357,10 @@ pub(crate) async fn update_select_option_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn get_select_option_handler( - data: Data, + data: Data, manager: AppData>, ) -> DataResult { - let params: CellIdentifierParams = data.into_inner().try_into()?; + let params: GridCellIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_field_rev(¶ms.field_id).await { None => { diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 5abda48c1b..0855ef0032 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -42,81 +42,166 @@ pub fn create(grid_manager: Arc) -> Module { module } +/// [GridEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf) +/// out, it includes how to use these annotations: input, output, etc. #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum GridEvent { + /// [GetGrid] event is used to get the [GridPB] + /// + /// The event handler accepts a [GridIdPB] and returns a [GridPB] if there are no errors. #[event(input = "GridIdPB", output = "GridPB")] GetGrid = 0, + /// [GetGridBlocks] event is used to get the grid's block. + /// + /// The event handler accepts a [QueryGridBlocksPayloadPB] and returns a [RepeatedGridBlockPB] + /// if there are no errors. #[event(input = "QueryGridBlocksPayloadPB", output = "RepeatedGridBlockPB")] GetGridBlocks = 1, + /// [GetGridSetting] event is used to get the grid's settings. + /// + /// The event handler accepts [GridIdPB] and return [GridSettingPB] + /// if there is no errors. #[event(input = "GridIdPB", output = "GridSettingPB")] GetGridSetting = 2, + /// [UpdateGridSetting] event is used to update the grid's settings. + /// + /// The event handler accepts [GridIdPB] and return errors if failed to modify the grid's settings. #[event(input = "GridIdPB", input = "GridSettingChangesetPayloadPB")] UpdateGridSetting = 3, + /// [GetFields] event is used to get the grid's settings. + /// + /// The event handler accepts a [QueryFieldPayloadPB] and returns a [RepeatedGridFieldPB] + /// if there are no errors. #[event(input = "QueryFieldPayloadPB", output = "RepeatedGridFieldPB")] GetFields = 10, + /// [UpdateField] event is used to update a field's attributes. + /// + /// The event handler accepts a [FieldChangesetPayloadPB] and returns errors if failed to modify the + /// field. #[event(input = "FieldChangesetPayloadPB")] UpdateField = 11, + /// [UpdateFieldTypeOption] event is used to update the field's type option data. Certain field + /// types have user-defined options such as color, date format, number format, or a list of values + /// for a multi-select list. These options are defined within a specialization of the + /// FieldTypeOption class. + /// + /// Check out [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid#fieldtype) + /// for more information. + /// + /// The event handler accepts a [UpdateFieldTypeOptionPayloadPB] and returns errors if failed to modify the + /// field. #[event(input = "UpdateFieldTypeOptionPayloadPB")] UpdateFieldTypeOption = 12, + /// [InsertField] event is used to insert a new Field. If the Field already exists, the event + /// handler will replace the value with the new Field value. #[event(input = "InsertFieldPayloadPB")] InsertField = 13, - #[event(input = "GridFieldIdentifierPayloadPB")] + /// [DeleteField] event is used to delete a Field. [DeleteFieldPayloadPB] is the context that + /// is used to delete the field from the Grid. + #[event(input = "DeleteFieldPayloadPB")] DeleteField = 14, + /// [SwitchToField] event is used to update the current Field's type. + /// It will insert a new FieldTypeOptionData if the new FieldType doesn't exist before, otherwise + /// reuse the existing FieldTypeOptionData. You could check the [GridRevisionPad] for more details. #[event(input = "EditFieldPayloadPB", output = "FieldTypeOptionDataPB")] SwitchToField = 20, - #[event(input = "GridFieldIdentifierPayloadPB")] + /// [DuplicateField] event is used to duplicate a Field. The duplicated field data is kind of + /// deep copy of the target field. The passed in [DuplicateFieldPayloadPB] is the context that is + /// used to duplicate the field. + /// + /// Return errors if failed to duplicate the field. + /// + #[event(input = "DuplicateFieldPayloadPB")] DuplicateField = 21, + /// [MoveItem] event is used to move an item. For the moment, Item has two types defined in + /// [MoveItemTypePB]. #[event(input = "MoveItemPayloadPB")] MoveItem = 22, - #[event(input = "EditFieldPayloadPB", output = "FieldTypeOptionDataPB")] + /// [GetFieldTypeOption] event is used to get the FieldTypeOption data for a specific field type. + /// + /// Check out the [FieldTypeOptionDataPB] for more details. If the [FieldTypeOptionData] does exist + /// for the target type, the [TypeOptionBuilder] will create the default data for that type. + /// + /// Return the [FieldTypeOptionDataPB] if there are no errors. + #[event(input = "GridFieldTypeOptionIdPB", output = "FieldTypeOptionDataPB")] GetFieldTypeOption = 23, - #[event(input = "EditFieldPayloadPB", output = "FieldTypeOptionDataPB")] + /// [CreateFieldTypeOption] event is used to create a new FieldTypeOptionData. + #[event(input = "CreateFieldPayloadPB", output = "FieldTypeOptionDataPB")] CreateFieldTypeOption = 24, + /// [NewSelectOption] event is used to create a new select option. Returns a [SelectOptionPB] if + /// there are no errors. #[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")] NewSelectOption = 30, - #[event(input = "GridCellIdentifierPayloadPB", output = "SelectOptionCellDataPB")] + /// [GetSelectOptionCellData] event is used to get the select option data for cell editing. + /// [GridCellIdPB] locate which cell data that will be read from. The return value, [SelectOptionCellDataPB] + /// contains the available options and the currently selected options. + #[event(input = "GridCellIdPB", output = "SelectOptionCellDataPB")] GetSelectOptionCellData = 31, + /// [UpdateSelectOption] event is used to update a FieldTypeOptionData whose field_type is + /// FieldType::SingleSelect or FieldType::MultiSelect. + /// + /// This event may trigger the GridNotification::DidUpdateCell event. + /// For example, GridNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPayloadPB] + /// carries a change that updates the name of the option. #[event(input = "SelectOptionChangesetPayloadPB")] UpdateSelectOption = 32, #[event(input = "CreateRowPayloadPB", output = "GridRowPB")] CreateRow = 50, - #[event(input = "GridRowIdPayloadPB", output = "OptionalRowPB")] + /// [GetRow] event is used to get the row data,[GridRowPB]. [OptionalRowPB] is a wrapper that enables + /// to return a nullable row data. + #[event(input = "GridRowIdPB", output = "OptionalRowPB")] GetRow = 51, - #[event(input = "GridRowIdPayloadPB")] + #[event(input = "GridRowIdPB")] DeleteRow = 52, - #[event(input = "GridRowIdPayloadPB")] + #[event(input = "GridRowIdPB")] DuplicateRow = 53, - #[event(input = "GridCellIdentifierPayloadPB", output = "GridCellPB")] + #[event(input = "GridCellIdPB", output = "GridCellPB")] GetCell = 70, + /// [UpdateCell] event is used to update the cell content. The passed in data, [CellChangesetPB], + /// carries the changes that will be applied to the cell content by calling `update_cell` function. + /// + /// The 'content' property of the [CellChangesetPB] is a String type. It can be used directly if the + /// cell uses string data. For example, the TextCell or NumberCell. + /// + /// But,it can be treated as a generic type, because we can use [serde] to deserialize the string + /// into a specific data type. For the moment, the 'content' will be deserialized to a concrete type + /// when the FieldType is SingleSelect, DateTime, and MultiSelect. Please see + /// the [UpdateSelectOptionCell] and [UpdateDateCell] events for more details. #[event(input = "CellChangesetPB")] UpdateCell = 71, + /// [UpdateSelectOptionCell] event is used to update a select option cell's data. [SelectOptionCellChangesetPayloadPB] + /// contains options that will be deleted or inserted. It can be cast to [CellChangesetPB] that + /// will be used by the `update_cell` function. #[event(input = "SelectOptionCellChangesetPayloadPB")] UpdateSelectOptionCell = 72, + /// [UpdateDateCell] event is used to update a date cell's data. [DateChangesetPayloadPB] + /// contains the date and the time string. It can be cast to [CellChangesetPB] that + /// will be used by the `update_cell` function. #[event(input = "DateChangesetPayloadPB")] UpdateDateCell = 80, } diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index 320f18b560..19a3669c46 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -192,7 +192,7 @@ pub async fn make_grid_view_data( // Create grid's block let grid_block_delta = make_grid_block_delta(block_meta_data); - let block_delta_data = grid_block_delta.to_delta_bytes(); + let block_delta_data = grid_block_delta.json_bytes(); let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, block_id, block_delta_data).into(); let _ = grid_manager.create_grid_block(&block_id, repeated_revision).await?; @@ -202,7 +202,7 @@ pub async fn make_grid_view_data( // Create grid let grid_meta_delta = make_grid_delta(&grid_rev); - let grid_delta_data = grid_meta_delta.to_delta_bytes(); + let grid_delta_data = grid_meta_delta.json_bytes(); let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, grid_delta_data.clone()).into(); let _ = grid_manager.create_grid(view_id, repeated_revision).await?; diff --git a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs b/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs index f74075ec6e..a9f68c5776 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs @@ -7,7 +7,7 @@ use flowy_sync::client_grid::{GridBlockMetaChange, GridBlockRevisionPad}; use flowy_sync::entities::revision::Revision; use flowy_sync::util::make_delta_from_revisions; use lib_infra::future::FutureResult; -use lib_ot::core::PlainTextAttributes; +use lib_ot::core::PhantomAttributes; use std::borrow::Cow; use std::sync::Arc; use tokio::sync::RwLock; @@ -161,7 +161,7 @@ impl GridBlockRevisionEditor { let GridBlockMetaChange { delta, md5 } = change; let user_id = self.user_id.clone(); let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); - let delta_data = delta.to_delta_bytes(); + let delta_data = delta.json_bytes(); let revision = Revision::new( &self.rev_manager.object_id, base_rev_id, @@ -200,7 +200,7 @@ impl RevisionObjectBuilder for GridBlockMetaPadBuilder { pub struct GridBlockRevisionCompactor(); impl RevisionCompactor for GridBlockRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; - Ok(delta.to_delta_bytes()) + let delta = make_delta_from_revisions::(revisions)?; + Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs index 1a60a0a5a8..ddd1ba049d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs @@ -1,30 +1,44 @@ #[cfg(test)] mod tests { - use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data}; - use crate::services::field::type_options::checkbox_type_option::{NO, YES}; - use crate::services::field::FieldBuilder; - use crate::entities::FieldType; + use crate::services::cell::CellDataOperation; + use crate::services::field::type_options::checkbox_type_option::*; + use crate::services::field::FieldBuilder; + use flowy_grid_data_model::revision::FieldRevision; #[test] fn checkout_box_description_test() { - let field_rev = FieldBuilder::from_field_type(&FieldType::Checkbox).build(); - let data = apply_cell_data_changeset("true", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev).to_string(), YES); + let type_option = CheckboxTypeOption::default(); + let field_type = FieldType::Checkbox; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); - let data = apply_cell_data_changeset("1", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES); + // the checkout value will be checked if the value is "1", "true" or "yes" + assert_checkbox(&type_option, "1", CHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "true", CHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "yes", CHECK, &field_type, &field_rev); - let data = apply_cell_data_changeset("yes", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES); + // the checkout value will be uncheck if the value is "false" or "No" + assert_checkbox(&type_option, "false", UNCHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "No", UNCHECK, &field_type, &field_rev); - let data = apply_cell_data_changeset("false", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); + // the checkout value will be empty if the value is letters or empty string + assert_checkbox(&type_option, "abc", "", &field_type, &field_rev); + assert_checkbox(&type_option, "", "", &field_type, &field_rev); + } - let data = apply_cell_data_changeset("no", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); - - let data = apply_cell_data_changeset("12", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), ""); + fn assert_checkbox( + type_option: &CheckboxTypeOption, + input_str: &str, + expected_str: &str, + field_type: &FieldType, + field_rev: &FieldRevision, + ) { + assert_eq!( + type_option + .decode_cell_data(input_str.to_owned().into(), field_type, field_rev) + .unwrap() + .to_string(), + expected_str.to_owned() + ); } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs index 233c481eb6..3da9ebd23f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs @@ -3,14 +3,14 @@ use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use std::str::FromStr; -pub const YES: &str = "Yes"; -pub const NO: &str = "No"; +pub const CHECK: &str = "Yes"; +pub const UNCHECK: &str = "No"; pub struct CheckboxCellData(String); impl CheckboxCellData { pub fn is_check(&self) -> bool { - self.0 == YES + self.0 == CHECK } } @@ -36,8 +36,8 @@ impl FromStr for CheckboxCellData { }; match val { - Some(true) => Ok(Self(YES.to_string())), - Some(false) => Ok(Self(NO.to_string())), + Some(true) => Ok(Self(CHECK.to_string())), + Some(false) => Ok(Self(UNCHECK.to_string())), None => Ok(Self("".to_string())), } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs index 5bb2b1c415..1229a8f5e1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs @@ -1,29 +1,12 @@ #[cfg(test)] mod tests { use crate::entities::FieldType; - use crate::services::cell::{CellDataChangeset, CellDataOperation}; + use crate::services::cell::CellDataOperation; use crate::services::field::*; // use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat}; use flowy_grid_data_model::revision::FieldRevision; use strum::IntoEnumIterator; - #[test] - fn date_type_option_invalid_input_test() { - let type_option = DateTypeOption::default(); - let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some("1e".to_string()), - time: Some("23:00".to_owned()), - }, - &field_type, - &field_rev, - "", - ); - } - #[test] fn date_type_option_date_format_test() { let mut type_option = DateTypeOption::default(); @@ -32,23 +15,23 @@ mod tests { type_option.date_format = date_format; match date_format { DateFormat::Friendly => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "Mar 14,2022"); + assert_date(&type_option, 1647251762, None, "Mar 14,2022", &field_rev); } DateFormat::US => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14"); + assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev); } DateFormat::ISO => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022-03-14"); + assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev); } DateFormat::Local => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14"); + assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev); } } } } #[test] - fn date_type_option_time_format_test() { + fn date_type_option_different_time_format_test() { let mut type_option = DateTypeOption::default(); let field_type = FieldType::DateTime; let field_rev = FieldBuilder::from_field_type(&field_type).build(); @@ -58,59 +41,23 @@ mod tests { type_option.include_time = true; match time_format { TimeFormat::TwentyFourHour => { - assert_changeset_result( + assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev); + assert_date( &type_option, - DateCellChangesetPB { - date: Some(1653609600.to_string()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(1653609600.to_string()), - time: Some("23:00".to_owned()), - }, - &field_type, - &field_rev, + 1653609600, + Some("23:00".to_owned()), "May 27,2022 23:00", + &field_rev, ); } TimeFormat::TwelveHour => { - assert_changeset_result( + assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev); + assert_date( &type_option, - DateCellChangesetPB { - date: Some(1653609600.to_string()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - // - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(1653609600.to_string()), - time: Some("".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022", - ); - - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(1653609600.to_string()), - time: Some("11:23 pm".to_owned()), - }, - &field_type, - &field_rev, + 1653609600, + Some("11:23 pm".to_owned()), "May 27,2022 11:23 PM", + &field_rev, ); } } @@ -118,141 +65,71 @@ mod tests { } #[test] - fn date_type_option_apply_changeset_test() { - let mut type_option = DateTypeOption::new(); + fn date_type_option_invalid_date_str_test() { + let type_option = DateTypeOption::default(); let field_type = FieldType::DateTime; let field_rev = FieldBuilder::from_field_type(&field_type).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(date_timestamp.clone()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); + assert_date(&type_option, "abc", None, "", &field_rev); + } + #[test] + #[should_panic] + fn date_type_option_invalid_include_time_str_test() { + let mut type_option = DateTypeOption::new(); type_option.include_time = true; - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(date_timestamp.clone()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - assert_changeset_result( + assert_date( &type_option, - DateCellChangesetPB { - date: Some(date_timestamp.clone()), - time: Some("1:00".to_owned()), - }, - &field_type, - &field_rev, + 1653609600, + Some("1:".to_owned()), "May 27,2022 01:00", - ); - - type_option.time_format = TimeFormat::TwelveHour; - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(date_timestamp), - time: Some("1:00 am".to_owned()), - }, - &field_type, &field_rev, + ); + } + + #[test] + fn date_type_option_empty_include_time_str_test() { + let mut type_option = DateTypeOption::new(); + type_option.include_time = true; + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + + assert_date(&type_option, 1653609600, Some("".to_owned()), "May 27,2022", &field_rev); + } + + /// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error. + #[test] + #[should_panic] + fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() { + let mut type_option = DateTypeOption::new(); + type_option.include_time = true; + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + + assert_date( + &type_option, + 1653609600, + Some("1:00 am".to_owned()), "May 27,2022 01:00 AM", + &field_rev, ); } - - #[test] - #[should_panic] - fn date_type_option_apply_changeset_error_test() { - let mut type_option = DateTypeOption::new(); - type_option.include_time = true; - let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(date_timestamp.clone()), - time: Some("1:".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(date_timestamp), - time: Some("1:00".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - } - - #[test] - #[should_panic] - fn date_type_option_twelve_hours_to_twenty_four_hours() { - let mut type_option = DateTypeOption::new(); - type_option.include_time = true; - let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangesetPB { - date: Some(date_timestamp), - time: Some("1:00 am".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - } - - fn assert_changeset_result( + fn assert_date( type_option: &DateTypeOption, - changeset: DateCellChangesetPB, - _field_type: &FieldType, + timestamp: T, + include_time_str: Option, + expected_str: &str, field_rev: &FieldRevision, - expected: &str, - ) { - let changeset = CellDataChangeset(Some(changeset)); - let encoded_data = type_option.apply_changeset(changeset, None).unwrap(); - assert_eq!( - expected.to_owned(), - decode_cell_data(encoded_data, type_option, field_rev) - ); - } - - fn assert_decode_timestamp( - timestamp: i64, - type_option: &DateTypeOption, - field_rev: &FieldRevision, - expected: &str, ) { let s = serde_json::to_string(&DateCellChangesetPB { date: Some(timestamp.to_string()), - time: None, + time: include_time_str, }) .unwrap(); let encoded_data = type_option.apply_changeset(s.into(), None).unwrap(); assert_eq!( - expected.to_owned(), - decode_cell_data(encoded_data, type_option, field_rev) + decode_cell_data(encoded_data, type_option, field_rev), + expected_str.to_owned(), ); } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs index aa8fab221a..1c54606f75 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -1,5 +1,5 @@ use crate::entities::CellChangesetPB; -use crate::entities::{CellIdentifierParams, GridCellIdentifierPayloadPB}; +use crate::entities::{GridCellIdPB, GridCellIdParams}; use crate::services::cell::{CellBytesParser, FromCellChangeset, FromCellString}; use bytes::Bytes; @@ -24,7 +24,7 @@ pub struct DateCellDataPB { #[derive(Clone, Debug, Default, ProtoBuf)] pub struct DateChangesetPayloadPB { #[pb(index = 1)] - pub cell_identifier: GridCellIdentifierPayloadPB, + pub cell_identifier: GridCellIdPB, #[pb(index = 2, one_of)] pub date: Option, @@ -34,7 +34,7 @@ pub struct DateChangesetPayloadPB { } pub struct DateChangesetParams { - pub cell_identifier: CellIdentifierParams, + pub cell_identifier: GridCellIdParams, pub date: Option, pub time: Option, } @@ -43,7 +43,7 @@ impl TryInto for DateChangesetPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { - let cell_identifier: CellIdentifierParams = self.cell_identifier.try_into()?; + let cell_identifier: GridCellIdParams = self.cell_identifier.try_into()?; Ok(DateChangesetParams { cell_identifier, date: self.date, diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs index 6ea1e8f302..41132ecfe4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs @@ -7,25 +7,30 @@ mod tests { use flowy_grid_data_model::revision::FieldRevision; use strum::IntoEnumIterator; + /// Testing when the input is not a number. #[test] fn number_type_option_invalid_input_test() { let type_option = NumberTypeOption::default(); let field_type = FieldType::Number; let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_equal(&type_option, "", "", &field_type, &field_rev); - assert_equal(&type_option, "abc", "", &field_type, &field_rev); + + // Input is empty String + assert_number(&type_option, "", "", &field_type, &field_rev); + + // Input is letter + assert_number(&type_option, "abc", "", &field_type, &field_rev); } + /// Testing the strip_currency_symbol function. It should return the string without the input symbol. #[test] fn number_type_option_strip_symbol_test() { - let mut type_option = NumberTypeOption::new(); - type_option.format = NumberFormat::USD; + // Remove the $ symbol assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned()); - - type_option.format = NumberFormat::Yuan; - assert_eq!(strip_currency_symbol("$0.2"), "0.2".to_owned()); + // Remove the ¥ symbol + assert_eq!(strip_currency_symbol("¥0.2"), "0.2".to_owned()); } + /// Format the input number to the corresponding format string. #[test] fn number_type_option_format_number_test() { let mut type_option = NumberTypeOption::default(); @@ -36,25 +41,26 @@ mod tests { type_option.format = format; match format { NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); } NumberFormat::USD => { - assert_equal(&type_option, "18443", "$18,443", &field_type, &field_rev); + assert_number(&type_option, "18443", "$18,443", &field_type, &field_rev); } NumberFormat::Yen => { - assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_rev); + assert_number(&type_option, "18443", "¥18,443", &field_type, &field_rev); } NumberFormat::Yuan => { - assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_rev); + assert_number(&type_option, "18443", "CN¥18,443", &field_type, &field_rev); } NumberFormat::EUR => { - assert_equal(&type_option, "18443", "€18.443", &field_type, &field_rev); + assert_number(&type_option, "18443", "€18.443", &field_type, &field_rev); } _ => {} } } } + /// Format the input String to the corresponding format string. #[test] fn number_type_option_format_str_test() { let mut type_option = NumberTypeOption::default(); @@ -65,33 +71,34 @@ mod tests { type_option.format = format; match format { NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); - assert_equal(&type_option, "0.2", "0.2", &field_type, &field_rev); + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); + assert_number(&type_option, "0.2", "0.2", &field_type, &field_rev); } NumberFormat::USD => { - assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_rev); - assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_rev); - assert_equal(&type_option, "", "", &field_type, &field_rev); - assert_equal(&type_option, "abc", "", &field_type, &field_rev); + assert_number(&type_option, "$18,44", "$1,844", &field_type, &field_rev); + assert_number(&type_option, "$0.2", "$0.2", &field_type, &field_rev); + assert_number(&type_option, "", "", &field_type, &field_rev); + assert_number(&type_option, "abc", "", &field_type, &field_rev); } NumberFormat::Yen => { - assert_equal(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev); - assert_equal(&type_option, "¥1844", "¥1,844", &field_type, &field_rev); + assert_number(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev); + assert_number(&type_option, "¥1844", "¥1,844", &field_type, &field_rev); } NumberFormat::Yuan => { - assert_equal(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_rev); - assert_equal(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev); + assert_number(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_rev); + assert_number(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev); } NumberFormat::EUR => { - assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_rev); - assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_rev); - assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_rev); + assert_number(&type_option, "€18.44", "€18,44", &field_type, &field_rev); + assert_number(&type_option, "€0.5", "€0,5", &field_type, &field_rev); + assert_number(&type_option, "€1844", "€1.844", &field_type, &field_rev); } _ => {} } } } + /// Carry out the sign positive to input number #[test] fn number_description_sign_test() { let mut type_option = NumberTypeOption { @@ -105,32 +112,32 @@ mod tests { type_option.format = format; match format { NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); } NumberFormat::USD => { - assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_rev); + assert_number(&type_option, "18443", "-$18,443", &field_type, &field_rev); } NumberFormat::Yen => { - assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_rev); + assert_number(&type_option, "18443", "-¥18,443", &field_type, &field_rev); } NumberFormat::EUR => { - assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_rev); + assert_number(&type_option, "18443", "-€18.443", &field_type, &field_rev); } _ => {} } } } - fn assert_equal( + fn assert_number( type_option: &NumberTypeOption, - cell_data: &str, + input_str: &str, expected_str: &str, field_type: &FieldType, field_rev: &FieldRevision, ) { assert_eq!( type_option - .decode_cell_data(cell_data.to_owned().into(), field_type, field_rev) + .decode_cell_data(input_str.to_owned().into(), field_type, field_rev) .unwrap() .to_string(), expected_str.to_owned() diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index dbffe79f56..d426844427 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -1,4 +1,4 @@ -use crate::entities::{CellChangesetPB, CellIdentifierParams, FieldType, GridCellIdentifierPayloadPB}; +use crate::entities::{CellChangesetPB, FieldType, GridCellIdPB, GridCellIdParams}; use crate::services::cell::{CellBytes, CellBytesParser, CellData, CellDisplayable, FromCellChangeset, FromCellString}; use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOptionPB}; use bytes::Bytes; @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; pub const SELECTION_IDS_SEPARATOR: &str = ","; +/// [SelectOptionPB] represents an option for a single select, and multiple select. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ProtoBuf)] pub struct SelectOptionPB { #[pb(index = 1)] @@ -225,7 +226,7 @@ impl CellBytesParser for SelectOptionCellDataParser { #[derive(Clone, Debug, Default, ProtoBuf)] pub struct SelectOptionCellChangesetPayloadPB { #[pb(index = 1)] - pub cell_identifier: GridCellIdentifierPayloadPB, + pub cell_identifier: GridCellIdPB, #[pb(index = 2, one_of)] pub insert_option_id: Option, @@ -235,7 +236,7 @@ pub struct SelectOptionCellChangesetPayloadPB { } pub struct SelectOptionCellChangesetParams { - pub cell_identifier: CellIdentifierParams, + pub cell_identifier: GridCellIdParams, pub insert_option_id: Option, pub delete_option_id: Option, } @@ -260,7 +261,7 @@ impl TryInto for SelectOptionCellChangesetPaylo type Error = ErrorCode; fn try_into(self) -> Result { - let cell_identifier: CellIdentifierParams = self.cell_identifier.try_into()?; + let cell_identifier: GridCellIdParams = self.cell_identifier.try_into()?; let insert_option_id = match self.insert_option_id { None => None, Some(insert_option_id) => Some( @@ -322,19 +323,25 @@ impl SelectOptionCellChangeset { } } +/// [SelectOptionCellDataPB] contains a list of user's selected options and a list of all the options +/// that the cell can use. #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] pub struct SelectOptionCellDataPB { + /// The available options that the cell can use. #[pb(index = 1)] pub options: Vec, + /// The selected options for the cell. #[pb(index = 2)] pub select_options: Vec, } +/// [SelectOptionChangesetPayloadPB] describes the changes of a FieldTypeOptionData. For the moment, +/// it is used by [MultiSelectTypeOptionPB] and [SingleSelectTypeOptionPB]. #[derive(Clone, Debug, Default, ProtoBuf)] pub struct SelectOptionChangesetPayloadPB { #[pb(index = 1)] - pub cell_identifier: GridCellIdentifierPayloadPB, + pub cell_identifier: GridCellIdPB, #[pb(index = 2, one_of)] pub insert_option: Option, @@ -347,7 +354,7 @@ pub struct SelectOptionChangesetPayloadPB { } pub struct SelectOptionChangeset { - pub cell_identifier: CellIdentifierParams, + pub cell_identifier: GridCellIdParams, pub insert_option: Option, pub update_option: Option, pub delete_option: Option, diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs index 77f4ea767e..3cf5d6f99b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs @@ -6,49 +6,172 @@ mod tests { use crate::services::field::{URLCellDataPB, URLTypeOption}; use flowy_grid_data_model::revision::FieldRevision; + /// The expected_str will equal to the input string, but the expected_url will be empty if there's no + /// http url in the input string. #[test] - fn url_type_option_test_no_url() { + fn url_type_option_does_not_contain_url_test() { let type_option = URLTypeOption::default(); let field_type = FieldType::URL; let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset(&type_option, "123", &field_type, &field_rev, "123", ""); + assert_url(&type_option, "123", "123", "", &field_type, &field_rev); + assert_url(&type_option, "", "", "", &field_type, &field_rev); } + /// The expected_str will equal to the input string, but the expected_url will not be empty + /// if there's a http url in the input string. #[test] - fn url_type_option_test_contains_url() { + fn url_type_option_contains_url_test() { let type_option = URLTypeOption::default(); let field_type = FieldType::URL; let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset( + assert_url( &type_option, "AppFlowy website - https://www.appflowy.io", - &field_type, - &field_rev, "AppFlowy website - https://www.appflowy.io", "https://www.appflowy.io/", - ); - - assert_changeset( - &type_option, - "AppFlowy website appflowy.io", &field_type, &field_rev, + ); + + assert_url( + &type_option, + "AppFlowy website appflowy.io", "AppFlowy website appflowy.io", "https://appflowy.io", + &field_type, + &field_rev, ); } - fn assert_changeset( + /// if there's a http url and some words following it in the input string. + #[test] + fn url_type_option_contains_url_with_string_after_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_url( + &type_option, + "AppFlowy website - https://www.appflowy.io welcome!", + "AppFlowy website - https://www.appflowy.io welcome!", + "https://www.appflowy.io/", + &field_type, + &field_rev, + ); + + assert_url( + &type_option, + "AppFlowy website appflowy.io welcome!", + "AppFlowy website appflowy.io welcome!", + "https://appflowy.io", + &field_type, + &field_rev, + ); + } + + /// if there's a http url and special words following it in the input string. + #[test] + fn url_type_option_contains_url_with_special_string_after_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_url( + &type_option, + "AppFlowy website - https://www.appflowy.io!", + "AppFlowy website - https://www.appflowy.io!", + "https://www.appflowy.io/", + &field_type, + &field_rev, + ); + + assert_url( + &type_option, + "AppFlowy website appflowy.io!", + "AppFlowy website appflowy.io!", + "https://appflowy.io", + &field_type, + &field_rev, + ); + } + + /// if there's a level4 url in the input string. + #[test] + fn level4_url_type_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_url( + &type_option, + "test - https://tester.testgroup.appflowy.io", + "test - https://tester.testgroup.appflowy.io", + "https://tester.testgroup.appflowy.io/", + &field_type, + &field_rev, + ); + + assert_url( + &type_option, + "test tester.testgroup.appflowy.io", + "test tester.testgroup.appflowy.io", + "https://tester.testgroup.appflowy.io", + &field_type, + &field_rev, + ); + } + + /// urls with different top level domains. + #[test] + fn different_top_level_domains_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_url( + &type_option, + "appflowy - https://appflowy.com", + "appflowy - https://appflowy.com", + "https://appflowy.com/", + &field_type, + &field_rev, + ); + + assert_url( + &type_option, + "appflowy - https://appflowy.top", + "appflowy - https://appflowy.top", + "https://appflowy.top/", + &field_type, + &field_rev, + ); + + assert_url( + &type_option, + "appflowy - https://appflowy.net", + "appflowy - https://appflowy.net", + "https://appflowy.net/", + &field_type, + &field_rev, + ); + + assert_url( + &type_option, + "appflowy - https://appflowy.edu", + "appflowy - https://appflowy.edu", + "https://appflowy.edu/", + &field_type, + &field_rev, + ); + } + + fn assert_url( type_option: &URLTypeOption, - cell_data: &str, + input_str: &str, + expected_str: &str, + expected_url: &str, field_type: &FieldType, field_rev: &FieldRevision, - expected: &str, - expected_url: &str, ) { - let encoded_data = type_option.apply_changeset(cell_data.to_owned().into(), None).unwrap(); + let encoded_data = type_option.apply_changeset(input_str.to_owned().into(), None).unwrap(); let decode_cell_data = decode_cell_data(encoded_data, type_option, field_rev, field_type); - assert_eq!(expected.to_owned(), decode_cell_data.content); + assert_eq!(expected_str.to_owned(), decode_cell_data.content); assert_eq!(expected_url.to_owned(), decode_cell_data.url); } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 572665bd0c..44626b96fd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -1,5 +1,5 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; -use crate::entities::CellIdentifierParams; +use crate::entities::GridCellIdParams; use crate::entities::*; use crate::manager::{GridTaskSchedulerRwLock, GridUser}; use crate::services::block_manager::GridBlockManager; @@ -21,7 +21,7 @@ use flowy_sync::entities::revision::Revision; use flowy_sync::errors::CollaborateResult; use flowy_sync::util::make_delta_from_revisions; use lib_infra::future::FutureResult; -use lib_ot::core::PlainTextAttributes; +use lib_ot::core::PhantomAttributes; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; @@ -103,7 +103,6 @@ impl GridRevisionEditor { .modify(|grid| { let builder = type_option_builder_from_bytes(type_option_data, &field.field_type); let field_rev = FieldBuilder::from_field(field, builder).build(); - Ok(grid.create_field_rev(field_rev, start_field_id)?) }) .await?; @@ -339,12 +338,12 @@ impl GridRevisionEditor { Ok(()) } - pub async fn get_cell(&self, params: &CellIdentifierParams) -> Option { + pub async fn get_cell(&self, params: &GridCellIdParams) -> Option { let cell_bytes = self.get_cell_bytes(params).await?; Some(GridCellPB::new(¶ms.field_id, cell_bytes.to_vec())) } - pub async fn get_cell_bytes(&self, params: &CellIdentifierParams) -> Option { + pub async fn get_cell_bytes(&self, params: &GridCellIdParams) -> Option { let field_rev = self.get_field_rev(¶ms.field_id).await?; let row_rev = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??; @@ -573,7 +572,7 @@ impl GridRevisionEditor { let GridChangeset { delta, md5 } = change; let user_id = self.user.user_id()?; let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); - let delta_data = delta.to_delta_bytes(); + let delta_data = delta.json_bytes(); let revision = Revision::new( &self.rev_manager.object_id, base_rev_id, @@ -664,8 +663,8 @@ impl RevisionCloudService for GridRevisionCloudService { pub struct GridRevisionCompactor(); impl RevisionCompactor for GridRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; - Ok(delta.to_delta_bytes()) + let delta = make_delta_from_revisions::(revisions)?; + Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs b/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs index 026ba3bc20..bf3aa7adfb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs @@ -9,7 +9,7 @@ use flowy_revision::{mk_grid_block_revision_disk_cache, RevisionLoader, Revision use flowy_sync::client_grid::{make_grid_rev_json_str, GridRevisionPad}; use flowy_sync::entities::revision::Revision; -use lib_ot::core::PlainTextDeltaBuilder; +use lib_ot::core::TextDeltaBuilder; use serde::{Deserialize, Serialize}; use std::str::FromStr; use std::sync::Arc; @@ -48,7 +48,7 @@ impl GridMigration { let pool = self.database.db_pool()?; let grid_rev_pad = self.get_grid_revision_pad(grid_id).await?; let json = grid_rev_pad.json_str()?; - let delta_data = PlainTextDeltaBuilder::new().insert(&json).build().to_delta_bytes(); + let delta_data = TextDeltaBuilder::new().insert(&json).build().json_bytes(); let revision = Revision::initial_revision(&user_id, grid_id, delta_data); let record = RevisionRecord::new(revision); // diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs index 7b64a44b48..85bfed576d 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs @@ -2,7 +2,7 @@ use crate::grid::block_test::script::RowScript::*; use crate::grid::block_test::script::{CreateRowScriptBuilder, GridRowTest}; use crate::grid::grid_editor::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, TWITTER}; use flowy_grid::entities::FieldType; -use flowy_grid::services::field::{NO, SELECTION_IDS_SEPARATOR}; +use flowy_grid::services::field::{SELECTION_IDS_SEPARATOR, UNCHECK}; use flowy_grid_data_model::revision::RowMetaChangeset; #[tokio::test] @@ -72,7 +72,7 @@ async fn grid_row_add_cells_test() { builder.insert(FieldType::RichText, "hello world", "hello world"); builder.insert(FieldType::DateTime, "1647251762", "2022/03/14"); builder.insert(FieldType::Number, "18,443", "$18,443.00"); - builder.insert(FieldType::Checkbox, "false", NO); + builder.insert(FieldType::Checkbox, "false", UNCHECK); builder.insert(FieldType::URL, "https://appflowy.io", "https://appflowy.io"); builder.insert_single_select_cell(|mut options| options.remove(0), COMPLETED); builder.insert_multi_select_cell( diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs index 33548f97c5..14671c2eb2 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs @@ -2,7 +2,7 @@ use crate::grid::block_test::script::RowScript::{AssertCell, CreateRow}; use crate::grid::block_test::util::GridRowTestBuilder; use crate::grid::grid_editor::GridEditorTest; -use flowy_grid::entities::{CellIdentifierParams, FieldType, GridRowPB}; +use flowy_grid::entities::{FieldType, GridCellIdParams, GridRowPB}; use flowy_grid::services::field::*; use flowy_grid_data_model::revision::{ GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision, @@ -109,7 +109,7 @@ impl GridRowTest { field_type, expected, } => { - let id = CellIdentifierParams { + let id = GridCellIdParams { grid_id: self.grid_id.clone(), field_id, row_id, @@ -154,7 +154,7 @@ impl GridRowTest { } } - async fn compare_cell_content(&self, cell_id: CellIdentifierParams, field_type: FieldType, expected: String) { + async fn compare_cell_content(&self, cell_id: GridCellIdParams, field_type: FieldType, expected: String) { match field_type { FieldType::RichText => { let cell_data = self diff --git a/frontend/rust-lib/flowy-revision/src/conflict_resolve.rs b/frontend/rust-lib/flowy-revision/src/conflict_resolve.rs index 0a63f37f3e..8b36d554cd 100644 --- a/frontend/rust-lib/flowy-revision/src/conflict_resolve.rs +++ b/frontend/rust-lib/flowy-revision/src/conflict_resolve.rs @@ -9,7 +9,7 @@ use flowy_sync::{ util::make_delta_from_revisions, }; use lib_infra::future::BoxResultFuture; -use lib_ot::core::{Attributes, Delta, PlainTextAttributes}; +use lib_ot::core::{Attributes, Delta, PhantomAttributes}; use lib_ot::rich_text::RichTextAttributes; use serde::de::DeserializeOwned; use std::{convert::TryFrom, sync::Arc}; @@ -31,7 +31,7 @@ pub trait ConflictRevisionSink: Send + Sync + 'static { } pub type RichTextConflictController = ConflictController; -pub type PlainTextConflictController = ConflictController; +pub type PlainTextConflictController = ConflictController; pub struct ConflictController where @@ -154,7 +154,7 @@ where &rev_manager.object_id, base_rev_id, rev_id, - client_delta.to_delta_bytes(), + client_delta.json_bytes(), user_id, md5.clone(), ); @@ -166,7 +166,7 @@ where &rev_manager.object_id, base_rev_id, rev_id, - server_delta.to_delta_bytes(), + server_delta.json_bytes(), user_id, md5, ); diff --git a/frontend/rust-lib/flowy-text-block/src/editor.rs b/frontend/rust-lib/flowy-text-block/src/editor.rs index 6fed85c913..2649b2bae1 100644 --- a/frontend/rust-lib/flowy-text-block/src/editor.rs +++ b/frontend/rust-lib/flowy-text-block/src/editor.rs @@ -238,7 +238,7 @@ impl RevisionObjectBuilder for TextBlockInfoBuilder { Result::::Ok(DocumentPB { block_id: object_id.to_owned(), - text: delta.to_delta_str(), + text: delta.json_str(), rev_id, base_rev_id, }) diff --git a/frontend/rust-lib/flowy-text-block/src/queue.rs b/frontend/rust-lib/flowy-text-block/src/queue.rs index 343ad11e7b..e48c39fb97 100644 --- a/frontend/rust-lib/flowy-text-block/src/queue.rs +++ b/frontend/rust-lib/flowy-text-block/src/queue.rs @@ -12,7 +12,7 @@ use flowy_sync::{ }; use futures::stream::StreamExt; use lib_ot::{ - core::{Interval, OperationTransformable}, + core::{Interval, OperationTransform}, rich_text::{RichTextAttribute, RichTextAttributes, RichTextDelta}, }; use std::sync::Arc; @@ -175,7 +175,7 @@ impl EditBlockQueue { } async fn save_local_delta(&self, delta: RichTextDelta, md5: String) -> Result { - let delta_data = delta.to_delta_bytes(); + let delta_data = delta.json_bytes(); let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); let user_id = self.user.user_id()?; let revision = Revision::new( @@ -195,7 +195,7 @@ pub(crate) struct TextBlockRevisionCompactor(); impl RevisionCompactor for TextBlockRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { let delta = make_delta_from_revisions::(revisions)?; - Ok(delta.to_delta_bytes()) + Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-text-block/tests/document/script.rs b/frontend/rust-lib/flowy-text-block/tests/document/script.rs index 1722724c3f..76c34a78e0 100644 --- a/frontend/rust-lib/flowy-text-block/tests/document/script.rs +++ b/frontend/rust-lib/flowy-text-block/tests/document/script.rs @@ -75,7 +75,7 @@ impl TextBlockEditorTest { let delta = self.editor.text_block_delta().await.unwrap(); if expected_delta != delta { eprintln!("✅ expect: {}", expected,); - eprintln!("❌ receive: {}", delta.to_delta_str()); + eprintln!("❌ receive: {}", delta.json_str()); } assert_eq!(expected_delta, delta); } diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/attribute_test.rs b/frontend/rust-lib/flowy-text-block/tests/editor/attribute_test.rs index 037b36970b..0653e88567 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/attribute_test.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/attribute_test.rs @@ -1,7 +1,7 @@ #![cfg_attr(rustfmt, rustfmt::skip)] use crate::editor::{TestBuilder, TestOp::*}; use flowy_sync::client_document::{NewlineDoc, PlainDoc}; -use lib_ot::core::{Interval, OperationTransformable, NEW_LINE, WHITESPACE, FlowyStr}; +use lib_ot::core::{Interval, OperationTransform, NEW_LINE, WHITESPACE, OTString}; use unicode_segmentation::UnicodeSegmentation; use lib_ot::rich_text::RichTextDelta; @@ -723,8 +723,8 @@ fn attributes_preserve_header_format_on_merge() { #[test] fn attributes_format_emoji() { let emoji_s = "👋 "; - let s: FlowyStr = emoji_s.into(); - let len = s.utf16_size(); + let s: OTString = emoji_s.into(); + let len = s.utf16_len(); assert_eq!(3, len); assert_eq!(2, s.graphemes(true).count()); @@ -762,19 +762,19 @@ fn attributes_preserve_list_format_on_merge() { #[test] fn delta_compose() { - let mut delta = RichTextDelta::from_delta_str(r#"[{"insert":"\n"}]"#).unwrap(); + let mut delta = RichTextDelta::from_json(r#"[{"insert":"\n"}]"#).unwrap(); let deltas = vec![ - RichTextDelta::from_delta_str(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(), - RichTextDelta::from_delta_str(r#"[{"insert":"a"}]"#).unwrap(), - RichTextDelta::from_delta_str(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(), - RichTextDelta::from_delta_str(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(), + RichTextDelta::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(), + RichTextDelta::from_json(r#"[{"insert":"a"}]"#).unwrap(), + RichTextDelta::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(), + RichTextDelta::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(), ]; for d in deltas { delta = delta.compose(&d).unwrap(); } assert_eq!( - delta.to_delta_str(), + delta.json_str(), r#"[{"insert":"a"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"\n"}]"# ); diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/mod.rs b/frontend/rust-lib/flowy-text-block/tests/editor/mod.rs index 676b9a1cb0..bbfd1da6f8 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/mod.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/mod.rs @@ -108,20 +108,20 @@ impl TestBuilder { TestOp::Insert(delta_i, s, index) => { let document = &mut self.documents[*delta_i]; let delta = document.insert(*index, s).unwrap(); - tracing::debug!("Insert delta: {}", delta.to_delta_str()); + tracing::debug!("Insert delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Delete(delta_i, iv) => { let document = &mut self.documents[*delta_i]; let delta = document.replace(*iv, "").unwrap(); - tracing::trace!("Delete delta: {}", delta.to_delta_str()); + tracing::trace!("Delete delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Replace(delta_i, iv, s) => { let document = &mut self.documents[*delta_i]; let delta = document.replace(*iv, s).unwrap(); - tracing::trace!("Replace delta: {}", delta.to_delta_str()); + tracing::trace!("Replace delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::InsertBold(delta_i, s, iv) => { @@ -133,7 +133,7 @@ impl TestBuilder { let document = &mut self.documents[*delta_i]; let attribute = RichTextAttribute::Bold(*enable); let delta = document.format(*iv, attribute).unwrap(); - tracing::trace!("Bold delta: {}", delta.to_delta_str()); + tracing::trace!("Bold delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Italic(delta_i, iv, enable) => { @@ -143,28 +143,28 @@ impl TestBuilder { false => RichTextAttribute::Italic(false), }; let delta = document.format(*iv, attribute).unwrap(); - tracing::trace!("Italic delta: {}", delta.to_delta_str()); + tracing::trace!("Italic delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Header(delta_i, iv, level) => { let document = &mut self.documents[*delta_i]; let attribute = RichTextAttribute::Header(*level); let delta = document.format(*iv, attribute).unwrap(); - tracing::trace!("Header delta: {}", delta.to_delta_str()); + tracing::trace!("Header delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Link(delta_i, iv, link) => { let document = &mut self.documents[*delta_i]; let attribute = RichTextAttribute::Link(link.to_owned()); let delta = document.format(*iv, attribute).unwrap(); - tracing::trace!("Link delta: {}", delta.to_delta_str()); + tracing::trace!("Link delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Bullet(delta_i, iv, enable) => { let document = &mut self.documents[*delta_i]; let attribute = RichTextAttribute::Bullet(*enable); let delta = document.format(*iv, attribute).unwrap(); - tracing::debug!("Bullet delta: {}", delta.to_delta_str()); + tracing::debug!("Bullet delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } @@ -194,15 +194,15 @@ impl TestBuilder { let delta_a = &self.documents[*delta_a_i].delta(); let delta_b = &self.documents[*delta_b_i].delta(); tracing::debug!("Invert: "); - tracing::debug!("a: {}", delta_a.to_delta_str()); - tracing::debug!("b: {}", delta_b.to_delta_str()); + tracing::debug!("a: {}", delta_a.json_str()); + tracing::debug!("b: {}", delta_b.json_str()); let (_, b_prime) = delta_a.transform(delta_b).unwrap(); let undo = b_prime.invert(delta_a); let new_delta = delta_a.compose(&b_prime).unwrap(); - tracing::debug!("new delta: {}", new_delta.to_delta_str()); - tracing::debug!("undo delta: {}", undo.to_delta_str()); + tracing::debug!("new delta: {}", new_delta.json_str()); + tracing::debug!("undo delta: {}", undo.json_str()); let new_delta_after_undo = new_delta.compose(&undo).unwrap(); @@ -238,7 +238,7 @@ impl TestBuilder { } TestOp::AssertPrimeJson(doc_i, expected) => { - let prime_json = self.primes[*doc_i].as_ref().unwrap().to_delta_str(); + let prime_json = self.primes[*doc_i].as_ref().unwrap().json_str(); let expected_prime: RichTextDelta = serde_json::from_str(expected).unwrap(); let target_prime: RichTextDelta = serde_json::from_str(&prime_json).unwrap(); @@ -300,9 +300,9 @@ impl Rng { pub fn gen_delta(&mut self, s: &str) -> RichTextDelta { let mut delta = RichTextDelta::default(); - let s = FlowyStr::from(s); + let s = OTString::from(s); loop { - let left = s.utf16_size() - delta.utf16_base_len; + let left = s.utf16_len() - delta.utf16_base_len; if left == 0 { break; } diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/op_test.rs b/frontend/rust-lib/flowy-text-block/tests/editor/op_test.rs index 3f174fa3bc..99d3136e55 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/op_test.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/op_test.rs @@ -1,7 +1,9 @@ #![allow(clippy::all)] use crate::editor::{Rng, TestBuilder, TestOp::*}; use flowy_sync::client_document::{NewlineDoc, PlainDoc}; +use lib_ot::rich_text::RichTextDeltaBuilder; use lib_ot::{ + core::Interval, core::*, rich_text::{AttributeBuilder, RichTextAttribute, RichTextAttributes, RichTextDelta}, }; @@ -38,24 +40,20 @@ fn attributes_insert_text_at_middle() { #[test] fn delta_get_ops_in_interval_1() { - let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("123").build(); - let insert_b = OpBuilder::insert("4").build(); + let operations = OperationsBuilder::new().insert("123").insert("4").build(); + let delta = RichTextDeltaBuilder::from_operations(operations); - delta.add(insert_a.clone()); - delta.add(insert_b.clone()); - - let mut iterator = DeltaIter::from_interval(&delta, Interval::new(0, 4)); + let mut iterator = DeltaIterator::from_interval(&delta, Interval::new(0, 4)); assert_eq!(iterator.ops(), delta.ops); } #[test] fn delta_get_ops_in_interval_2() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("123").build(); - let insert_b = OpBuilder::insert("4").build(); - let insert_c = OpBuilder::insert("5").build(); - let retain_a = OpBuilder::retain(3).build(); + let insert_a = Operation::insert("123"); + let insert_b = Operation::insert("4"); + let insert_c = Operation::insert("5"); + let retain_a = Operation::retain(3); delta.add(insert_a.clone()); delta.add(retain_a.clone()); @@ -63,32 +61,32 @@ fn delta_get_ops_in_interval_2() { delta.add(insert_c.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 2)).ops(), - vec![OpBuilder::insert("12").build()] + DeltaIterator::from_interval(&delta, Interval::new(0, 2)).ops(), + vec![Operation::insert("12")] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(1, 3)).ops(), - vec![OpBuilder::insert("23").build()] + DeltaIterator::from_interval(&delta, Interval::new(1, 3)).ops(), + vec![Operation::insert("23")] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 3)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(0, 3)).ops(), vec![insert_a.clone()] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 4)).ops(), - vec![insert_a.clone(), OpBuilder::retain(1).build()] + DeltaIterator::from_interval(&delta, Interval::new(0, 4)).ops(), + vec![insert_a.clone(), Operation::retain(1)] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 6)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(0, 6)).ops(), vec![insert_a.clone(), retain_a.clone()] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 7)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(0, 7)).ops(), vec![insert_a.clone(), retain_a.clone(), insert_b.clone()] ); } @@ -96,113 +94,113 @@ fn delta_get_ops_in_interval_2() { #[test] fn delta_get_ops_in_interval_3() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("123456").build(); + let insert_a = Operation::insert("123456"); delta.add(insert_a.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(3, 5)).ops(), - vec![OpBuilder::insert("45").build()] + DeltaIterator::from_interval(&delta, Interval::new(3, 5)).ops(), + vec![Operation::insert("45")] ); } #[test] fn delta_get_ops_in_interval_4() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("12").build(); - let insert_b = OpBuilder::insert("34").build(); - let insert_c = OpBuilder::insert("56").build(); + let insert_a = Operation::insert("12"); + let insert_b = Operation::insert("34"); + let insert_c = Operation::insert("56"); delta.ops.push(insert_a.clone()); delta.ops.push(insert_b.clone()); delta.ops.push(insert_c.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 2)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(0, 2)).ops(), vec![insert_a] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(2, 4)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(2, 4)).ops(), vec![insert_b] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(4, 6)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(4, 6)).ops(), vec![insert_c] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(2, 5)).ops(), - vec![OpBuilder::insert("34").build(), OpBuilder::insert("5").build()] + DeltaIterator::from_interval(&delta, Interval::new(2, 5)).ops(), + vec![Operation::insert("34"), Operation::insert("5")] ); } #[test] fn delta_get_ops_in_interval_5() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("123456").build(); - let insert_b = OpBuilder::insert("789").build(); + let insert_a = Operation::insert("123456"); + let insert_b = Operation::insert("789"); delta.ops.push(insert_a.clone()); delta.ops.push(insert_b.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(4, 8)).ops(), - vec![OpBuilder::insert("56").build(), OpBuilder::insert("78").build()] + DeltaIterator::from_interval(&delta, Interval::new(4, 8)).ops(), + vec![Operation::insert("56"), Operation::insert("78")] ); // assert_eq!( // DeltaIter::from_interval(&delta, Interval::new(8, 9)).ops(), - // vec![Builder::insert("9").build()] + // vec![Builder::insert("9")] // ); } #[test] fn delta_get_ops_in_interval_6() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("12345678").build(); + let insert_a = Operation::insert("12345678"); delta.add(insert_a.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(4, 6)).ops(), - vec![OpBuilder::insert("56").build()] + DeltaIterator::from_interval(&delta, Interval::new(4, 6)).ops(), + vec![Operation::insert("56")] ); } #[test] fn delta_get_ops_in_interval_7() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("12345").build(); - let retain_a = OpBuilder::retain(3).build(); + let insert_a = Operation::insert("12345"); + let retain_a = Operation::retain(3); delta.add(insert_a.clone()); delta.add(retain_a.clone()); - let mut iter_1 = DeltaIter::from_offset(&delta, 2); - assert_eq!(iter_1.next_op().unwrap(), OpBuilder::insert("345").build()); - assert_eq!(iter_1.next_op().unwrap(), OpBuilder::retain(3).build()); + let mut iter_1 = DeltaIterator::from_offset(&delta, 2); + assert_eq!(iter_1.next_op().unwrap(), Operation::insert("345")); + assert_eq!(iter_1.next_op().unwrap(), Operation::retain(3)); - let mut iter_2 = DeltaIter::new(&delta); - assert_eq!(iter_2.next_op_with_len(2).unwrap(), OpBuilder::insert("12").build()); - assert_eq!(iter_2.next_op().unwrap(), OpBuilder::insert("345").build()); + let mut iter_2 = DeltaIterator::new(&delta); + assert_eq!(iter_2.next_op_with_len(2).unwrap(), Operation::insert("12")); + assert_eq!(iter_2.next_op().unwrap(), Operation::insert("345")); - assert_eq!(iter_2.next_op().unwrap(), OpBuilder::retain(3).build()); + assert_eq!(iter_2.next_op().unwrap(), Operation::retain(3)); } #[test] fn delta_op_seek() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("12345").build(); - let retain_a = OpBuilder::retain(3).build(); + let insert_a = Operation::insert("12345"); + let retain_a = Operation::retain(3); delta.add(insert_a.clone()); delta.add(retain_a.clone()); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); iter.seek::(1); - assert_eq!(iter.next_op().unwrap(), OpBuilder::retain(3).build()); + assert_eq!(iter.next_op().unwrap(), retain_a); } #[test] fn delta_utf16_code_unit_seek() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); + delta.add(Operation::insert("12345")); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); iter.seek::(3); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("45").build()); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("45")); } #[test] @@ -213,77 +211,77 @@ fn delta_utf16_code_unit_seek_with_attributes() { .add_attr(RichTextAttribute::Italic(true)) .build(); - delta.add(OpBuilder::insert("1234").attributes(attributes.clone()).build()); - delta.add(OpBuilder::insert("\n").build()); + delta.add(Operation::insert_with_attributes("1234", attributes.clone())); + delta.add(Operation::insert("\n")); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); iter.seek::(0); assert_eq!( iter.next_op_with_len(4).unwrap(), - OpBuilder::insert("1234").attributes(attributes).build(), + Operation::insert_with_attributes("1234", attributes), ); } #[test] fn delta_next_op_len() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - let mut iter = DeltaIter::new(&delta); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("12").build()); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("34").build()); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("5").build()); + delta.add(Operation::insert("12345")); + let mut iter = DeltaIterator::new(&delta); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("12")); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("34")); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("5")); assert_eq!(iter.next_op_with_len(1), None); } #[test] fn delta_next_op_len_with_chinese() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("你好").build()); + delta.add(Operation::insert("你好")); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 2); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("你好").build()); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("你好")); } #[test] fn delta_next_op_len_with_english() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("ab").build()); - let mut iter = DeltaIter::new(&delta); + delta.add(Operation::insert("ab")); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 2); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("ab").build()); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("ab")); } #[test] fn delta_next_op_len_after_seek() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - let mut iter = DeltaIter::new(&delta); + delta.add(Operation::insert("12345")); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 5); iter.seek::(3); assert_eq!(iter.next_op_len().unwrap(), 2); - assert_eq!(iter.next_op_with_len(1).unwrap(), OpBuilder::insert("4").build()); + assert_eq!(iter.next_op_with_len(1).unwrap(), Operation::insert("4")); assert_eq!(iter.next_op_len().unwrap(), 1); - assert_eq!(iter.next_op().unwrap(), OpBuilder::insert("5").build()); + assert_eq!(iter.next_op().unwrap(), Operation::insert("5")); } #[test] fn delta_next_op_len_none() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - let mut iter = DeltaIter::new(&delta); + delta.add(Operation::insert("12345")); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 5); - assert_eq!(iter.next_op_with_len(5).unwrap(), OpBuilder::insert("12345").build()); + assert_eq!(iter.next_op_with_len(5).unwrap(), Operation::insert("12345")); assert_eq!(iter.next_op_len(), None); } #[test] fn delta_next_op_with_len_zero() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - let mut iter = DeltaIter::new(&delta); + delta.add(Operation::insert("12345")); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_with_len(0), None,); assert_eq!(iter.next_op_len().unwrap(), 5); } @@ -291,14 +289,14 @@ fn delta_next_op_with_len_zero() { #[test] fn delta_next_op_with_len_cross_op_return_last() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - delta.add(OpBuilder::retain(1).build()); - delta.add(OpBuilder::insert("678").build()); + delta.add(Operation::insert("12345")); + delta.add(Operation::retain(1)); + delta.add(Operation::insert("678")); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); iter.seek::(4); assert_eq!(iter.next_op_len().unwrap(), 1); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::retain(1).build()); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::retain(1)); } #[test] @@ -335,25 +333,21 @@ fn sequence() { fn apply_1000() { for _ in 0..1 { let mut rng = Rng::default(); - let s: FlowyStr = rng.gen_string(50).into(); + let s: OTString = rng.gen_string(50).into(); let delta = rng.gen_delta(&s); - assert_eq!(s.utf16_size(), delta.utf16_base_len); + assert_eq!(s.utf16_len(), delta.utf16_base_len); } } #[test] -fn apply() { - let s = "hello world,".to_owned(); - let mut delta_a = RichTextDelta::default(); - delta_a.insert(&s, RichTextAttributes::default()); +fn apply_test() { + let s = "hello"; + let delta_a = TextDeltaBuilder::new().insert(s).build(); + let delta_b = TextDeltaBuilder::new().retain(s.len()).insert(", AppFlowy").build(); - let mut delta_b = RichTextDelta::default(); - delta_b.retain(s.len(), RichTextAttributes::default()); - delta_b.insert("appflowy", RichTextAttributes::default()); - - let after_a = delta_a.apply("").unwrap(); + let after_a = delta_a.content().unwrap(); let after_b = delta_b.apply(&after_a).unwrap(); - assert_eq!("hello world,appflowy", &after_b); + assert_eq!("hello, AppFlowy", &after_b); } #[test] @@ -384,6 +378,17 @@ fn invert() { } } +#[test] +fn invert_test() { + let s = "hello world"; + let delta = TextDeltaBuilder::new().insert(s).build(); + let invert_delta = delta.invert_str(""); + assert_eq!(delta.utf16_base_len, invert_delta.utf16_target_len); + assert_eq!(delta.utf16_target_len, invert_delta.utf16_base_len); + + assert_eq!(invert_delta.apply(s).unwrap(), "") +} + #[test] fn empty_ops() { let mut delta = RichTextDelta::default(); @@ -415,23 +420,24 @@ fn ops_merging() { assert_eq!(delta.ops.len(), 0); delta.retain(2, RichTextAttributes::default()); assert_eq!(delta.ops.len(), 1); - assert_eq!(delta.ops.last(), Some(&OpBuilder::retain(2).build())); + assert_eq!(delta.ops.last(), Some(&Operation::retain(2))); delta.retain(3, RichTextAttributes::default()); assert_eq!(delta.ops.len(), 1); - assert_eq!(delta.ops.last(), Some(&OpBuilder::retain(5).build())); + assert_eq!(delta.ops.last(), Some(&Operation::retain(5))); delta.insert("abc", RichTextAttributes::default()); assert_eq!(delta.ops.len(), 2); - assert_eq!(delta.ops.last(), Some(&OpBuilder::insert("abc").build())); + assert_eq!(delta.ops.last(), Some(&Operation::insert("abc"))); delta.insert("xyz", RichTextAttributes::default()); assert_eq!(delta.ops.len(), 2); - assert_eq!(delta.ops.last(), Some(&OpBuilder::insert("abcxyz").build())); + assert_eq!(delta.ops.last(), Some(&Operation::insert("abcxyz"))); delta.delete(1); assert_eq!(delta.ops.len(), 3); - assert_eq!(delta.ops.last(), Some(&OpBuilder::delete(1).build())); + assert_eq!(delta.ops.last(), Some(&Operation::delete(1))); delta.delete(1); assert_eq!(delta.ops.len(), 3); - assert_eq!(delta.ops.last(), Some(&OpBuilder::delete(2).build())); + assert_eq!(delta.ops.last(), Some(&Operation::delete(2))); } + #[test] fn is_noop() { let mut delta = RichTextDelta::default(); @@ -449,16 +455,16 @@ fn compose() { let mut rng = Rng::default(); let s = rng.gen_string(20); let a = rng.gen_delta(&s); - let after_a: FlowyStr = a.apply(&s).unwrap().into(); - assert_eq!(a.utf16_target_len, after_a.utf16_size()); + let after_a: OTString = a.apply(&s).unwrap().into(); + assert_eq!(a.utf16_target_len, after_a.utf16_len()); let b = rng.gen_delta(&after_a); - let after_b: FlowyStr = b.apply(&after_a).unwrap().into(); - assert_eq!(b.utf16_target_len, after_b.utf16_size()); + let after_b: OTString = b.apply(&after_a).unwrap().into(); + assert_eq!(b.utf16_target_len, after_b.utf16_len()); let ab = a.compose(&b).unwrap(); assert_eq!(ab.utf16_target_len, b.utf16_target_len); - let after_ab: FlowyStr = ab.apply(&s).unwrap().into(); + let after_ab: OTString = ab.apply(&s).unwrap().into(); assert_eq!(after_b, after_ab); } } @@ -582,11 +588,11 @@ fn transform_two_conflict_non_seq_delta() { #[test] fn delta_invert_no_attribute_delta() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("123").build()); + delta.add(Operation::insert("123")); let mut change = RichTextDelta::default(); - change.add(OpBuilder::retain(3).build()); - change.add(OpBuilder::insert("456").build()); + change.add(Operation::retain(3)); + change.add(Operation::insert("456")); let undo = change.invert(&delta); let new_delta = delta.compose(&change).unwrap(); diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/serde_test.rs b/frontend/rust-lib/flowy-text-block/tests/editor/serde_test.rs index 87d425901b..ac4a77412c 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/serde_test.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/serde_test.rs @@ -11,7 +11,7 @@ fn operation_insert_serialize_test() { .add_attr(RichTextAttribute::Bold(true)) .add_attr(RichTextAttribute::Italic(true)) .build(); - let operation = OpBuilder::insert("123").attributes(attributes).build(); + let operation = Operation::insert_with_attributes("123", attributes); let json = serde_json::to_string(&operation).unwrap(); eprintln!("{}", json); @@ -42,7 +42,7 @@ fn attributes_serialize_test() { .add_attr(RichTextAttribute::Bold(true)) .add_attr(RichTextAttribute::Italic(true)) .build(); - let retain = OpBuilder::insert("123").attributes(attributes).build(); + let retain = Operation::insert_with_attributes("123", attributes); let json = serde_json::to_string(&retain).unwrap(); eprintln!("{}", json); @@ -56,7 +56,7 @@ fn delta_serialize_multi_attribute_test() { .add_attr(RichTextAttribute::Bold(true)) .add_attr(RichTextAttribute::Italic(true)) .build(); - let retain = OpBuilder::insert("123").attributes(attributes).build(); + let retain = Operation::insert_with_attributes("123", attributes); delta.add(retain); delta.add(Operation::Retain(5.into())); @@ -65,7 +65,7 @@ fn delta_serialize_multi_attribute_test() { let json = serde_json::to_string(&delta).unwrap(); eprintln!("{}", json); - let delta_from_json = Delta::from_delta_str(&json).unwrap(); + let delta_from_json = Delta::from_json(&json).unwrap(); assert_eq!(delta_from_json, delta); } @@ -77,7 +77,7 @@ fn delta_deserialize_test() { {"retain":2,"attributes":{"italic":"true","bold":"true"}}, {"retain":2,"attributes":{"italic":true,"bold":true}} ]"#; - let delta = RichTextDelta::from_delta_str(json).unwrap(); + let delta = RichTextDelta::from_json(json).unwrap(); eprintln!("{}", delta); } @@ -86,13 +86,13 @@ fn delta_deserialize_null_test() { let json = r#"[ {"retain":7,"attributes":{"bold":null}} ]"#; - let delta1 = RichTextDelta::from_delta_str(json).unwrap(); + let delta1 = RichTextDelta::from_json(json).unwrap(); let mut attribute = RichTextAttribute::Bold(true); attribute.value = RichTextAttributeValue(None); let delta2 = DeltaBuilder::new().retain_with_attributes(7, attribute.into()).build(); - assert_eq!(delta2.to_delta_str(), r#"[{"retain":7,"attributes":{"bold":""}}]"#); + assert_eq!(delta2.json_str(), r#"[{"retain":7,"attributes":{"bold":""}}]"#); assert_eq!(delta1, delta2); } diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/undo_redo_test.rs b/frontend/rust-lib/flowy-text-block/tests/editor/undo_redo_test.rs index e6ea9200ab..78aa034744 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/undo_redo_test.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/undo_redo_test.rs @@ -108,6 +108,7 @@ fn history_bold_redo_with_lagging() { fn history_delete_undo() { let ops = vec![ Insert(0, "123", 0), + Wait(RECORD_THRESHOLD), AssertDocJson(0, r#"[{"insert":"123"}]"#), Delete(0, Interval::new(0, 3)), AssertDocJson(0, r#"[]"#), diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 54b79173bc..f050087977 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -33,7 +33,7 @@ source $HOME/.cargo/env && \ cargo install --force cargo-make && \ cargo install --force duckscript_cli && \ cargo make flowy_dev && \ -cargo make -p production-linux-x86 appflowy-linux +cargo make -p production-linux-x86_64 appflowy-linux CMD ["/home/makepkg/appflowy/frontend/app_flowy/build/linux/x64/release/bundle/app_flowy"] diff --git a/frontend/scripts/docker-buildfiles/docker-compose.yml b/frontend/scripts/docker-buildfiles/docker-compose.yml index 581a1232e9..ac1cc29339 100644 --- a/frontend/scripts/docker-buildfiles/docker-compose.yml +++ b/frontend/scripts/docker-buildfiles/docker-compose.yml @@ -6,8 +6,11 @@ services: image: appflowy/appflowy:latest stdin_open: true # tty: true + devices: + - "/dev/dri:/dev/dri" # fixes MESA-LOADER error environment: - DISPLAY=${DISPLAY} + - NO_AT_BRIDGE=1 # fixes dbind-WARNING volumes: - $HOME/.Xauthority:/root/.Xauthority:rw - /tmp/.X11-unix:/tmp/.X11-unix diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index a580c22b19..396a66cd63 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -50,6 +50,13 @@ flutter doctor printMessage "Setting up githooks." git config core.hooksPath .githooks +# Install go-gitlint +printMessage "Installing go-gitlint." +GOLINT_FILENAME="go-gitlint_1.1.0_linux_x86_64.tar.gz" +wget https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} +tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint +rm ${GOLINT_FILENAME} + # Change to the frontend directory cd frontend @@ -61,10 +68,6 @@ cargo install --force cargo-make printMessage "Installing duckscript." cargo install --force duckscript_cli -# Install CommitLint -printMessage "Installing CommitLint." -npm install @commitlint/cli @commitlint/config-conventional --save-dev - # Check prerequisites printMessage "Checking prerequisites." cargo make flowy_dev diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index edc0c40b26..f7e9d73909 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -22,12 +22,14 @@ printError() { printMessage "The Rust programming language is required to compile AppFlowy." printMessage "We can install it now if you don't already have it on your system." -read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust +read -p "$(printSuccess "Do you want to install Rust? [Y/N]") " installrust -if [ ${installrust^^} == "Y" ]; then +if [ ${installrust} == "Y" ] || [ ${installrust} == "y" ]; then printMessage "Installing Rust." brew install rustup-init rustup-init -y --default-toolchain=stable + + source "$HOME/.cargo/env" else printMessage "Skipping Rust installation." fi @@ -50,6 +52,13 @@ flutter doctor printMessage "Setting up githooks." git config core.hooksPath .githooks +# Install go-gitlint +printMessage "Installing go-gitlint." +GOLINT_FILENAME="go-gitlint_1.1.0_osx_x86_64.tar.gz" +curl -L https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} --output ${GOLINT_FILENAME} +tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint +rm ${GOLINT_FILENAME} + # Change to the frontend directory cd frontend @@ -61,10 +70,6 @@ cargo install --force cargo-make printMessage "Installing duckscript." cargo install --force duckscript_cli -# Install CommitLint -printMessagae "Installing CommitLint." -npm install @commitlint/cli @commitlint/config-conventional --save-dev - # Check prerequisites printMessage "Checking prerequisites." cargo make flowy_dev diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh new file mode 100644 index 0000000000..bbba4cf2d5 --- /dev/null +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + + +# Note: This script does not install applications which are installed by the package manager. There are too many package managers out there. + +# Install Rust +if ! rustc --version; then + + printMessage "The Rust programming language is required to compile AppFlowy." + printMessage "It has not been detected on your system." + + read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust + + if [ ${installrust^^} == "Y" ]; then + printMessage "Installing Rust." + if ! curl --proto '=https' --tlsv1.2 -sSf https://win.rustup.rs/x86_64 -o rustup-init.exe; then + printError "Failed to download the Rust installer" + exit 1 + fi + start "Rust Installer" rustup-init.exe + read -p "$(printSuccess "Press enter when Rust installation is done") " isDone + rm rustup-init.exe + $USERPROFILE/.cargo/bin/rustup toolchain install stable + $USERPROFILE/.cargo/bin/rustup default stable + else + printMessage "Skipping Rust installation." + fi +else + printSuccess "Rust has been detected on your system, so Rust installation has been skipped" +fi + +# Enable the flutter stable channel +printMessage "Setting up Flutter" +flutter channel stable + +# Add pub cache and cargo to PATH +powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:LOCALAPPDATA + "\Pub\Cache\Bin", [EnvironmentVariableTarget]::User)' +powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:USERPROFILE + "\.cargo\bin", [EnvironmentVariableTarget]::User)' + +# Enable linux desktop +flutter config --enable-windows-desktop + +# Fix any problems reported by flutter doctor +flutter doctor + +# Add the githooks directory to your git configuration +printMessage "Setting up githooks." +git config core.hooksPath .githooks + +# Install go-gitlint +printMessage "Installing go-gitlint." +GOLINT_FILENAME="go-gitlint_1.1.0_windows_x86_64.tar.gz" +if curl --proto '=https' --tlsv1.2 -sSfL https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} -o ${GOLINT_FILENAME}; then + tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint.exe + rm ${GOLINT_FILENAME} +else + printError "Failed to install go-gitlint" +fi + +# Change to the frontend directory +cd frontend + +# Install cargo make +printMessage "Installing cargo-make." +$USERPROFILE/.cargo/bin/cargo install --force cargo-make + +# Install duckscript +printMessage "Installing duckscript." +$USERPROFILE/.cargo/bin/cargo install --force duckscript_cli + +# Enable vcpkg integration +# Note: Requires admin +printMessage "Setting up vcpkg." +vcpkg integrate install + +# Check prerequisites +printMessage "Checking prerequisites." +PATH="$PATH;$LOCALAPPDATA\Pub\Cache\bin" bash -c '$USERPROFILE/.cargo/bin/cargo make flowy_dev' diff --git a/frontend/scripts/makefile/githooks.toml b/frontend/scripts/makefile/githooks.toml deleted file mode 100644 index f6b066ec49..0000000000 --- a/frontend/scripts/makefile/githooks.toml +++ /dev/null @@ -1,39 +0,0 @@ -[tasks.install-commitlint.mac] -script = [ - """ - brew install npm - npm install @commitlint/cli @commitlint/config-conventional --save-dev - - git config core.hooksPath .githooks - """, -] -script_runner = "@shell" - -[tasks.install-commitlint.windows] -script = [ - """ - echo "WIP" - - git config core.hooksPath .githooks - """, -] -script_runner = "@duckscript" - -[tasks.install-commitlint.linux] -script = [ - """ - if command -v apt &> /dev/null - then - echo "Installing node.js (sudo apt install nodejs)" - sudo apt install nodejs - else - echo "Installing node.js (sudo pacman -S nodejs)" - sudo pacman -S nodejs - fi - - npm install @commitlint/cli @commitlint/config-conventional --save-dev - - git config core.hooksPath .githooks - """, -] -script_runner = "@shell" diff --git a/package.json b/package.json deleted file mode 100644 index 0f6a7fcafe..0000000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "devDependencies": { - "@commitlint/cli": "^16.1.0", - "@commitlint/config-conventional": "^16.0.0" - } -} diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index 62d70be943..9b8e5f6081 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -332,9 +332,9 @@ dependencies = [ [[package]] name = "faccess" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e039175679baf763ddddf4f76900b92d4dae9411ee88cf42d2f11b976b09e07c" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" dependencies = [ "bitflags", "libc", diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs index 2298eb485f..09056f827d 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs @@ -123,6 +123,7 @@ pub struct FieldRevision { /// type_options contains key/value pairs /// key: id of the FieldType /// value: type option data that can be parsed into specified TypeOptionStruct. + /// /// For example, CheckboxTypeOption, MultiSelectTypeOption etc. #[serde(with = "indexmap::serde_seq")] pub type_options: IndexMap, @@ -185,15 +186,20 @@ impl FieldRevision { } } +/// The macro [impl_type_option] will implement the [TypeOptionDataEntry] for the type that +/// supports the serde trait and the TryInto trait. pub trait TypeOptionDataEntry { fn json_str(&self) -> String; fn protobuf_bytes(&self) -> Bytes; } +/// The macro [impl_type_option] will implement the [TypeOptionDataDeserializer] for the type that +/// supports the serde trait and the TryFrom trait. pub trait TypeOptionDataDeserializer { fn from_json_str(s: &str) -> Self; fn from_protobuf_bytes(bytes: Bytes) -> Self; } + pub type FieldId = String; #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct RowRevision { diff --git a/shared-lib/flowy-sync/src/client_document/default/mod.rs b/shared-lib/flowy-sync/src/client_document/default/mod.rs index f5fb180571..ee69e9efd9 100644 --- a/shared-lib/flowy-sync/src/client_document/default/mod.rs +++ b/shared-lib/flowy-sync/src/client_document/default/mod.rs @@ -7,13 +7,13 @@ pub fn initial_quill_delta() -> RichTextDelta { #[inline] pub fn initial_quill_delta_string() -> String { - initial_quill_delta().to_delta_str() + initial_quill_delta().json_str() } #[inline] pub fn initial_read_me() -> RichTextDelta { let json = include_str!("READ_ME.json"); - RichTextDelta::from_delta_str(json).unwrap() + RichTextDelta::from_json(json).unwrap() } #[cfg(test)] @@ -22,6 +22,6 @@ mod tests { #[test] fn load_read_me() { - println!("{}", initial_read_me().to_delta_str()); + println!("{}", initial_read_me().json_str()); } } diff --git a/shared-lib/flowy-sync/src/client_document/document_pad.rs b/shared-lib/flowy-sync/src/client_document/document_pad.rs index 72b52f6415..9a1037d0c8 100644 --- a/shared-lib/flowy-sync/src/client_document/document_pad.rs +++ b/shared-lib/flowy-sync/src/client_document/document_pad.rs @@ -55,16 +55,16 @@ impl ClientDocument { } pub fn from_json(json: &str) -> Result { - let delta = RichTextDelta::from_delta_str(json)?; + let delta = RichTextDelta::from_json(json)?; Ok(Self::from_delta(delta)) } pub fn delta_str(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } pub fn to_bytes(&self) -> Bytes { - self.delta.to_delta_bytes() + self.delta.json_bytes() } pub fn to_plain_string(&self) -> String { @@ -85,7 +85,7 @@ impl ClientDocument { } pub fn set_delta(&mut self, data: RichTextDelta) { - tracing::trace!("document: {}", data.to_delta_str()); + tracing::trace!("document: {}", data.json_str()); self.delta = data; match &self.notify { @@ -97,7 +97,7 @@ impl ClientDocument { } pub fn compose_delta(&mut self, delta: RichTextDelta) -> Result<(), CollaborateError> { - tracing::trace!("{} compose {}", &self.delta.to_delta_str(), delta.to_delta_str()); + tracing::trace!("{} compose {}", &self.delta.json_str(), delta.json_str()); let composed_delta = self.delta.compose(&delta)?; let mut undo_delta = delta.invert(&self.delta); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs b/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs index 4da15e5f06..6c7283e4c7 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs @@ -1,6 +1,6 @@ use crate::{client_document::DeleteExt, util::is_newline}; use lib_ot::{ - core::{Attributes, DeltaBuilder, DeltaIter, Interval, Utf16CodeUnitMetric, NEW_LINE}, + core::{Attributes, DeltaBuilder, DeltaIterator, Interval, Utf16CodeUnitMetric, NEW_LINE}, rich_text::{plain_attributes, RichTextDelta}, }; @@ -16,7 +16,7 @@ impl DeleteExt for PreserveLineFormatOnMerge { } // seek to the interval start pos. e.g. You backspace enter pos - let mut iter = DeltaIter::from_offset(delta, interval.start); + let mut iter = DeltaIterator::from_offset(delta, interval.start); // op will be the "\n" let newline_op = iter.next_op_with_len(1)?; diff --git a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs index 46a0e0290d..4a4c00a19d 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs @@ -1,5 +1,5 @@ use lib_ot::{ - core::{DeltaBuilder, DeltaIter, Interval}, + core::{DeltaBuilder, DeltaIterator, Interval}, rich_text::{plain_attributes, AttributeScope, RichTextAttribute, RichTextDelta}, }; @@ -20,7 +20,7 @@ impl FormatExt for ResolveBlockFormat { } let mut new_delta = DeltaBuilder::new().retain(interval.start).build(); - let mut iter = DeltaIter::from_offset(delta, interval.start); + let mut iter = DeltaIterator::from_offset(delta, interval.start); let mut start = 0; let end = interval.size(); while start < end && iter.has_next() { diff --git a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs index 383b780aca..567d06d6f2 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs @@ -1,5 +1,5 @@ use lib_ot::{ - core::{DeltaBuilder, DeltaIter, Interval}, + core::{DeltaBuilder, DeltaIterator, Interval}, rich_text::{AttributeScope, RichTextAttribute, RichTextDelta}, }; @@ -19,7 +19,7 @@ impl FormatExt for ResolveInlineFormat { return None; } let mut new_delta = DeltaBuilder::new().retain(interval.start).build(); - let mut iter = DeltaIter::from_offset(delta, interval.start); + let mut iter = DeltaIterator::from_offset(delta, interval.start); let mut start = 0; let end = interval.size(); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs index e41470b9eb..253833d255 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs @@ -1,8 +1,6 @@ use crate::{client_document::InsertExt, util::is_newline}; -use lib_ot::{ - core::{is_empty_line_at_index, DeltaBuilder, DeltaIter}, - rich_text::{attributes_except_header, RichTextAttributeKey, RichTextDelta}, -}; +use lib_ot::core::{is_empty_line_at_index, DeltaBuilder, DeltaIterator}; +use lib_ot::rich_text::{attributes_except_header, RichTextAttributeKey, RichTextDelta}; pub struct AutoExitBlock {} @@ -21,7 +19,7 @@ impl InsertExt for AutoExitBlock { return None; } - let mut iter = DeltaIter::from_offset(delta, index); + let mut iter = DeltaIterator::from_offset(delta, index); let next = iter.next_op()?; let mut attributes = next.get_attributes(); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs index d7f1da4f89..4c7cd642c2 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs @@ -1,6 +1,6 @@ use crate::{client_document::InsertExt, util::is_whitespace}; use lib_ot::{ - core::{count_utf16_code_units, DeltaBuilder, DeltaIter}, + core::{count_utf16_code_units, DeltaBuilder, DeltaIterator}, rich_text::{plain_attributes, RichTextAttribute, RichTextAttributes, RichTextDelta}, }; use std::cmp::min; @@ -17,7 +17,7 @@ impl InsertExt for AutoFormatExt { if !is_whitespace(text) { return None; } - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); if let Some(prev) = iter.next_op_with_len(index) { match AutoFormat::parse(prev.get_data()) { None => {} diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs index 628d55cb24..c165985493 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs @@ -1,6 +1,6 @@ use crate::client_document::InsertExt; use lib_ot::{ - core::{Attributes, DeltaBuilder, DeltaIter, NEW_LINE}, + core::{Attributes, DeltaBuilder, DeltaIterator, NEW_LINE}, rich_text::{RichTextAttributeKey, RichTextAttributes, RichTextDelta}, }; @@ -11,7 +11,7 @@ impl InsertExt for DefaultInsertAttribute { } fn apply(&self, delta: &RichTextDelta, replace_len: usize, text: &str, index: usize) -> Option { - let iter = DeltaIter::new(delta); + let iter = DeltaIterator::new(delta); let mut attributes = RichTextAttributes::new(); // Enable each line split by "\n" remains the block attributes. for example: diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs index 2c0ceae111..03c9723d7b 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs @@ -1,6 +1,6 @@ use crate::{client_document::InsertExt, util::is_newline}; use lib_ot::{ - core::{DeltaBuilder, DeltaIter, NEW_LINE}, + core::{DeltaBuilder, DeltaIterator, NEW_LINE}, rich_text::{ attributes_except_header, plain_attributes, RichTextAttribute, RichTextAttributeKey, RichTextAttributes, RichTextDelta, @@ -18,7 +18,7 @@ impl InsertExt for PreserveBlockFormatOnInsert { return None; } - let mut iter = DeltaIter::from_offset(delta, index); + let mut iter = DeltaIterator::from_offset(delta, index); match iter.next_op_with_newline() { None => {} Some((newline_op, offset)) => { diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs index 37b31e3d26..b0de6451aa 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs @@ -3,7 +3,7 @@ use crate::{ util::{contain_newline, is_newline}, }; use lib_ot::{ - core::{DeltaBuilder, DeltaIter, OpNewline, NEW_LINE}, + core::{DeltaBuilder, DeltaIterator, OpNewline, NEW_LINE}, rich_text::{plain_attributes, RichTextAttributeKey, RichTextDelta}, }; @@ -18,7 +18,7 @@ impl InsertExt for PreserveInlineFormat { return None; } - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); let prev = iter.next_op_with_len(index)?; if OpNewline::parse(&prev).is_contain() { return None; @@ -64,7 +64,7 @@ impl InsertExt for PreserveLineFormatOnSplit { return None; } - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); let prev = iter.next_op_with_len(index)?; if OpNewline::parse(&prev).is_end() { return None; diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs index f63bf9bddf..8671049ee7 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs @@ -1,6 +1,6 @@ use crate::{client_document::InsertExt, util::is_newline}; use lib_ot::{ - core::{DeltaBuilder, DeltaIter, Utf16CodeUnitMetric, NEW_LINE}, + core::{DeltaBuilder, DeltaIterator, Utf16CodeUnitMetric, NEW_LINE}, rich_text::{RichTextAttributeKey, RichTextAttributes, RichTextDelta}, }; @@ -15,7 +15,7 @@ impl InsertExt for ResetLineFormatOnNewLine { return None; } - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); iter.seek::(index); let next_op = iter.next_op()?; if !next_op.get_data().starts_with(NEW_LINE) { diff --git a/shared-lib/flowy-sync/src/client_folder/builder.rs b/shared-lib/flowy-sync/src/client_folder/builder.rs index 4c27f278f5..3855d62834 100644 --- a/shared-lib/flowy-sync/src/client_folder/builder.rs +++ b/shared-lib/flowy-sync/src/client_folder/builder.rs @@ -7,7 +7,7 @@ use crate::{ }; use flowy_folder_data_model::revision::{TrashRevision, WorkspaceRevision}; -use lib_ot::core::{PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder}; +use lib_ot::core::{PhantomAttributes, TextDelta, TextDeltaBuilder}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -35,15 +35,15 @@ impl FolderPadBuilder { self } - pub(crate) fn build_with_delta(self, mut delta: PlainTextDelta) -> CollaborateResult { + pub(crate) fn build_with_delta(self, mut delta: TextDelta) -> CollaborateResult { if delta.is_empty() { delta = default_folder_delta(); } // TODO: Reconvert from history if delta.to_str() failed. - let folder_json = delta.to_str()?; - let mut folder: FolderPad = serde_json::from_str(&folder_json).map_err(|e| { - tracing::error!("Deserialize folder from json failed: {}", folder_json); + let content = delta.content()?; + let mut folder: FolderPad = serde_json::from_str(&content).map_err(|e| { + tracing::error!("Deserialize folder from {} failed", content); return CollaborateError::internal().context(format!("Deserialize delta to folder failed: {}", e)); })?; folder.delta = delta; @@ -51,7 +51,7 @@ impl FolderPadBuilder { } pub(crate) fn build_with_revisions(self, revisions: Vec) -> CollaborateResult { - let folder_delta: FolderDelta = make_delta_from_revisions::(revisions)?; + let folder_delta: FolderDelta = make_delta_from_revisions::(revisions)?; self.build_with_delta(folder_delta) } @@ -61,7 +61,7 @@ impl FolderPadBuilder { Ok(FolderPad { workspaces: self.workspaces, trash: self.trash, - delta: PlainTextDeltaBuilder::new().insert(&json).build(), + delta: TextDeltaBuilder::new().insert(&json).build(), }) } } diff --git a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs index f821c03e6f..5927be2c34 100644 --- a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs @@ -295,7 +295,7 @@ impl FolderPad { } pub fn md5(&self) -> String { - md5(&self.delta.to_delta_bytes()) + md5(&self.delta.json_bytes()) } pub fn to_json(&self) -> CollaborateResult { @@ -315,7 +315,7 @@ impl FolderPad { Some(_) => { let old = cloned_self.to_json()?; let new = self.to_json()?; - match cal_diff::(old, new) { + match cal_diff::(old, new) { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; @@ -350,7 +350,7 @@ impl FolderPad { Some(_) => { let old = cloned_self.to_json()?; let new = self.to_json()?; - match cal_diff::(old, new) { + match cal_diff::(old, new) { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; @@ -400,14 +400,14 @@ impl FolderPad { } pub fn default_folder_delta() -> FolderDelta { - PlainTextDeltaBuilder::new() + TextDeltaBuilder::new() .insert(r#"{"workspaces":[],"trash":[]}"#) .build() } pub fn initial_folder_delta(folder_pad: &FolderPad) -> CollaborateResult { let json = folder_pad.to_json()?; - let delta = PlainTextDeltaBuilder::new().insert(&json).build(); + let delta = TextDeltaBuilder::new().insert(&json).build(); Ok(delta) } @@ -434,7 +434,7 @@ mod tests { use chrono::Utc; use flowy_folder_data_model::revision::{AppRevision, TrashRevision, ViewRevision, WorkspaceRevision}; - use lib_ot::core::{OperationTransformable, PlainTextDelta, PlainTextDeltaBuilder}; + use lib_ot::core::{OperationTransform, TextDelta, TextDeltaBuilder}; #[test] fn folder_add_workspace() { @@ -749,7 +749,7 @@ mod tests { fn test_folder() -> (FolderPad, FolderDelta, WorkspaceRevision) { let mut folder = FolderPad::default(); let folder_json = serde_json::to_string(&folder).unwrap(); - let mut delta = PlainTextDeltaBuilder::new().insert(&folder_json).build(); + let mut delta = TextDeltaBuilder::new().insert(&folder_json).build(); let mut workspace_rev = WorkspaceRevision::default(); workspace_rev.name = "😁 my first workspace".to_owned(); @@ -791,7 +791,7 @@ mod tests { fn test_trash() -> (FolderPad, FolderDelta, TrashRevision) { let mut folder = FolderPad::default(); let folder_json = serde_json::to_string(&folder).unwrap(); - let mut delta = PlainTextDeltaBuilder::new().insert(&folder_json).build(); + let mut delta = TextDeltaBuilder::new().insert(&folder_json).build(); let mut trash_rev = TrashRevision::default(); trash_rev.name = "🚽 my first trash".to_owned(); @@ -810,7 +810,7 @@ mod tests { (folder, delta, trash_rev) } - fn make_folder_from_delta(mut initial_delta: FolderDelta, deltas: Vec) -> FolderPad { + fn make_folder_from_delta(mut initial_delta: FolderDelta, deltas: Vec) -> FolderPad { for delta in deltas { initial_delta = initial_delta.compose(&delta).unwrap(); } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs index 16b98dc7fc..51a331ecf7 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs @@ -4,13 +4,13 @@ use crate::util::{cal_diff, make_delta_from_revisions}; use flowy_grid_data_model::revision::{ gen_block_id, gen_row_id, CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision, }; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -pub type GridBlockRevisionDelta = PlainTextDelta; -pub type GridBlockRevisionDeltaBuilder = PlainTextDeltaBuilder; +pub type GridBlockRevisionDelta = TextDelta; +pub type GridBlockRevisionDeltaBuilder = TextDeltaBuilder; #[derive(Debug, Clone)] pub struct GridBlockRevisionPad { @@ -46,7 +46,7 @@ impl GridBlockRevisionPad { } pub fn from_delta(delta: GridBlockRevisionDelta) -> CollaborateResult { - let s = delta.to_str()?; + let s = delta.content()?; let block_revision: GridBlockRevision = serde_json::from_str(&s).map_err(|e| { let msg = format!("Deserialize delta to block meta failed: {}", e); tracing::error!("{}", s); @@ -56,7 +56,7 @@ impl GridBlockRevisionPad { } pub fn from_revisions(_grid_id: &str, revisions: Vec) -> CollaborateResult { - let block_delta: GridBlockRevisionDelta = make_delta_from_revisions::(revisions)?; + let block_delta: GridBlockRevisionDelta = make_delta_from_revisions::(revisions)?; Self::from_delta(block_delta) } @@ -195,10 +195,10 @@ impl GridBlockRevisionPad { Some(_) => { let old = cloned_self.to_json()?; let new = self.to_json()?; - match cal_diff::(old, new) { + match cal_diff::(old, new) { None => Ok(None), Some(delta) => { - tracing::trace!("[GridBlockMeta] Composing delta {}", delta.to_delta_str()); + tracing::trace!("[GridBlockMeta] Composing delta {}", delta.json_str()); // tracing::debug!( // "[GridBlockMeta] current delta: {}", // self.delta.to_str().unwrap_or_else(|_| "".to_string()) @@ -231,11 +231,11 @@ impl GridBlockRevisionPad { } pub fn md5(&self) -> String { - md5(&self.delta.to_delta_bytes()) + md5(&self.delta.json_bytes()) } pub fn delta_str(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } } @@ -247,12 +247,12 @@ pub struct GridBlockMetaChange { pub fn make_grid_block_delta(block_rev: &GridBlockRevision) -> GridBlockRevisionDelta { let json = serde_json::to_string(&block_rev).unwrap(); - PlainTextDeltaBuilder::new().insert(&json).build() + TextDeltaBuilder::new().insert(&json).build() } pub fn make_grid_block_revisions(user_id: &str, grid_block_meta_data: &GridBlockRevision) -> RepeatedRevision { let delta = make_grid_block_delta(grid_block_meta_data); - let bytes = delta.to_delta_bytes(); + let bytes = delta.json_bytes(); let revision = Revision::initial_revision(user_id, &grid_block_meta_data.block_id, bytes); revision.into() } @@ -289,7 +289,7 @@ mod tests { let change = pad.add_row_rev(row.clone(), None).unwrap().unwrap(); assert_eq!(pad.rows.first().unwrap().as_ref(), &row); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":24},{"insert":"{\"id\":\"1\",\"block_id\":\"1\",\"cells\":[],\"height\":0,\"visibility\":false}"},{"retain":2}]"# ); } @@ -303,19 +303,19 @@ mod tests { let change = pad.add_row_rev(row_1.clone(), None).unwrap().unwrap(); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":24},{"insert":"{\"id\":\"1\",\"block_id\":\"1\",\"cells\":[],\"height\":0,\"visibility\":false}"},{"retain":2}]"# ); let change = pad.add_row_rev(row_2.clone(), None).unwrap().unwrap(); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":90},{"insert":",{\"id\":\"2\",\"block_id\":\"1\",\"cells\":[],\"height\":0,\"visibility\":false}"},{"retain":2}]"# ); let change = pad.add_row_rev(row_3.clone(), Some("2".to_string())).unwrap().unwrap(); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":157},{"insert":",{\"id\":\"3\",\"block_id\":\"1\",\"cells\":[],\"height\":0,\"visibility\":false}"},{"retain":2}]"# ); @@ -380,10 +380,7 @@ mod tests { let _ = pad.add_row_rev(row.clone(), None).unwrap().unwrap(); let change = pad.delete_rows(vec![Cow::Borrowed(&row.id)]).unwrap().unwrap(); - assert_eq!( - change.delta.to_delta_str(), - r#"[{"retain":24},{"delete":66},{"retain":2}]"# - ); + assert_eq!(change.delta.json_str(), r#"[{"retain":24},{"delete":66},{"retain":2}]"#); assert_eq!(pad.delta_str(), pre_delta_str); } @@ -410,7 +407,7 @@ mod tests { let change = pad.update_row(changeset).unwrap().unwrap(); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":69},{"insert":"10"},{"retain":15},{"insert":"tru"},{"delete":4},{"retain":4}]"# ); @@ -421,8 +418,7 @@ mod tests { } fn test_pad() -> GridBlockRevisionPad { - let delta = - GridBlockRevisionDelta::from_delta_str(r#"[{"insert":"{\"block_id\":\"1\",\"rows\":[]}"}]"#).unwrap(); + let delta = GridBlockRevisionDelta::from_json(r#"[{"insert":"{\"block_id\":\"1\",\"rows\":[]}"}]"#).unwrap(); GridBlockRevisionPad::from_delta(delta).unwrap() } } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index eaa6f1823a..b0188066ef 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -9,12 +9,12 @@ use flowy_grid_data_model::revision::{ GridLayoutRevision, GridRevision, GridSettingRevision, GridSortRevision, }; use lib_infra::util::move_vec_element; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; use std::collections::HashMap; use std::sync::Arc; -pub type GridRevisionDelta = PlainTextDelta; -pub type GridRevisionDeltaBuilder = PlainTextDeltaBuilder; +pub type GridRevisionDelta = TextDelta; +pub type GridRevisionDeltaBuilder = TextDeltaBuilder; pub struct GridRevisionPad { grid_rev: Arc, @@ -52,8 +52,8 @@ impl GridRevisionPad { } pub fn from_delta(delta: GridRevisionDelta) -> CollaborateResult { - let s = delta.to_str()?; - let grid: GridRevision = serde_json::from_str(&s) + let content = delta.content()?; + let grid: GridRevision = serde_json::from_str(&content) .map_err(|e| CollaborateError::internal().context(format!("Deserialize delta to grid failed: {}", e)))?; Ok(Self { @@ -63,7 +63,7 @@ impl GridRevisionPad { } pub fn from_revisions(revisions: Vec) -> CollaborateResult { - let grid_delta: GridRevisionDelta = make_delta_from_revisions::(revisions)?; + let grid_delta: GridRevisionDelta = make_delta_from_revisions::(revisions)?; Self::from_delta(grid_delta) } @@ -457,15 +457,15 @@ impl GridRevisionPad { } pub fn md5(&self) -> String { - md5(&self.delta.to_delta_bytes()) + md5(&self.delta.json_bytes()) } pub fn delta_str(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } pub fn delta_bytes(&self) -> Bytes { - self.delta.to_delta_bytes() + self.delta.json_bytes() } pub fn fields(&self) -> &[Arc] { @@ -482,7 +482,7 @@ impl GridRevisionPad { Some(_) => { let old = make_grid_rev_json_str(&cloned_grid)?; let new = self.json_str()?; - match cal_diff::(old, new) { + match cal_diff::(old, new) { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; @@ -548,12 +548,12 @@ pub struct GridChangeset { pub fn make_grid_delta(grid_rev: &GridRevision) -> GridRevisionDelta { let json = serde_json::to_string(&grid_rev).unwrap(); - PlainTextDeltaBuilder::new().insert(&json).build() + TextDeltaBuilder::new().insert(&json).build() } pub fn make_grid_revisions(user_id: &str, grid_rev: &GridRevision) -> RepeatedRevision { let delta = make_grid_delta(grid_rev); - let bytes = delta.to_delta_bytes(); + let bytes = delta.json_bytes(); let revision = Revision::initial_revision(user_id, &grid_rev.grid_id, bytes); revision.into() } diff --git a/shared-lib/flowy-sync/src/entities/folder.rs b/shared-lib/flowy-sync/src/entities/folder.rs index 214190f517..952827d7d6 100644 --- a/shared-lib/flowy-sync/src/entities/folder.rs +++ b/shared-lib/flowy-sync/src/entities/folder.rs @@ -1,7 +1,7 @@ use flowy_derive::ProtoBuf; -use lib_ot::core::PlainTextDelta; +use lib_ot::core::TextDelta; -pub type FolderDelta = PlainTextDelta; +pub type FolderDelta = TextDelta; #[derive(ProtoBuf, Default, Debug, Clone, Eq, PartialEq)] pub struct FolderInfo { diff --git a/shared-lib/flowy-sync/src/entities/revision.rs b/shared-lib/flowy-sync/src/entities/revision.rs index d909efa3d5..55567a43bf 100644 --- a/shared-lib/flowy-sync/src/entities/revision.rs +++ b/shared-lib/flowy-sync/src/entities/revision.rs @@ -89,7 +89,7 @@ impl std::fmt::Debug for Revision { let _ = f.write_fmt(format_args!("rev_id {}, ", self.rev_id))?; match RichTextDelta::from_bytes(&self.delta_data) { Ok(delta) => { - let _ = f.write_fmt(format_args!("delta {:?}", delta.to_delta_str()))?; + let _ = f.write_fmt(format_args!("delta {:?}", delta.json_str()))?; } Err(e) => { let _ = f.write_fmt(format_args!("delta {:?}", e))?; diff --git a/shared-lib/flowy-sync/src/entities/text_block.rs b/shared-lib/flowy-sync/src/entities/text_block.rs index 8753103369..7cb8c6df63 100644 --- a/shared-lib/flowy-sync/src/entities/text_block.rs +++ b/shared-lib/flowy-sync/src/entities/text_block.rs @@ -46,7 +46,7 @@ impl std::convert::TryFrom for DocumentPB { } let delta = RichTextDelta::from_bytes(&revision.delta_data)?; - let doc_json = delta.to_delta_str(); + let doc_json = delta.json_str(); Ok(DocumentPB { block_id: revision.object_id, diff --git a/shared-lib/flowy-sync/src/server_document/document_pad.rs b/shared-lib/flowy-sync/src/server_document/document_pad.rs index 1f4fa7bda1..28623a0169 100644 --- a/shared-lib/flowy-sync/src/server_document/document_pad.rs +++ b/shared-lib/flowy-sync/src/server_document/document_pad.rs @@ -39,7 +39,7 @@ impl RevisionSyncObject for ServerDocument { } fn to_json(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } fn set_delta(&mut self, new_delta: Delta) { diff --git a/shared-lib/flowy-sync/src/server_folder/folder_manager.rs b/shared-lib/flowy-sync/src/server_folder/folder_manager.rs index 16b753f50b..95e3c2330f 100644 --- a/shared-lib/flowy-sync/src/server_folder/folder_manager.rs +++ b/shared-lib/flowy-sync/src/server_folder/folder_manager.rs @@ -13,7 +13,7 @@ use crate::{ use async_stream::stream; use futures::stream::StreamExt; use lib_infra::future::BoxResultFuture; -use lib_ot::core::PlainTextAttributes; +use lib_ot::core::PhantomAttributes; use std::{collections::HashMap, fmt::Debug, sync::Arc}; use tokio::{ sync::{mpsc, oneshot, RwLock}, @@ -188,7 +188,7 @@ impl ServerFolderManager { } } -type FolderRevisionSynchronizer = RevisionSynchronizer; +type FolderRevisionSynchronizer = RevisionSynchronizer; struct OpenFolderHandler { folder_id: String, diff --git a/shared-lib/flowy-sync/src/server_folder/folder_pad.rs b/shared-lib/flowy-sync/src/server_folder/folder_pad.rs index 09b4c9d048..74e164600c 100644 --- a/shared-lib/flowy-sync/src/server_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/server_folder/folder_pad.rs @@ -1,5 +1,5 @@ use crate::{entities::folder::FolderDelta, errors::CollaborateError, synchronizer::RevisionSyncObject}; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta}; pub struct ServerFolder { folder_id: String, @@ -15,27 +15,27 @@ impl ServerFolder { } } -impl RevisionSyncObject for ServerFolder { +impl RevisionSyncObject for ServerFolder { fn id(&self) -> &str { &self.folder_id } - fn compose(&mut self, other: &PlainTextDelta) -> Result<(), CollaborateError> { + fn compose(&mut self, other: &TextDelta) -> Result<(), CollaborateError> { let new_delta = self.delta.compose(other)?; self.delta = new_delta; Ok(()) } - fn transform(&self, other: &PlainTextDelta) -> Result<(PlainTextDelta, PlainTextDelta), CollaborateError> { + fn transform(&self, other: &TextDelta) -> Result<(TextDelta, TextDelta), CollaborateError> { let value = self.delta.transform(other)?; Ok(value) } fn to_json(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } - fn set_delta(&mut self, new_delta: PlainTextDelta) { + fn set_delta(&mut self, new_delta: TextDelta) { self.delta = new_delta; } } diff --git a/shared-lib/flowy-sync/src/util.rs b/shared-lib/flowy-sync/src/util.rs index c4504a806a..7dd5c4af5c 100644 --- a/shared-lib/flowy-sync/src/util.rs +++ b/shared-lib/flowy-sync/src/util.rs @@ -7,9 +7,9 @@ use crate::{ errors::{CollaborateError, CollaborateResult}, }; use dissimilar::Chunk; -use lib_ot::core::{DeltaBuilder, FlowyStr}; +use lib_ot::core::{DeltaBuilder, OTString}; use lib_ot::{ - core::{Attributes, Delta, OperationTransformable, NEW_LINE, WHITESPACE}, + core::{Attributes, Delta, OperationTransform, NEW_LINE, WHITESPACE}, rich_text::RichTextDelta, }; use serde::de::DeserializeOwned; @@ -149,7 +149,7 @@ pub fn make_folder_from_revisions_pb( folder_delta = folder_delta.compose(&delta)?; } - let text = folder_delta.to_delta_str(); + let text = folder_delta.json_str(); Ok(Some(FolderInfo { folder_id: folder_id.to_string(), text, @@ -183,7 +183,7 @@ pub fn make_document_from_revision_pbs( delta = delta.compose(&new_delta)?; } - let text = delta.to_delta_str(); + let text = delta.json_str(); Ok(Some(DocumentPB { block_id: doc_id.to_owned(), @@ -208,10 +208,10 @@ pub fn cal_diff(old: String, new: String) -> Option> { for chunk in &chunks { match chunk { Chunk::Equal(s) => { - delta_builder = delta_builder.retain(FlowyStr::from(*s).utf16_size()); + delta_builder = delta_builder.retain(OTString::from(*s).utf16_len()); } Chunk::Delete(s) => { - delta_builder = delta_builder.delete(FlowyStr::from(*s).utf16_size()); + delta_builder = delta_builder.delete(OTString::from(*s).utf16_len()); } Chunk::Insert(s) => { delta_builder = delta_builder.insert(*s); diff --git a/shared-lib/lib-ot/src/codec/markdown/mod.rs b/shared-lib/lib-ot/src/codec/markdown/mod.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/shared-lib/lib-ot/src/codec/markdown/mod.rs @@ -0,0 +1 @@ + diff --git a/shared-lib/lib-ot/src/codec/mod.rs b/shared-lib/lib-ot/src/codec/mod.rs new file mode 100644 index 0000000000..163a4fba82 --- /dev/null +++ b/shared-lib/lib-ot/src/codec/mod.rs @@ -0,0 +1 @@ +pub mod markdown; diff --git a/shared-lib/lib-ot/src/core/delta/builder.rs b/shared-lib/lib-ot/src/core/delta/builder.rs index b065b645af..482b1ca8a9 100644 --- a/shared-lib/lib-ot/src/core/delta/builder.rs +++ b/shared-lib/lib-ot/src/core/delta/builder.rs @@ -1,7 +1,21 @@ -use crate::core::{trim, Attributes, Delta, PlainTextAttributes}; - -pub type PlainTextDeltaBuilder = DeltaBuilder; +use crate::core::delta::{trim, Delta}; +use crate::core::operation::Attributes; +use crate::core::Operation; +/// A builder for creating new [Delta] objects. +/// +/// Note that all edit operations must be sorted; the start point of each +/// interval must be no less than the end point of the previous one. +/// +/// # Examples +/// +/// ``` +/// use lib_ot::core::TextDeltaBuilder; +/// let delta = TextDeltaBuilder::new() +/// .insert("AppFlowy") +/// .build(); +/// assert_eq!(delta.content().unwrap(), "AppFlowy"); +/// ``` pub struct DeltaBuilder { delta: Delta, } @@ -23,6 +37,26 @@ where DeltaBuilder::default() } + pub fn from_operations(operations: Vec>) -> Delta { + let mut delta = DeltaBuilder::default().build(); + operations.into_iter().for_each(|operation| { + delta.add(operation); + }); + delta + } + + /// Retain the 'n' characters with the attributes. Use 'retain' instead if you don't + /// need any attributes. + /// # Examples + /// + /// ``` + /// use lib_ot::rich_text::{RichTextAttribute, RichTextDelta, RichTextDeltaBuilder}; + /// + /// let mut attribute = RichTextAttribute::Bold(true); + /// let delta = RichTextDeltaBuilder::new().retain_with_attributes(7, attribute.into()).build(); + /// + /// assert_eq!(delta.json_str(), r#"[{"retain":7,"attributes":{"bold":true}}]"#); + /// ``` pub fn retain_with_attributes(mut self, n: usize, attrs: T) -> Self { self.delta.retain(n, attrs); self @@ -33,11 +67,32 @@ where self } + /// Deletes the given interval. Panics if interval is not properly sorted. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OperationTransform, TextDeltaBuilder}; + /// + /// let delta = TextDeltaBuilder::new() + /// .insert("AppFlowy...") + /// .build(); + /// + /// let changeset = TextDeltaBuilder::new() + /// .retain(8) + /// .delete(3) + /// .build(); + /// + /// let new_delta = delta.compose(&changeset).unwrap(); + /// assert_eq!(new_delta.content().unwrap(), "AppFlowy"); + /// ``` pub fn delete(mut self, n: usize) -> Self { self.delta.delete(n); self } + /// Inserts the string with attributes. Use 'insert' instead if you don't + /// need any attributes. pub fn insert_with_attributes(mut self, s: &str, attrs: T) -> Self { self.delta.insert(s, attrs); self @@ -48,11 +103,31 @@ where self } + /// Removes trailing retain operation with empty attributes + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OperationTransform, TextDeltaBuilder}; + /// use lib_ot::rich_text::{RichTextAttribute, RichTextDeltaBuilder}; + /// let delta = TextDeltaBuilder::new() + /// .retain(3) + /// .trim() + /// .build(); + /// assert_eq!(delta.ops.len(), 0); + /// + /// let delta = RichTextDeltaBuilder::new() + /// .retain_with_attributes(3, RichTextAttribute::Bold(true).into()) + /// .trim() + /// .build(); + /// assert_eq!(delta.ops.len(), 1); + /// ``` pub fn trim(mut self) -> Self { trim(&mut self.delta); self } + /// Builds the `Delta` pub fn build(self) -> Delta { self.delta } diff --git a/shared-lib/lib-ot/src/core/delta/cursor.rs b/shared-lib/lib-ot/src/core/delta/cursor.rs index d6ebb8b3cc..a068f1c029 100644 --- a/shared-lib/lib-ot/src/core/delta/cursor.rs +++ b/shared-lib/lib-ot/src/core/delta/cursor.rs @@ -1,33 +1,54 @@ #![allow(clippy::while_let_on_iterator)] -use crate::{ - core::{Attributes, Delta, Interval, Operation}, - errors::{ErrorBuilder, OTError, OTErrorCode}, -}; +use crate::core::delta::Delta; +use crate::core::interval::Interval; +use crate::core::operation::{Attributes, Operation}; +use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use std::{cmp::min, iter::Enumerate, slice::Iter}; +/// A [DeltaCursor] is used to iterate the delta and return the corresponding delta. #[derive(Debug)] -pub struct OpCursor<'a, T: Attributes> { +pub struct DeltaCursor<'a, T: Attributes> { pub(crate) delta: &'a Delta, pub(crate) origin_iv: Interval, pub(crate) consume_iv: Interval, pub(crate) consume_count: usize, - pub(crate) op_index: usize, + pub(crate) op_offset: usize, iter: Enumerate>>, next_op: Option>, } -impl<'a, T> OpCursor<'a, T> +impl<'a, T> DeltaCursor<'a, T> where T: Attributes, { - pub fn new(delta: &'a Delta, interval: Interval) -> OpCursor<'a, T> { + /// # Arguments + /// + /// * `delta`: The delta you want to iterate over. + /// * `interval`: The range for the cursor movement. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{DeltaCursor, DeltaIterator, Interval, Operation}; + /// use lib_ot::rich_text::RichTextDelta; + /// let mut delta = RichTextDelta::default(); + /// delta.add(Operation::insert("123")); + /// delta.add(Operation::insert("4")); + /// + /// let mut cursor = DeltaCursor::new(&delta, Interval::new(0, 3)); + /// assert_eq!(cursor.next_iv(), Interval::new(0,3)); + /// assert_eq!(cursor.next_with_len(Some(2)).unwrap(), Operation::insert("12")); + /// assert_eq!(cursor.get_next_op().unwrap(), Operation::insert("3")); + /// assert_eq!(cursor.get_next_op(), None); + /// ``` + pub fn new(delta: &'a Delta, interval: Interval) -> DeltaCursor<'a, T> { // debug_assert!(interval.start <= delta.target_len); let mut cursor = Self { delta, origin_iv: interval, consume_iv: interval, consume_count: 0, - op_index: 0, + op_offset: 0, iter: delta.ops.iter().enumerate(), next_op: None, }; @@ -35,17 +56,37 @@ where cursor } - // get the next operation interval + /// Returns the next operation interval pub fn next_iv(&self) -> Interval { self.next_iv_with_len(None).unwrap_or_else(|| Interval::new(0, 0)) } - pub fn next_op(&mut self) -> Option> { + /// Returns the next operation + pub fn get_next_op(&mut self) -> Option> { self.next_with_len(None) } - // get the last operation before the end. - // checkout the delta_next_op_with_len_cross_op_return_last test for more detail + /// Returns the reference of the next operation + pub fn next_op(&self) -> Option<&Operation> { + let mut next_op = self.next_op.as_ref(); + if next_op.is_none() { + let mut offset = 0; + for op in &self.delta.ops { + offset += op.len(); + if offset > self.consume_count { + next_op = Some(op); + break; + } + } + } + next_op + } + + /// # Arguments + /// + /// * `expected_len`: Return the next operation with the specified length. + /// + /// pub fn next_with_len(&mut self, expected_len: Option) -> Option> { let mut find_op = None; let holder = self.next_op.clone(); @@ -97,17 +138,24 @@ where } pub fn has_next(&self) -> bool { - self.next_iter_op().is_some() + self.next_op().is_some() } - fn descend(&mut self, index: usize) { - self.consume_iv.start += index; + /// Finds the op within the current offset. + /// This function sets the start of the consume_iv to the offset, updates the consume_count + /// and the next_op reference. + /// + /// # Arguments + /// + /// * `offset`: Represents the offset of the delta string, in Utf16CodeUnit unit. + fn descend(&mut self, offset: usize) { + self.consume_iv.start += offset; if self.consume_count >= self.consume_iv.start { return; } while let Some((o_index, op)) = self.iter.next() { - self.op_index = o_index; + self.op_offset = o_index; let start = self.consume_count; let end = start + op.len(); let intersect = Interval::new(start, end).intersect(self.consume_iv); @@ -121,7 +169,7 @@ where } fn next_iv_with_len(&self, expected_len: Option) -> Option { - let op = self.next_iter_op()?; + let op = self.next_op()?; let start = self.consume_count; let end = match expected_len { None => self.consume_count + op.len(), @@ -132,31 +180,16 @@ where let interval = intersect.translate_neg(start); Some(interval) } - - pub fn next_iter_op(&self) -> Option<&Operation> { - let mut next_op = self.next_op.as_ref(); - if next_op.is_none() { - let mut offset = 0; - for op in &self.delta.ops { - offset += op.len(); - if offset > self.consume_count { - next_op = Some(op); - break; - } - } - } - next_op - } } -fn find_next<'a, T>(cursor: &mut OpCursor<'a, T>) -> Option<&'a Operation> +fn find_next<'a, T>(cursor: &mut DeltaCursor<'a, T>) -> Option<&'a Operation> where T: Attributes, { match cursor.iter.next() { None => None, Some((o_index, op)) => { - cursor.op_index = o_index; + cursor.op_offset = o_index; Some(op) } } @@ -164,31 +197,34 @@ where type SeekResult = Result<(), OTError>; pub trait Metric { - fn seek(cursor: &mut OpCursor, offset: usize) -> SeekResult; + fn seek(cursor: &mut DeltaCursor, offset: usize) -> SeekResult; } +/// [OpMetric] is used by [DeltaIterator] for seeking operations +/// The unit of the movement is Operation pub struct OpMetric(); impl Metric for OpMetric { - fn seek(cursor: &mut OpCursor, offset: usize) -> SeekResult { - let _ = check_bound(cursor.op_index, offset)?; - let mut seek_cursor = OpCursor::new(cursor.delta, cursor.origin_iv); - let mut cur_offset = 0; + fn seek(cursor: &mut DeltaCursor, op_offset: usize) -> SeekResult { + let _ = check_bound(cursor.op_offset, op_offset)?; + let mut seek_cursor = DeltaCursor::new(cursor.delta, cursor.origin_iv); + while let Some((_, op)) = seek_cursor.iter.next() { - cur_offset += op.len(); - if cur_offset > offset { + cursor.descend(op.len()); + if cursor.op_offset >= op_offset { break; } } - cursor.descend(cur_offset); Ok(()) } } +/// [Utf16CodeUnitMetric] is used by [DeltaIterator] for seeking operations. +/// The unit of the movement is Utf16CodeUnit pub struct Utf16CodeUnitMetric(); impl Metric for Utf16CodeUnitMetric { - fn seek(cursor: &mut OpCursor, offset: usize) -> SeekResult { + fn seek(cursor: &mut DeltaCursor, offset: usize) -> SeekResult { if offset > 0 { let _ = check_bound(cursor.consume_count, offset)?; let _ = cursor.next_with_len(Some(offset)); diff --git a/shared-lib/lib-ot/src/core/delta/delta.rs b/shared-lib/lib-ot/src/core/delta/delta.rs index 29fd424dcb..b422205a90 100644 --- a/shared-lib/lib-ot/src/core/delta/delta.rs +++ b/shared-lib/lib-ot/src/core/delta/delta.rs @@ -1,8 +1,10 @@ -use crate::{ - core::{operation::*, DeltaIter, FlowyStr, Interval, OperationTransformable, MAX_IV_LEN}, - errors::{ErrorBuilder, OTError, OTErrorCode}, -}; +use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; +use crate::core::delta::{DeltaIterator, MAX_IV_LEN}; +use crate::core::interval::Interval; +use crate::core::operation::{Attributes, Operation, OperationTransform, PhantomAttributes}; +use crate::core::ot_str::OTString; +use crate::core::DeltaBuilder; use bytes::Bytes; use serde::de::DeserializeOwned; use std::{ @@ -13,13 +15,28 @@ use std::{ str::FromStr, }; -pub type PlainTextDelta = Delta; +pub type TextDelta = Delta; +pub type TextDeltaBuilder = DeltaBuilder; -// TODO: optimize the memory usage with Arc::make_mut or Cow +/// A [Delta] contains list of operations that consists of 'Retain', 'Delete' and 'Insert' operation. +/// Check out the [Operation] for more details. It describes the document as a sequence of +/// operations. +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/delta) out for more information. +/// +/// If the [T] supports 'serde', that will enable delta to serialize to JSON or deserialize from +/// a JSON string. +/// #[derive(Clone, Debug, PartialEq, Eq)] pub struct Delta { pub ops: Vec>, + + /// 'Delete' and 'Retain' operation will update the [utf16_base_len] + /// Transforming the other delta, it requires the utf16_base_len must be equal. pub utf16_base_len: usize, + + /// Represents the current len of the delta. + /// 'Insert' and 'Retain' operation will update the [utf16_target_len] pub utf16_target_len: usize, } @@ -81,6 +98,7 @@ where } } + /// Adding an operation. It will be added in sequence. pub fn add(&mut self, op: Operation) { match op { Operation::Delete(i) => self.delete(i), @@ -89,6 +107,7 @@ where } } + /// Creating a [Delete] operation with len [n] pub fn delete(&mut self, n: usize) { if n == 0 { return; @@ -97,17 +116,18 @@ where if let Some(Operation::Delete(n_last)) = self.ops.last_mut() { *n_last += n; } else { - self.ops.push(OpBuilder::delete(n).build()); + self.ops.push(Operation::delete(n)); } } + /// Creating a [Insert] operation with string, [s]. pub fn insert(&mut self, s: &str, attributes: T) { - let s: FlowyStr = s.into(); + let s: OTString = s.into(); if s.is_empty() { return; } - self.utf16_target_len += s.utf16_size(); + self.utf16_target_len += s.utf16_len(); let new_last = match self.ops.as_mut_slice() { [.., Operation::::Insert(insert)] => { // @@ -119,10 +139,10 @@ where } [.., op_last @ Operation::::Delete(_)] => { let new_last = op_last.clone(); - *op_last = OpBuilder::::insert(&s).attributes(attributes).build(); + *op_last = Operation::::insert_with_attributes(&s, attributes); Some(new_last) } - _ => Some(OpBuilder::::insert(&s).attributes(attributes).build()), + _ => Some(Operation::::insert_with_attributes(&s, attributes)), }; match new_last { @@ -131,6 +151,7 @@ where } } + /// Creating a [Retain] operation with len, [n]. pub fn retain(&mut self, n: usize, attributes: T) { if n == 0 { return; @@ -143,24 +164,47 @@ where self.ops.push(new_op); } } else { - self.ops.push(OpBuilder::::retain(n).attributes(attributes).build()); + self.ops.push(Operation::::retain_with_attributes(n, attributes)); } } - /// Applies an operation to a string, returning a new string. - pub fn apply(&self, s: &str) -> Result { - let s: FlowyStr = s.into(); - if s.utf16_size() != self.utf16_base_len { + /// Return the a new string described by this delta. The new string will contains the input string. + /// The length of the [applied_str] must be equal to the the [utf16_base_len]. + /// + /// # Arguments + /// + /// * `applied_str`: A string represents the utf16_base_len content. it will be consumed by the [retain] + /// or [delete] operations. + /// + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::TextDeltaBuilder; + /// let s = "hello"; + /// let delta_a = TextDeltaBuilder::new().insert(s).build(); + /// let delta_b = TextDeltaBuilder::new() + /// .retain(s.len()) + /// .insert(", AppFlowy") + /// .build(); + /// + /// let after_a = delta_a.content().unwrap(); + /// let after_b = delta_b.apply(&after_a).unwrap(); + /// assert_eq!("hello, AppFlowy", &after_b); + /// ``` + pub fn apply(&self, applied_str: &str) -> Result { + let applied_str: OTString = applied_str.into(); + if applied_str.utf16_len() != self.utf16_base_len { return Err(ErrorBuilder::new(OTErrorCode::IncompatibleLength) .msg(format!( - "Expected: {}, received: {}", + "Expected: {}, but received: {}", self.utf16_base_len, - s.utf16_size() + applied_str.utf16_len() )) .build()); } let mut new_s = String::new(); - let code_point_iter = &mut s.utf16_code_unit_iter(); + let code_point_iter = &mut applied_str.utf16_iter(); for op in &self.ops { match &op { Operation::Retain(retain) => { @@ -181,34 +225,60 @@ where Ok(new_s) } - /// Computes the inverse of an operation. The inverse of an operation is the - /// operation that reverts the effects of the operation - pub fn invert_str(&self, s: &str) -> Self { + /// Computes the inverse [Delta]. The inverse of an operation is the + /// operation that reverts the effects of the operation + /// # Arguments + /// + /// * `inverted_s`: A string represents the utf16_base_len content. The len of [inverted_s] + /// must equal to the [utf16_base_len], it will be consumed by the [retain] or [delete] operations. + /// + /// If the delta's operations just contain a insert operation. The inverted_s must be empty string. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::TextDeltaBuilder; + /// let s = "hello world"; + /// let delta = TextDeltaBuilder::new().insert(s).build(); + /// let invert_delta = delta.invert_str(s); + /// assert_eq!(delta.utf16_base_len, invert_delta.utf16_target_len); + /// assert_eq!(delta.utf16_target_len, invert_delta.utf16_base_len); + /// + /// assert_eq!(invert_delta.apply(s).unwrap(), "") + /// + /// ``` + /// + pub fn invert_str(&self, inverted_s: &str) -> Self { let mut inverted = Delta::default(); - let chars = &mut s.chars(); + let inverted_s: OTString = inverted_s.into(); + let code_point_iter = &mut inverted_s.utf16_iter(); + for op in &self.ops { match &op { Operation::Retain(retain) => { inverted.retain(retain.n, T::default()); - // TODO: use advance_by instead, but it's unstable now - // chars.advance_by(retain.num) for _ in 0..retain.n { - chars.next(); + code_point_iter.next(); } } Operation::Insert(insert) => { inverted.delete(insert.utf16_size()); } Operation::Delete(delete) => { - inverted.insert(&chars.take(*delete as usize).collect::(), op.get_attributes()); + let bytes = code_point_iter + .take(*delete as usize) + .into_iter() + .flat_map(|a| str::from_utf8(a.0).ok()) + .collect::(); + + inverted.insert(&bytes, op.get_attributes()); } } } inverted } - /// Checks if this operation has no effect. - #[inline] + /// Return true if the delta doesn't contain any [Insert] or [Delete] operations. pub fn is_noop(&self) -> bool { matches!(self.ops.as_slice(), [] | [Operation::Retain(_)]) } @@ -222,7 +292,7 @@ where } } -impl OperationTransformable for Delta +impl OperationTransform for Delta where T: Attributes, { @@ -231,8 +301,8 @@ where Self: Sized, { let mut new_delta = Delta::default(); - let mut iter = DeltaIter::new(self); - let mut other_iter = DeltaIter::new(other); + let mut iter = DeltaIterator::new(self); + let mut other_iter = DeltaIterator::new(other); while iter.has_next() || other_iter.has_next() { if other_iter.is_next_insert() { @@ -252,10 +322,10 @@ where let op = iter .next_op_with_len(length) - .unwrap_or_else(|| OpBuilder::retain(length).build()); + .unwrap_or_else(|| Operation::retain(length)); let other_op = other_iter .next_op_with_len(length) - .unwrap_or_else(|| OpBuilder::retain(length).build()); + .unwrap_or_else(|| Operation::retain(length)); // debug_assert_eq!(op.len(), other_op.len(), "Composing delta failed,"); @@ -263,12 +333,12 @@ where (Operation::Retain(retain), Operation::Retain(other_retain)) => { let composed_attrs = retain.attributes.compose(&other_retain.attributes)?; - new_delta.add(OpBuilder::retain(retain.n).attributes(composed_attrs).build()) + new_delta.add(Operation::retain_with_attributes(retain.n, composed_attrs)) } (Operation::Insert(insert), Operation::Retain(other_retain)) => { let mut composed_attrs = insert.attributes.compose(&other_retain.attributes)?; composed_attrs.remove_empty(); - new_delta.add(OpBuilder::insert(op.get_data()).attributes(composed_attrs).build()) + new_delta.add(Operation::insert_with_attributes(op.get_data(), composed_attrs)) } (Operation::Retain(_), Operation::Delete(_)) => { new_delta.add(other_op); @@ -331,7 +401,7 @@ where Ordering::Less => { a_prime.retain(retain.n, composed_attrs.clone()); b_prime.retain(retain.n, composed_attrs.clone()); - next_op2 = Some(OpBuilder::retain(o_retain.n - retain.n).build()); + next_op2 = Some(Operation::retain(o_retain.n - retain.n)); next_op1 = ops1.next(); } Ordering::Equal => { @@ -343,14 +413,14 @@ where Ordering::Greater => { a_prime.retain(o_retain.n, composed_attrs.clone()); b_prime.retain(o_retain.n, composed_attrs.clone()); - next_op1 = Some(OpBuilder::retain(retain.n - o_retain.n).build()); + next_op1 = Some(Operation::retain(retain.n - o_retain.n)); next_op2 = ops2.next(); } }; } (Some(Operation::Delete(i)), Some(Operation::Delete(j))) => match i.cmp(j) { Ordering::Less => { - next_op2 = Some(OpBuilder::delete(*j - *i).build()); + next_op2 = Some(Operation::delete(*j - *i)); next_op1 = ops1.next(); } Ordering::Equal => { @@ -358,7 +428,7 @@ where next_op2 = ops2.next(); } Ordering::Greater => { - next_op1 = Some(OpBuilder::delete(*i - *j).build()); + next_op1 = Some(Operation::delete(*i - *j)); next_op2 = ops2.next(); } }, @@ -366,7 +436,7 @@ where match i.cmp(o_retain) { Ordering::Less => { a_prime.delete(*i); - next_op2 = Some(OpBuilder::retain(o_retain.n - *i).build()); + next_op2 = Some(Operation::retain(o_retain.n - *i)); next_op1 = ops1.next(); } Ordering::Equal => { @@ -376,7 +446,7 @@ where } Ordering::Greater => { a_prime.delete(o_retain.n); - next_op1 = Some(OpBuilder::delete(*i - o_retain.n).build()); + next_op1 = Some(Operation::delete(*i - o_retain.n)); next_op2 = ops2.next(); } }; @@ -385,7 +455,7 @@ where match retain.cmp(j) { Ordering::Less => { b_prime.delete(retain.n); - next_op2 = Some(OpBuilder::delete(*j - retain.n).build()); + next_op2 = Some(Operation::delete(*j - retain.n)); next_op1 = ops1.next(); } Ordering::Equal => { @@ -395,7 +465,7 @@ where } Ordering::Greater => { b_prime.delete(*j); - next_op1 = Some(OpBuilder::retain(retain.n - *j).build()); + next_op1 = Some(Operation::retain(retain.n - *j)); next_op2 = ops2.next(); } }; @@ -407,21 +477,17 @@ where fn invert(&self, other: &Self) -> Self { let mut inverted = Delta::default(); - if other.is_empty() { - return inverted; - } - let mut index = 0; for op in &self.ops { let len: usize = op.len() as usize; match op { Operation::Delete(n) => { - invert_from_other(&mut inverted, other, op, index, index + *n); + invert_other(&mut inverted, other, op, index, index + *n); index += len; } Operation::Retain(_) => { match op.has_attribute() { - true => invert_from_other(&mut inverted, other, op, index, index + len), + true => invert_other(&mut inverted, other, op, index, index + len), false => { // tracing::trace!("invert retain: {} by retain {} {}", op, len, // op.get_attributes()); @@ -452,7 +518,7 @@ where } } -fn invert_from_other( +fn invert_other( base: &mut Delta, other: &Delta, operation: &Operation, @@ -460,7 +526,7 @@ fn invert_from_other( end: usize, ) { tracing::trace!("invert op: {} [{}:{}]", operation, start, end); - let other_ops = DeltaIter::from_interval(other, Interval::new(start, end)).ops(); + let other_ops = DeltaIterator::from_interval(other, Interval::new(start, end)).ops(); other_ops.into_iter().for_each(|other_op| match operation { Operation::Delete(_n) => { // tracing::trace!("invert delete: {} by add {}", n, other_op); @@ -493,7 +559,7 @@ fn transform_op_attribute( } let left = left.as_ref().unwrap().get_attributes(); let right = right.as_ref().unwrap().get_attributes(); - // TODO: replace with anyhow and thiserror. + // TODO: replace with anyhow and this error. Ok(left.transform(&right)?.0) } @@ -501,7 +567,18 @@ impl Delta where T: Attributes + DeserializeOwned, { - pub fn from_delta_str(json: &str) -> Result { + /// # Examples + /// + /// ``` + /// use lib_ot::core::DeltaBuilder; + /// use lib_ot::rich_text::{RichTextDelta}; + /// let json = r#"[ + /// {"retain":7,"attributes":{"bold":null}} + /// ]"#; + /// let delta = RichTextDelta::from_json(json).unwrap(); + /// assert_eq!(delta.json_str(), r#"[{"retain":7,"attributes":{"bold":""}}]"#); + /// ``` + pub fn from_json(json: &str) -> Result { let delta = serde_json::from_str(json).map_err(|e| { tracing::trace!("Deserialize failed: {:?}", e); tracing::trace!("{:?}", json); @@ -510,9 +587,10 @@ where Ok(delta) } + /// Deserialize the bytes into [Delta]. It requires the bytes is in utf8 format. pub fn from_bytes>(bytes: B) -> Result { let json = str::from_utf8(bytes.as_ref())?.to_owned(); - let val = Self::from_delta_str(&json)?; + let val = Self::from_json(&json)?; Ok(val) } } @@ -521,16 +599,19 @@ impl Delta where T: Attributes + serde::Serialize, { - pub fn to_delta_str(&self) -> String { + /// Serialize the [Delta] into a String in JSON format + pub fn json_str(&self) -> String { serde_json::to_string(self).unwrap_or_else(|_| "".to_owned()) } - pub fn to_str(&self) -> Result { + /// Get the content the [Delta] represents. + pub fn content(&self) -> Result { self.apply("") } - pub fn to_delta_bytes(&self) -> Bytes { - let json = self.to_delta_str(); + /// Serial the [Delta] into a String in Bytes format + pub fn json_bytes(&self) -> Bytes { + let json = self.json_str(); Bytes::from(json.into_bytes()) } } diff --git a/shared-lib/lib-ot/src/core/delta/delta_serde.rs b/shared-lib/lib-ot/src/core/delta/delta_serde.rs index ceac31ee58..7dff063211 100644 --- a/shared-lib/lib-ot/src/core/delta/delta_serde.rs +++ b/shared-lib/lib-ot/src/core/delta/delta_serde.rs @@ -1,4 +1,5 @@ -use crate::core::{Attributes, Delta}; +use crate::core::delta::Delta; +use crate::core::operation::Attributes; use serde::{ de::{SeqAccess, Visitor}, ser::SerializeSeq, diff --git a/shared-lib/lib-ot/src/core/delta/iterator.rs b/shared-lib/lib-ot/src/core/delta/iterator.rs index ccad56845d..7997d23bae 100644 --- a/shared-lib/lib-ot/src/core/delta/iterator.rs +++ b/shared-lib/lib-ot/src/core/delta/iterator.rs @@ -1,17 +1,38 @@ use super::cursor::*; -use crate::{ - core::{Attributes, Delta, Interval, Operation, NEW_LINE}, - rich_text::RichTextAttributes, -}; +use crate::core::delta::{Delta, NEW_LINE}; +use crate::core::interval::Interval; +use crate::core::operation::{Attributes, Operation}; +use crate::rich_text::RichTextAttributes; use std::ops::{Deref, DerefMut}; pub(crate) const MAX_IV_LEN: usize = i32::MAX as usize; -pub struct DeltaIter<'a, T: Attributes> { - cursor: OpCursor<'a, T>, +/// [DeltaIterator] is used to iterate over a delta. +/// # Examples +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/delta) out for more information. +/// +/// ``` +/// use lib_ot::core::{DeltaIterator, Interval, Operation}; +/// use lib_ot::rich_text::RichTextDelta; +/// let mut delta = RichTextDelta::default(); +/// delta.add(Operation::insert("123")); +/// delta.add(Operation::insert("4")); +/// assert_eq!( +/// DeltaIterator::from_interval(&delta, Interval::new(0, 2)).ops(), +/// vec![Operation::insert("12")] +/// ); +/// +/// assert_eq!( +/// DeltaIterator::from_interval(&delta, Interval::new(1, 3)).ops(), +/// vec![Operation::insert("23")] +/// ); +/// ``` +pub struct DeltaIterator<'a, T: Attributes> { + cursor: DeltaCursor<'a, T>, } -impl<'a, T> DeltaIter<'a, T> +impl<'a, T> DeltaIterator<'a, T> where T: Attributes, { @@ -28,7 +49,7 @@ where } pub fn from_interval(delta: &'a Delta, interval: Interval) -> Self { - let cursor = OpCursor::new(delta, interval); + let cursor = DeltaCursor::new(delta, interval); Self { cursor } } @@ -46,7 +67,7 @@ where } pub fn next_op(&mut self) -> Option> { - self.cursor.next_op() + self.cursor.get_next_op() } pub fn next_op_with_len(&mut self, len: usize) -> Option> { @@ -80,28 +101,28 @@ where } pub fn is_next_insert(&self) -> bool { - match self.cursor.next_iter_op() { + match self.cursor.next_op() { None => false, Some(op) => op.is_insert(), } } pub fn is_next_retain(&self) -> bool { - match self.cursor.next_iter_op() { + match self.cursor.next_op() { None => false, Some(op) => op.is_retain(), } } pub fn is_next_delete(&self) -> bool { - match self.cursor.next_iter_op() { + match self.cursor.next_op() { None => false, Some(op) => op.is_delete(), } } } -impl<'a, T> Iterator for DeltaIter<'a, T> +impl<'a, T> Iterator for DeltaIterator<'a, T> where T: Attributes, { @@ -112,7 +133,7 @@ where } pub fn is_empty_line_at_index(delta: &Delta, index: usize) -> bool { - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); let (prev, next) = (iter.next_op_with_len(index), iter.next_op()); if prev.is_none() { return true; @@ -128,7 +149,7 @@ pub fn is_empty_line_at_index(delta: &Delta, index: usize) - } pub struct AttributesIter<'a, T: Attributes> { - delta_iter: DeltaIter<'a, T>, + delta_iter: DeltaIterator<'a, T>, } impl<'a, T> AttributesIter<'a, T> @@ -141,7 +162,7 @@ where } pub fn from_interval(delta: &'a Delta, interval: Interval) -> Self { - let delta_iter = DeltaIter::from_interval(delta, interval); + let delta_iter = DeltaIterator::from_interval(delta, interval); Self { delta_iter } } @@ -157,7 +178,7 @@ impl<'a, T> Deref for AttributesIter<'a, T> where T: Attributes, { - type Target = DeltaIter<'a, T>; + type Target = DeltaIterator<'a, T>; fn deref(&self) -> &Self::Target { &self.delta_iter diff --git a/shared-lib/lib-ot/src/core/interval.rs b/shared-lib/lib-ot/src/core/interval.rs index a6f3131b6a..cc907ec3ea 100644 --- a/shared-lib/lib-ot/src/core/interval.rs +++ b/shared-lib/lib-ot/src/core/interval.rs @@ -157,7 +157,7 @@ impl From> for Interval { #[cfg(test)] mod tests { - use crate::core::Interval; + use crate::core::interval::Interval; #[test] fn contains() { diff --git a/shared-lib/lib-ot/src/core/mod.rs b/shared-lib/lib-ot/src/core/mod.rs index b5bc594246..7c1ed3f2ef 100644 --- a/shared-lib/lib-ot/src/core/mod.rs +++ b/shared-lib/lib-ot/src/core/mod.rs @@ -1,30 +1,9 @@ mod delta; -mod flowy_str; mod interval; mod operation; +mod ot_str; -use crate::errors::OTError; pub use delta::*; -pub use flowy_str::*; pub use interval::*; pub use operation::*; - -pub trait OperationTransformable { - /// Merges the operation with `other` into one operation while preserving - /// the changes of both. - fn compose(&self, other: &Self) -> Result - where - Self: Sized; - /// Transforms two operations a and b that happened concurrently and - /// produces two operations a' and b'. - /// (a', b') = a.transform(b) - /// a.compose(b') = b.compose(a') - fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> - where - Self: Sized; - /// Inverts the operation with `other` to produces undo operation. - /// undo = a.invert(b) - /// new_b = b.compose(a) - /// b = new_b.compose(undo) - fn invert(&self, other: &Self) -> Self; -} +pub use ot_str::*; diff --git a/shared-lib/lib-ot/src/core/operation/builder.rs b/shared-lib/lib-ot/src/core/operation/builder.rs index dc7b6dd7d9..9483d4cae7 100644 --- a/shared-lib/lib-ot/src/core/operation/builder.rs +++ b/shared-lib/lib-ot/src/core/operation/builder.rs @@ -1,51 +1,51 @@ -use crate::{ - core::{Attributes, Operation, PlainTextAttributes}, - rich_text::RichTextAttributes, -}; +use crate::core::operation::{Attributes, Operation, PhantomAttributes}; +use crate::rich_text::RichTextAttributes; -pub type RichTextOpBuilder = OpBuilder; -pub type PlainTextOpBuilder = OpBuilder; +pub type RichTextOpBuilder = OperationsBuilder; +pub type PlainTextOpBuilder = OperationsBuilder; -pub struct OpBuilder { - ty: Operation, - attrs: T, +pub struct OperationsBuilder { + operations: Vec>, } -impl OpBuilder +impl OperationsBuilder where T: Attributes, { - pub fn new(ty: Operation) -> OpBuilder { - OpBuilder { - ty, - attrs: T::default(), - } + pub fn new() -> OperationsBuilder { + OperationsBuilder { operations: vec![] } } - pub fn retain(n: usize) -> OpBuilder { - OpBuilder::new(Operation::Retain(n.into())) - } - - pub fn delete(n: usize) -> OpBuilder { - OpBuilder::new(Operation::Delete(n)) - } - - pub fn insert(s: &str) -> OpBuilder { - OpBuilder::new(Operation::Insert(s.into())) - } - - pub fn attributes(mut self, attrs: T) -> OpBuilder { - self.attrs = attrs; + pub fn retain_with_attributes(mut self, n: usize, attributes: T) -> OperationsBuilder { + let retain = Operation::retain_with_attributes(n.into(), attributes); + self.operations.push(retain); self } - pub fn build(self) -> Operation { - let mut operation = self.ty; - match &mut operation { - Operation::Delete(_) => {} - Operation::Retain(retain) => retain.attributes = self.attrs, - Operation::Insert(insert) => insert.attributes = self.attrs, - } - operation + pub fn retain(mut self, n: usize) -> OperationsBuilder { + let retain = Operation::retain(n.into()); + self.operations.push(retain); + self + } + + pub fn delete(mut self, n: usize) -> OperationsBuilder { + self.operations.push(Operation::Delete(n)); + self + } + + pub fn insert_with_attributes(mut self, s: &str, attributes: T) -> OperationsBuilder { + let insert = Operation::insert_with_attributes(s.into(), attributes); + self.operations.push(insert); + self + } + + pub fn insert(mut self, s: &str) -> OperationsBuilder { + let insert = Operation::insert(s.into()); + self.operations.push(insert); + self + } + + pub fn build(self) -> Vec> { + self.operations } } diff --git a/shared-lib/lib-ot/src/core/operation/operation.rs b/shared-lib/lib-ot/src/core/operation/operation.rs index 82228c19fc..4bacd1ea7d 100644 --- a/shared-lib/lib-ot/src/core/operation/operation.rs +++ b/shared-lib/lib-ot/src/core/operation/operation.rs @@ -1,8 +1,8 @@ -use crate::{ - core::{FlowyStr, Interval, OpBuilder, OperationTransformable}, - errors::OTError, -}; +use crate::core::interval::Interval; +use crate::core::ot_str::OTString; +use crate::errors::OTError; use serde::{Deserialize, Serialize, __private::Formatter}; +use std::fmt::Display; use std::{ cmp::min, fmt, @@ -10,15 +10,91 @@ use std::{ ops::{Deref, DerefMut}, }; -pub trait Attributes: fmt::Display + Eq + PartialEq + Default + Clone + Debug + OperationTransformable { - fn is_empty(&self) -> bool; +pub trait OperationTransform { + /// Merges the operation with `other` into one operation while preserving + /// the changes of both. + /// + /// # Arguments + /// + /// * `other`: The delta gonna to merge. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OperationTransform, TextDeltaBuilder}; + /// let document = TextDeltaBuilder::new().build(); + /// let delta = TextDeltaBuilder::new().insert("abc").build(); + /// let new_document = document.compose(&delta).unwrap(); + /// assert_eq!(new_document.content().unwrap(), "abc".to_owned()); + /// ``` + fn compose(&self, other: &Self) -> Result + where + Self: Sized; - // Remove the empty attribute which value is None. - fn remove_empty(&mut self); + /// Transforms two operations a and b that happened concurrently and + /// produces two operations a' and b'. + /// (a', b') = a.transform(b) + /// a.compose(b') = b.compose(a') + /// + fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> + where + Self: Sized; - fn extend_other(&mut self, other: Self); + /// Returns the invert delta from the other. It can be used to do the undo operation. + /// + /// # Arguments + /// + /// * `other`: Generate the undo delta for [Other]. [Other] can compose the undo delta to return + /// to the previous state. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OperationTransform, TextDeltaBuilder}; + /// let original_document = TextDeltaBuilder::new().build(); + /// let delta = TextDeltaBuilder::new().insert("abc").build(); + /// + /// let undo_delta = delta.invert(&original_document); + /// let new_document = original_document.compose(&delta).unwrap(); + /// let document = new_document.compose(&undo_delta).unwrap(); + /// + /// assert_eq!(original_document, document); + /// + /// ``` + fn invert(&self, other: &Self) -> Self; } +/// Each operation can carry attributes. For example, the [RichTextAttributes] has a list of key/value attributes. +/// Such as { bold: true, italic: true }. +/// +///Because [Operation] is generic over the T, so you must specify the T. For example, the [TextDelta] uses +///[PhantomAttributes] as the T. [PhantomAttributes] does nothing, just a phantom. +/// +pub trait Attributes: Default + Display + Eq + PartialEq + Clone + Debug + OperationTransform { + fn is_empty(&self) -> bool { + true + } + + /// Remove the empty attribute which value is None. + fn remove_empty(&mut self) { + // Do nothing + } + + fn extend_other(&mut self, _other: Self) { + // Do nothing + } +} + +/// [Operation] consists of three types. +/// * Delete +/// * Retain +/// * Insert +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/delta) out for more information. +/// +/// The [T] should support serde if you want to serialize/deserialize the operation +/// to json string. You could check out the operation_serde.rs for more information. +/// #[derive(Debug, Clone, Eq, PartialEq)] pub enum Operation { Delete(usize), @@ -30,6 +106,40 @@ impl Operation where T: Attributes, { + pub fn delete(n: usize) -> Self { + Self::Delete(n) + } + + /// Create a [Retain] operation with the given attributes + pub fn retain_with_attributes(n: usize, attributes: T) -> Self { + Self::Retain(Retain { n, attributes }) + } + + /// Create a [Retain] operation without attributes + pub fn retain(n: usize) -> Self { + Self::Retain(Retain { + n, + attributes: T::default(), + }) + } + + /// Create a [Insert] operation with the given attributes + pub fn insert_with_attributes(s: &str, attributes: T) -> Self { + Self::Insert(Insert { + s: OTString::from(s), + attributes, + }) + } + + /// Create a [Insert] operation without attributes + pub fn insert(s: &str) -> Self { + Self::Insert(Insert { + s: OTString::from(s), + attributes: T::default(), + }) + } + + /// Return the String if the operation is [Insert] operation, otherwise return the empty string. pub fn get_data(&self) -> &str { match self { Operation::Delete(_) => "", @@ -77,43 +187,58 @@ where let right; match self { Operation::Delete(n) => { - left = Some(OpBuilder::::delete(index).build()); - right = Some(OpBuilder::::delete(*n - index).build()); + left = Some(Operation::::delete(index)); + right = Some(Operation::::delete(*n - index)); } Operation::Retain(retain) => { - left = Some(OpBuilder::::delete(index).build()); - right = Some(OpBuilder::::delete(retain.n - index).build()); + left = Some(Operation::::delete(index)); + right = Some(Operation::::delete(retain.n - index)); } Operation::Insert(insert) => { let attributes = self.get_attributes(); - left = Some( - OpBuilder::::insert(&insert.s[0..index]) - .attributes(attributes.clone()) - .build(), - ); - right = Some( - OpBuilder::::insert(&insert.s[index..insert.utf16_size()]) - .attributes(attributes) - .build(), - ); + left = Some(Operation::::insert_with_attributes( + &insert.s[0..index], + attributes.clone(), + )); + right = Some(Operation::::insert_with_attributes( + &insert.s[index..insert.utf16_size()], + attributes, + )); } } (left, right) } + /// Returns an operation with the specified width. + /// # Arguments + /// + /// * `interval`: Specify the shrink width of the operation. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{Interval, Operation, PhantomAttributes}; + /// let operation = Operation::::insert("1234"); + /// + /// let op1 = operation.shrink(Interval::new(0,3)).unwrap(); + /// assert_eq!(op1 , Operation::insert("123")); + /// + /// let op2= operation.shrink(Interval::new(3,4)).unwrap(); + /// assert_eq!(op2, Operation::insert("4")); + /// ``` pub fn shrink(&self, interval: Interval) -> Option> { let op = match self { - Operation::Delete(n) => OpBuilder::delete(min(*n, interval.size())).build(), - Operation::Retain(retain) => OpBuilder::retain(min(retain.n, interval.size())) - .attributes(retain.attributes.clone()) - .build(), + Operation::Delete(n) => Operation::delete(min(*n, interval.size())), + Operation::Retain(retain) => { + Operation::retain_with_attributes(min(retain.n, interval.size()), retain.attributes.clone()) + } Operation::Insert(insert) => { if interval.start > insert.utf16_size() { - OpBuilder::insert("").build() + Operation::insert("") } else { let s = insert.s.sub_str(interval).unwrap_or_else(|| "".to_owned()); - OpBuilder::insert(&s).attributes(insert.attributes.clone()).build() + Operation::insert_with_attributes(&s, insert.attributes.clone()) } } }; @@ -178,9 +303,7 @@ where #[derive(Clone, Debug, Eq, PartialEq)] pub struct Retain { - // #[serde(rename(serialize = "retain", deserialize = "retain"))] pub n: usize, - // #[serde(skip_serializing_if = "is_empty")] pub attributes: T, } @@ -212,7 +335,7 @@ where self.n += n; None } else { - Some(OpBuilder::retain(n).attributes(attributes).build()) + Some(Operation::retain_with_attributes(n, attributes)) } } @@ -255,10 +378,7 @@ where #[derive(Clone, Debug, Eq, PartialEq)] pub struct Insert { - // #[serde(rename(serialize = "insert", deserialize = "insert"))] - pub s: FlowyStr, - - // #[serde(skip_serializing_if = "is_empty")] + pub s: OTString, pub attributes: T, } @@ -288,7 +408,7 @@ where T: Attributes, { pub fn utf16_size(&self) -> usize { - self.s.utf16_size() + self.s.utf16_len() } pub fn merge_or_new_op(&mut self, s: &str, attributes: T) -> Option> { @@ -296,7 +416,7 @@ where self.s += s; None } else { - Some(OpBuilder::::insert(s).attributes(attributes).build()) + Some(Operation::::insert_with_attributes(s, attributes)) } } @@ -326,11 +446,11 @@ where } } -impl std::convert::From for Insert +impl std::convert::From for Insert where T: Attributes, { - fn from(s: FlowyStr) -> Self { + fn from(s: OTString) -> Self { Insert { s, attributes: T::default(), @@ -339,24 +459,16 @@ where } #[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)] -pub struct PlainTextAttributes(); -impl fmt::Display for PlainTextAttributes { +pub struct PhantomAttributes(); +impl fmt::Display for PhantomAttributes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("PlainAttributes") + f.write_str("PhantomAttributes") } } -impl Attributes for PlainTextAttributes { - fn is_empty(&self) -> bool { - true - } +impl Attributes for PhantomAttributes {} - fn remove_empty(&mut self) {} - - fn extend_other(&mut self, _other: Self) {} -} - -impl OperationTransformable for PlainTextAttributes { +impl OperationTransform for PhantomAttributes { fn compose(&self, _other: &Self) -> Result { Ok(self.clone()) } diff --git a/shared-lib/lib-ot/src/core/operation/operation_serde.rs b/shared-lib/lib-ot/src/core/operation/operation_serde.rs index aefb909d0a..b7ff7b3c20 100644 --- a/shared-lib/lib-ot/src/core/operation/operation_serde.rs +++ b/shared-lib/lib-ot/src/core/operation/operation_serde.rs @@ -1,4 +1,5 @@ -use crate::core::{Attributes, FlowyStr, Insert, Operation, Retain}; +use crate::core::operation::{Attributes, Insert, Operation, Retain}; +use crate::core::ot_str::OTString; use serde::{ de, de::{MapAccess, SeqAccess, Visitor}, @@ -248,7 +249,7 @@ where where A: SeqAccess<'de>, { - let s = match serde::de::SeqAccess::next_element::(&mut seq)? { + let s = match serde::de::SeqAccess::next_element::(&mut seq)? { Some(val) => val, None => { return Err(de::Error::invalid_length(0, &"struct Insert with 2 elements")); @@ -270,7 +271,7 @@ where where V: MapAccess<'de>, { - let mut s: Option = None; + let mut s: Option = None; let mut attributes: Option = None; while let Some(key) = map.next_key()? { match key { diff --git a/shared-lib/lib-ot/src/core/flowy_str.rs b/shared-lib/lib-ot/src/core/ot_str.rs similarity index 58% rename from shared-lib/lib-ot/src/core/flowy_str.rs rename to shared-lib/lib-ot/src/core/ot_str.rs index 95cc1735cc..7f84de6d20 100644 --- a/shared-lib/lib-ot/src/core/flowy_str.rs +++ b/shared-lib/lib-ot/src/core/ot_str.rs @@ -1,19 +1,51 @@ use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; use std::{fmt, fmt::Formatter}; +/// [OTString] uses [String] as its inner container. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct FlowyStr(pub String); +pub struct OTString(pub String); -impl FlowyStr { - // https://stackoverflow.com/questions/2241348/what-is-unicode-utf-8-utf-16 - pub fn utf16_size(&self) -> usize { +impl OTString { + /// Returns the number of UTF-16 code units in this string. + /// + /// The length of strings behaves differently in different languages. For example: [Dart] string's + /// length is calculated with UTF-16 code units. The method [utf16_len] returns the length of a + /// String in UTF-16 code units. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::OTString; + /// let utf16_len = OTString::from("👋").utf16_len(); + /// assert_eq!(utf16_len, 2); + /// let bytes_len = String::from("👋").len(); + /// assert_eq!(bytes_len, 4); + /// + /// ``` + pub fn utf16_len(&self) -> usize { count_utf16_code_units(&self.0) } - pub fn utf16_code_unit_iter(&self) -> Utf16CodeUnitIterator { + pub fn utf16_iter(&self) -> Utf16CodeUnitIterator { Utf16CodeUnitIterator::new(self) } + /// Returns a new string with the given [Interval] + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OTString, Interval}; + /// let s: OTString = "你好\n😁".into(); + /// assert_eq!(s.utf16_len(), 5); + /// let output1 = s.sub_str(Interval::new(0, 2)).unwrap(); + /// assert_eq!(output1, "你好"); + /// + /// let output2 = s.sub_str(Interval::new(2, 3)).unwrap(); + /// assert_eq!(output2, "\n"); + /// + /// let output3 = s.sub_str(Interval::new(3, 5)).unwrap(); + /// assert_eq!(output3, "😁"); + /// ``` pub fn sub_str(&self, interval: Interval) -> Option { let mut iter = Utf16CodeUnitIterator::new(self); let mut buf = vec![]; @@ -33,13 +65,33 @@ impl FlowyStr { } } + /// Return a new string with the given [Interval] + /// # Examples + /// + /// ``` + /// use lib_ot::core::OTString; + /// let s: OTString = "👋😁👋".into(); /// + /// let mut iter = s.utf16_code_point_iter(); + /// assert_eq!(iter.next().unwrap(), "👋".to_string()); + /// assert_eq!(iter.next().unwrap(), "😁".to_string()); + /// assert_eq!(iter.next().unwrap(), "👋".to_string()); + /// assert_eq!(iter.next(), None); + /// + /// let s: OTString = "👋12ab一二👋".into(); /// + /// let mut iter = s.utf16_code_point_iter(); + /// assert_eq!(iter.next().unwrap(), "👋".to_string()); + /// assert_eq!(iter.next().unwrap(), "1".to_string()); + /// assert_eq!(iter.next().unwrap(), "2".to_string()); + /// + /// assert_eq!(iter.skip(OTString::from("ab一二").utf16_len()).next().unwrap(), "👋".to_string()); + /// ``` #[allow(dead_code)] - fn utf16_code_point_iter(&self) -> FlowyUtf16CodePointIterator { - FlowyUtf16CodePointIterator::new(self, 0) + pub fn utf16_code_point_iter(&self) -> OTUtf16CodePointIterator { + OTUtf16CodePointIterator::new(self, 0) } } -impl std::ops::Deref for FlowyStr { +impl std::ops::Deref for OTString { type Target = String; fn deref(&self) -> &Self::Target { @@ -47,46 +99,46 @@ impl std::ops::Deref for FlowyStr { } } -impl std::ops::DerefMut for FlowyStr { +impl std::ops::DerefMut for OTString { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl std::convert::From for FlowyStr { +impl std::convert::From for OTString { fn from(s: String) -> Self { - FlowyStr(s) + OTString(s) } } -impl std::convert::From<&str> for FlowyStr { +impl std::convert::From<&str> for OTString { fn from(s: &str) -> Self { s.to_owned().into() } } -impl std::fmt::Display for FlowyStr { +impl std::fmt::Display for OTString { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } -impl std::ops::Add<&str> for FlowyStr { - type Output = FlowyStr; +impl std::ops::Add<&str> for OTString { + type Output = OTString; - fn add(self, rhs: &str) -> FlowyStr { + fn add(self, rhs: &str) -> OTString { let new_value = self.0 + rhs; new_value.into() } } -impl std::ops::AddAssign<&str> for FlowyStr { +impl std::ops::AddAssign<&str> for OTString { fn add_assign(&mut self, rhs: &str) { self.0 += rhs; } } -impl Serialize for FlowyStr { +impl Serialize for OTString { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -95,15 +147,15 @@ impl Serialize for FlowyStr { } } -impl<'de> Deserialize<'de> for FlowyStr { - fn deserialize(deserializer: D) -> Result +impl<'de> Deserialize<'de> for OTString { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - struct FlowyStrVisitor; + struct OTStringVisitor; - impl<'de> Visitor<'de> for FlowyStrVisitor { - type Value = FlowyStr; + impl<'de> Visitor<'de> for OTStringVisitor { + type Value = OTString; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a str") @@ -116,19 +168,19 @@ impl<'de> Deserialize<'de> for FlowyStr { Ok(s.into()) } } - deserializer.deserialize_str(FlowyStrVisitor) + deserializer.deserialize_str(OTStringVisitor) } } pub struct Utf16CodeUnitIterator<'a> { - s: &'a FlowyStr, + s: &'a OTString, byte_offset: usize, utf16_offset: usize, utf16_count: usize, } impl<'a> Utf16CodeUnitIterator<'a> { - pub fn new(s: &'a FlowyStr) -> Self { + pub fn new(s: &'a OTString) -> Self { Utf16CodeUnitIterator { s, byte_offset: 0, @@ -166,21 +218,21 @@ impl<'a> Iterator for Utf16CodeUnitIterator<'a> { } } -pub struct FlowyUtf16CodePointIterator<'a> { - s: &'a FlowyStr, +pub struct OTUtf16CodePointIterator<'a> { + s: &'a OTString, offset: usize, } -impl<'a> FlowyUtf16CodePointIterator<'a> { - pub fn new(s: &'a FlowyStr, offset: usize) -> Self { - FlowyUtf16CodePointIterator { s, offset } +impl<'a> OTUtf16CodePointIterator<'a> { + pub fn new(s: &'a OTString, offset: usize) -> Self { + OTUtf16CodePointIterator { s, offset } } } -use crate::core::Interval; +use crate::core::interval::Interval; use std::str; -impl<'a> Iterator for FlowyUtf16CodePointIterator<'a> { +impl<'a> Iterator for OTUtf16CodePointIterator<'a> { type Item = String; fn next(&mut self) -> Option { @@ -226,14 +278,15 @@ pub fn len_utf8_from_first_byte(b: u8) -> usize { #[cfg(test)] mod tests { - use crate::core::{FlowyStr, Interval}; + use crate::core::interval::Interval; + use crate::core::ot_str::OTString; #[test] fn flowy_str_code_unit() { - let size = FlowyStr::from("👋").utf16_size(); + let size = OTString::from("👋").utf16_len(); assert_eq!(size, 2); - let s: FlowyStr = "👋 \n👋".into(); + let s: OTString = "👋 \n👋".into(); let output = s.sub_str(Interval::new(0, size)).unwrap(); assert_eq!(output, "👋"); @@ -247,24 +300,10 @@ mod tests { assert_eq!(output, "👋"); } - #[test] - fn flowy_str_sub_str_in_chinese() { - let s: FlowyStr = "你好\n😁".into(); - let size = s.utf16_size(); - assert_eq!(size, 5); - - let output1 = s.sub_str(Interval::new(0, 2)).unwrap(); - let output2 = s.sub_str(Interval::new(2, 3)).unwrap(); - let output3 = s.sub_str(Interval::new(3, 5)).unwrap(); - assert_eq!(output1, "你好"); - assert_eq!(output2, "\n"); - assert_eq!(output3, "😁"); - } - #[test] fn flowy_str_sub_str_in_chinese2() { - let s: FlowyStr = "😁 \n".into(); - let size = s.utf16_size(); + let s: OTString = "😁 \n".into(); + let size = s.utf16_len(); assert_eq!(size, 4); let output1 = s.sub_str(Interval::new(0, 3)).unwrap(); @@ -275,27 +314,17 @@ mod tests { #[test] fn flowy_str_sub_str_in_english() { - let s: FlowyStr = "ab".into(); - let size = s.utf16_size(); + let s: OTString = "ab".into(); + let size = s.utf16_len(); assert_eq!(size, 2); let output = s.sub_str(Interval::new(0, 2)).unwrap(); assert_eq!(output, "ab"); } - #[test] - fn flowy_str_utf16_code_point_iter_test1() { - let s: FlowyStr = "👋😁👋".into(); - let mut iter = s.utf16_code_point_iter(); - assert_eq!(iter.next().unwrap(), "👋".to_string()); - assert_eq!(iter.next().unwrap(), "😁".to_string()); - assert_eq!(iter.next().unwrap(), "👋".to_string()); - assert_eq!(iter.next(), None); - } - #[test] fn flowy_str_utf16_code_point_iter_test2() { - let s: FlowyStr = "👋😁👋".into(); + let s: OTString = "👋😁👋".into(); let iter = s.utf16_code_point_iter(); let result = iter.skip(1).take(1).collect::(); assert_eq!(result, "😁".to_string()); diff --git a/shared-lib/lib-ot/src/lib.rs b/shared-lib/lib-ot/src/lib.rs index 5a3be0ede7..df314d667e 100644 --- a/shared-lib/lib-ot/src/lib.rs +++ b/shared-lib/lib-ot/src/lib.rs @@ -1,3 +1,4 @@ +pub mod codec; pub mod core; pub mod errors; pub mod rich_text; diff --git a/shared-lib/lib-ot/src/rich_text/attributes.rs b/shared-lib/lib-ot/src/rich_text/attributes.rs index d70e23228b..54826c49a0 100644 --- a/shared-lib/lib-ot/src/rich_text/attributes.rs +++ b/shared-lib/lib-ot/src/rich_text/attributes.rs @@ -1,10 +1,6 @@ #![allow(non_snake_case)] -use crate::{ - block_attribute, - core::{Attributes, Operation, OperationTransformable}, - errors::OTError, - ignore_attribute, inline_attribute, list_attribute, -}; +use crate::core::{Attributes, Operation, OperationTransform}; +use crate::{block_attribute, errors::OTError, ignore_attribute, inline_attribute, list_attribute}; use lazy_static::lazy_static; use std::{ collections::{HashMap, HashSet}, @@ -126,7 +122,7 @@ impl Attributes for RichTextAttributes { } } -impl OperationTransformable for RichTextAttributes { +impl OperationTransform for RichTextAttributes { fn compose(&self, other: &Self) -> Result where Self: Sized, diff --git a/shared-lib/lib-ot/src/rich_text/delta.rs b/shared-lib/lib-ot/src/rich_text/delta.rs index 99cb35f2bc..3a60cf7b02 100644 --- a/shared-lib/lib-ot/src/rich_text/delta.rs +++ b/shared-lib/lib-ot/src/rich_text/delta.rs @@ -1,7 +1,5 @@ -use crate::{ - core::{Delta, DeltaBuilder}, - rich_text::RichTextAttributes, -}; +use crate::core::{Delta, DeltaBuilder}; +use crate::rich_text::RichTextAttributes; pub type RichTextDelta = Delta; pub type RichTextDeltaBuilder = DeltaBuilder;