Compare commits

...
24 Commits
Author SHA1 Message Date
Lucas.Xu 2c513be305 chore: bump version 0.2.5 (#2924) 2023-07-02 15:15:57 +08:00
Lucas.Xu 11d05b303d feat: support inserting local image (#2913) 2023-07-02 11:46:45 +08:00
Richard Shiue c870dbeac4 test: add more cover integration tests (#2922) 2023-07-01 23:13:09 +08:00
Richard ShiueandLucas.Xu 3acd36e580 test: fix calendar integration test #2919 (#2920)
* test: fix calendar integration test #2919

* chore: update frontend/appflowy_flutter/integration_test/database_calendar_test.dart

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>

* test: fix date cell test

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-07-01 20:29:49 +08:00
Aman Negi 95d4fb6865 feat: add outline block (#2750) 2023-06-29 20:28:30 +08:00
Muhammad Rizwan 30b52a29fd feat: support Inline page reference #2196 (#2898) 2023-06-29 10:04:24 +08:00
Richard Shiue a17d6b7eec test: fix integration test (#2906)
* test: fix calendar integration test

* test: add comment for workaround
2023-06-28 22:11:50 +08:00
Kilu.He 18ed553296 feat: support equation block (#2903) 2023-06-28 12:39:04 +08:00
Richard Shiue 7f74fd6149 refactor: cover node widget code (#2899)
* chore: change initial cover type's name to none

* chore: refactor cover node widget

* chore: use a constant instead of magic value

* fix: make the size of icon hover effect smaller

* chore: improve appearance of selected color

* test: add cover integration tests

* fix: inner ring of selected color in dark mode

* refactor: cover node to document header node

* test: simplify tests

* chore: rename files
2023-06-27 15:17:51 +08:00
LainHE a3e09f6021 chore: update language zh-CN (#2900) 2023-06-27 11:11:09 +08:00
Kilu.He 5e0a0f92ff feat: support text equation (#2876) 2023-06-27 10:44:06 +08:00
Alex Wallen 2a2b5fe246 [feat] Add actions to error widget. (#2865)
* chore: rebase error?

* fix: revert changes in Cargo.toml

* feat: add actions row
2023-06-27 09:55:10 +08:00
rmri f914e7aa33 chore: Update hu-HU.json (#2837)
Fixed typos and added missing items in Hungarian translation.
2023-06-27 09:54:46 +08:00
Alex Wallen 386d3f2ba7 feat github redirect (#2867)
* feat: add GitHub to question bubble

* feat: translate to feedback
2023-06-27 09:53:38 +08:00
Nathan.fooo 88faa0f02a feat: using cached view (#2877) 2023-06-23 23:19:34 +08:00
Lucas.Xu f3d6c66f5c chore: bump version 0.2.4 (#2890) 2023-06-23 15:36:44 +08:00
Lucas.Xu d665153a9f fix: 0.2.3 known issues (#2886)
* fix: option menu position

* fix: rename helps highlight the text for the user

* fix: export as markdown file name invalid

* chore: align the emoji in callout

* fix: leave more space in the editor

* fix: 0.2.3 known issues

* chore: add flutter pub get in flutter.toml
2023-06-23 10:54:35 +08:00
Askarbek Zadaulyandqinluhe eee32110f4 feat: nested views
* chore: remove folder code merge page and folder into navitem component

* chore: test fix

* fix: nav item expand fix

* fix: unfold page and active page

* fix: nav item click area fix

* chore: remove old components

* chore: remove old code

* chore: cell controller reorganize

* chore: nav item optimizations

* fix: add async queue to fix data problem

* chore: change semantics of new folder button

* chore: move row methods to database controller

---------

Co-authored-by: qinluhe <qinluhe.twodog@gmail.com>
2023-06-23 10:17:50 +08:00
Richard Shiue 9834eccc7b fix: remove icon button not working in popover (#2885) 2023-06-23 10:01:06 +08:00
Richard Shiue bef7fe87aa test: add more calendar integration tests (#2883)
* test: make sure that the row details page opens

* test: add more calendar tests
2023-06-22 23:40:49 +08:00
Lucas.Xu feceb430cf chore: optimize CI (#2875) 2023-06-22 21:22:46 +08:00
Nathan.foooandvedon a29c8ab27a fix: cell focus issue on windows (#2880)
* fix: cell focus issue on windows

* fix: try to fix

* fix: cell focus not working on Windows platform there are multiple text cells

* docs: add documentation

* chore: adjust row detail page ui

* test: add test

* test: fix test

---------

Co-authored-by: vedon <vedon.fu@gmail.com>
2023-06-22 20:16:31 +08:00
Nathan.foooandvedon f1bfcb6066 feat: only allow one windows instance (#2879)
Co-authored-by: vedon <vedon.fu@gmail.com>
2023-06-22 13:25:01 +08:00
Richard Shiue 99cfda0a5c test: more integration tests (#2871)
* test: date cell integration tests

* test: single/multi-select integration tests
2023-06-21 22:53:49 +08:00
207 changed files with 6703 additions and 2602 deletions

No files matched your search

+4 -3
View File
@@ -21,12 +21,13 @@ on:
- unlocked
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-app:
if: github.event.pull_request.draft != true
concurrency:
group: docker_ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- name: Checkout source code
+5 -1
View File
@@ -25,8 +25,13 @@ env:
FLUTTER_VERSION: "3.10.1"
RUST_TOOLCHAIN: "1.70"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
@@ -102,7 +107,6 @@ jobs:
git config --system core.longpaths true
flutter config --enable-windows-desktop
fi
dart pub global activate protoc_plugin 20.0.1
shell: bash
- name: Build AppFlowy
+10 -7
View File
@@ -6,7 +6,7 @@ on:
- "main"
- "release/*"
paths:
- ".github/workflows/flutter_ci.yaml"
- ".github/workflows/integration_test.yml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
@@ -15,7 +15,7 @@ on:
- "main"
- "release/*"
paths:
- ".github/workflows/flutter_ci.yaml"
- ".github/workflows/integration_test.yml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
@@ -24,8 +24,13 @@ env:
FLUTTER_VERSION: "3.10.1"
RUST_TOOLCHAIN: "1.70"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tests:
if: github.event.pull_request.draft != true
strategy:
matrix:
os: [ubuntu-latest]
@@ -43,7 +48,6 @@ jobs:
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
target: ${{ matrix.target }}
override: true
profile: minimal
@@ -92,7 +96,6 @@ jobs:
elif [ "$RUNNER_OS" == "Windows" ]; then
flutter config --enable-windows-desktop
fi
dart pub global activate protoc_plugin 20.0.1
shell: bash
- name: Build Test lib
@@ -114,11 +117,11 @@ jobs:
export DISPLAY=:99
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
flutter test integration_test/runner.dart -d Linux --coverage --verbose
flutter test integration_test/runner.dart -d Linux --coverage
elif [ "$RUNNER_OS" == "macOS" ]; then
flutter test integration_test/runner.dart -d macOS --coverage --verbose
flutter test integration_test/runner.dart -d macOS --coverage
elif [ "$RUNNER_OS" == "Windows" ]; then
flutter test integration_test/runner.dart -d Windows --coverage --verbose
flutter test integration_test/runner.dart -d Windows --coverage
fi
shell: bash
+7 -1
View File
@@ -2,6 +2,7 @@ name: Tauri-CI
on:
pull_request:
paths:
- ".github/workflows/tauri_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_tauri/**"
@@ -10,8 +11,13 @@ env:
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.70"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tauri-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
@@ -38,7 +44,7 @@ jobs:
with:
prefix-key: 'ubuntu-latest-tauri'
workspaces: |
frontend/rust-lib
frontend/rust-lib
frontend/appflowy_tauri/src-tauri
- name: install dependencies (windows only)
+35 -1
View File
@@ -1,27 +1,51 @@
# Release Notes
## Version 0.2.4 - 06/23/2023
### Bug fixes:
- Unable to copy and paste a word
- Some UI issues
## Version 0.2.4 - 06/23/2023
### Bug fixes:
- Unable to copy and paste a word
- Some UI issues
## Version 0.2.3 - 06/21/2023
### New Features
- Added support for creating multiple database views for existing database
- Added support for creating multiple database views for existing database
## Version 0.2.2 - 06/15/2023
### New Features
- Added support for embedding a document in the database's row detail page
- Added support for inserting an emoji in the database's row detail page
### Other Updates
- Added language selector on the welcome page
- Added support for importing multiple markdown files all at once
## Version 0.2.1 - 06/11/2023
### New Features
- Added support for creating or referencing a calendar in the document
- Added `+` icon in grid's add field
### Other Updates
- Added vertical padding for progress bar
- Hide url cell accessory when the content is empty
### Bug fixes:
- Fixed unable to export markdown
- Fixed adding vertical padding for progress bar
- Fixed database view didn't update after the database layout changed.
@@ -29,6 +53,7 @@
## Version 0.2.0 - 06/08/2023
### New Features
- Improved checklists to support each cell having its own list
- Drag and drop calendar events
- Switch layouts (calendar, grid, kanban) of a database
@@ -42,44 +67,53 @@
- Added support for an 'Option' button to delete, duplicate, and customize block actions
### Other Updates
- Added support for importing v0.1.x documents and databases
- Added support for database import and export to CSV
- Optimized scroll behavior in documents.
- Redesigned the launch page
### Bug fixes
- Fixed bugs related to numbers
- Fixed issues with referenced databases in documents
- Fixed menu overflow issues in documents
### Data migration
The data format of this version is not compatible with previous versions. Therefore, to migrate your data to the new version, you need to use the export and import functions. Please follow the guide to learn how to export and import your data.
#### Export files in v0.1.6
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/0c89bf2b-cd97-4a7b-b627-59df8d2967d9
#### Import files in v0.2.0
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/7b392f35-4972-497a-8a7f-f38efced32e2
## Version 0.1.5 - 11/05/2023
### Bug Fixes
- Fix: calendar dates don't match with weekdays.
- Fix: sort numbers in Grid.
## Version 0.1.4 - 04/05/2023
### New features
- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.
- Writing can be improved with the help of OpenAI.
## Version 0.1.3 - 24/04/2023
### New features
- Launch the official Dark Mode.
- Customize the font color and highlight color by setting a hex color value and an opacity level.
### Bug Fixes
- Fix: the slash menu can be triggered by all other keyboards than English.
- Fix: convert the single asterisk to italic text and the double asterisks to bold text.
+1 -1
View File
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.2.3"
CURRENT_APP_VERSION = "0.2.5"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@@ -90,7 +90,8 @@
"name": "Debug Info",
"success": "Copied debug info to clipboard!",
"fail": "Unable to copy debug info to clipboard"
}
},
"feedback": "Feedback"
},
"menuAppHeader": {
"addPageTooltip": "Quickly add a page inside",
@@ -384,6 +385,9 @@
"createANewCalendar": "Create a new Calendar"
}
},
"selectionMenu": {
"outline": "Outline"
},
"plugins": {
"referencedBoard": "Referenced Board",
"referencedGrid": "Referenced Grid",
@@ -448,6 +452,12 @@
"center": "Center",
"right": "Right",
"defaultColor": "Default"
},
"image": {
"copiedToPasteBoard": "The image link has been copied to the clipboard"
},
"outline": {
"addHeadingToCreateOutline": "Add headings to create a table of contents."
}
}
},
@@ -120,14 +120,14 @@
"oAuth": {
"err": {
"failedTitle": "Sikertelen bejelentkezés.",
"failedMsg": "Kérjük győződj meg róla, hogy elvégezted a bejelentkezési folyamatot a böngésződben"
"failedMsg": "Kérjük győződj meg róla, hogy elvégezted a bejelentkezési folyamatot a böngésződben!"
},
"google": {
"title": "Bejelentkezés Google-al",
"title": "Bejelentkezés Google-lal",
"instruction1": "Ahhoz, hogy hozzáférj a Google Kontaktjaidhoz, kérjük hatalmazd fel ezt az alkalmazást a böngésződben.",
"instruction2": "Másold ezt a kódot a vágólapra az ikonra kattintással vagy a szöveg kijelölésével:",
"instruction3": "Nyisd meg ezt a linket a böngésződben, és írjd be a fenti kódot:",
"instruction4": "Nyomd meg az alábbi gombot, ha elvégezted a registrációt:"
"instruction3": "Nyisd meg ezt a linket a böngésződben, és írd be a fenti kódot:",
"instruction4": "Nyomd meg az alábbi gombot, ha elvégezted a regisztrációt:"
}
},
"settings": {
@@ -139,15 +139,15 @@
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"label": "Téma",
"light": "Világos mód",
"dark": "Éjjeli mód",
"system": "Adapt to System"
"system": "Rendszerbeállítás követése"
}
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
"openSidebar": "Oldalsáv megnyitása",
"closeSidebar": "Oldalsáv bezárása"
}
}
}
@@ -6,6 +6,8 @@
"subscribeNewsletterText": "消息订阅",
"letsGoButtonText": "开始",
"title": "标题",
"youCanAlso": "您还可以",
"and": "以及",
"signUp": {
"buttonText": "注册",
"title": "注册 @:appName 账户",
@@ -21,6 +23,7 @@
"signIn": {
"loginTitle": "登录 @:appName",
"loginButtonText": "登录",
"loginAsGuestButtonText": "开始使用",
"buttonText": "登录",
"forgotPassword": "忘记密码?",
"emailHint": "邮箱",
@@ -40,6 +43,21 @@
"markdown": "Markdown",
"copyLink": "复制链接"
},
"moreAction": {
"small": "小",
"medium": "中",
"large": "大",
"fontSize": "字号",
"import": "导入",
"moreOptions": "更多选项"
},
"importPanel": {
"textAndMarkdown": "文本 & Markdown",
"documentFromV010": "来自 v0.1.0 的文档",
"databaseFromV010": "来自 v0.1.0 的数据库",
"csv": "CSV",
"database": "数据库"
},
"disclosureAction": {
"rename": "重命名",
"delete": "删除",
@@ -54,7 +72,7 @@
"pageHeader": {
"fileName": "文件名",
"lastModified": "最近修改",
"created": "创建"
"created": "创建时间"
}
},
"deletePagePrompt": {
@@ -64,8 +82,10 @@
},
"dialogCreatePageNameHint": "页面名称",
"questionBubble": {
"shortcuts": "快捷键",
"whatsNew": "新功能?",
"help": "帮助 & 支持",
"markdown": "Markdown",
"debug": {
"name": "调试信息",
"success": "将调试信息复制到剪贴板!",
@@ -90,14 +110,18 @@
"inlineCode": "内联代码",
"quote": "块引用",
"header": "标题",
"highlight": "高亮"
"highlight": "高亮",
"color": "颜色"
},
"tooltip": {
"lightMode": "切换到亮色模式",
"darkMode": "切换到暗色模式",
"openAsPage": "作为页面打开",
"addNewRow": "增加一行",
"openMenu": "点击打开菜单"
"addNewRow": "添加一行",
"openMenu": "点击打开菜单",
"dragRow": "拖动来调整行",
"viewDataBase": "查看数据库",
"referencePage": "这个 {name} 正在被引用"
},
"sideBar": {
"openSidebar": "打开侧边栏",
@@ -117,11 +141,19 @@
},
"button": {
"OK": "确认",
"Done": "完成",
"Cancel": "取消",
"signIn": "登录",
"signOut": "登出",
"complete": "完成",
"save": "保存"
"save": "保存",
"generate": "生成",
"esc": "ESC",
"keep": "保留",
"tryAgain": "重试",
"discard": "放弃",
"replace": "替换",
"insertBelow": "下方插入"
},
"label": {
"welcome": "欢迎!",
@@ -149,37 +181,133 @@
"appearance": "外观",
"language": "语言",
"user": "用户",
"files": "文件",
"open": "打开设置"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"label": "主题模式",
"light": "日间模式",
"dark": "夜间模式",
"system": "Adapt to System"
}
"system": "系统自适应"
},
"theme": "主题"
},
"files": {
"copy": "复制",
"defaultLocation": "读取文件和数据存储位置",
"exportData": "导出您的数据",
"doubleTapToCopy": "双击复制路径",
"restoreLocation": "恢复为 AppFlowy 默认路径",
"customizeLocation": "打开另一个文件夹",
"restartApp": "请重启 app 使设置生效",
"exportDatabase": "导出数据库",
"selectFiles": "选择需要导出的文件",
"selectAll": "全选",
"deselectAll": "取消全选",
"createNewFolder": "新建文件夹",
"createNewFolderDesc": "告诉我们您要将数据存储在何处",
"defineWhereYourDataIsStored": "定义数据存储位置",
"open": "打开",
"openFolder": "打开现有文件夹",
"openFolderDesc": "读取并将其写入您现有的 AppFlowy 文件夹",
"folderHintText": "文件夹名",
"location": "正在新建文件夹",
"locationDesc": "为您的 AppFlowy 数据文件夹选择一个名称",
"browser": "浏览",
"create": "新建",
"set": "设置",
"folderPath": "保存文件夹的路径",
"locationCannotBeEmpty": "路径不能为空",
"pathCopiedSnackbar": "文件存储路径已被复制到剪贴板!",
"changeLocationTooltips": "更改数据目录",
"change": "更改",
"openLocationTooltips": "打开另一个数据目录",
"openCurrentDataFolder": "打开当前数据目录",
"recoverLocationTooltips": "恢复为 AppFlowy 默认数据目录",
"exportFileSuccess": "导出文件成功!",
"exportFileFail": "导出文件失败!",
"export": "导出"
},
"user": {
"name": "名字",
"icon": "图标",
"selectAnIcon": "选择一个图标",
"pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥"
}
},
"grid": {
"deleteView": "您确定要删除这个视窗吗?",
"createView": "新建",
"settings": {
"filter": "过滤器",
"filter": "筛选",
"sort": "以……排序",
"sortBy": "排序",
"Properties": "属性",
"group": "组"
"group": "组",
"addFilter": "添加筛选",
"deleteFilter": "删除筛选",
"filterBy": "以……筛选",
"typeAValue": "请输入一个值",
"layout": "布局",
"databaseLayout": "布局"
},
"textFilter": {
"contains": "包含",
"doesNotContain": "不包含",
"endsWith": "以……结束",
"startWith": "以……开始",
"is": "等于",
"isNot": "不等于",
"isEmpty": "为空",
"isNotEmpty": "不为空",
"choicechipPrefix": {
"isNot": "不等于",
"startWith": "以……开始",
"endWith": "以……结束",
"isEmpty": "为空",
"isNotEmpty": "不为空"
}
},
"checkboxFilter": {
"isChecked": "已勾选",
"isUnchecked": "未勾选",
"choicechipPrefix": {
"is": "is"
}
},
"checklistFilter": {
"isComplete": "已完成",
"isIncomplted": "未完成"
},
"singleSelectOptionFilter": {
"is": "等于",
"isNot": "不等于",
"isEmpty": "为空",
"isNotEmpty": "不为空"
},
"multiSelectOptionFilter": {
"contains": "包含",
"doesNotContain": "不包含",
"isEmpty": "为空",
"isNotEmpty": "不为空"
},
"field": {
"hide": "隐藏",
"insertLeft": "左侧插入",
"insertRight": "右侧插入",
"duplicate": "拷贝",
"duplicate": "复制",
"delete": "删除",
"textFieldName": "文本",
"checkboxFieldName": "勾选框",
"dateFieldName": "日期",
"updatedAtFieldName": "修改时间",
"createdAtFieldName": "创建时间",
"numberFieldName": "数字",
"singleSelectFieldName": "单项选择器",
"multiSelectFieldName": "多项选择器",
"urlFieldName": "链接",
"checklistFieldName": "清单",
"numberFormat": "数字格式",
"dateFormat": "日期格式",
"includeTime": "包含时间",
@@ -187,6 +315,7 @@
"dateFormatISO": "年-月-日",
"dateFormatLocal": "月/日/年",
"dateFormatUS": "年/月/日",
"dateFormatDayMonthYear": "日/月/年",
"timeFormat": "时间格式",
"invalidTimeFormat": "时间格式错误",
"timeFormatTwelveHour": "12小时制",
@@ -195,16 +324,23 @@
"optionTitle": "标签",
"addOption": "添加标签",
"editProperty": "编辑列属性",
"newProperty": "增加一列",
"newProperty": "添加一列",
"deleteFieldPromptMessage": "确定要删除这个属性吗? "
},
"sort": {
"ascending": "升序",
"descending": "降序",
"deleteSort": "删除排序",
"addSort": "添加排序"
},
"row": {
"duplicate": "复制",
"delete": "删除",
"textPlaceholder": "空",
"copyProperty": "复制列",
"count": "数量",
"newRow": "添加一行"
"newRow": "添加一行",
"action": "执行"
},
"selectOption": {
"create": "新建",
@@ -222,19 +358,130 @@
"panelTitle": "选择或新建一个标签",
"searchOption": "搜索标签"
},
"menuName": "网格"
"checklist": {
"panelTitle": "添加项"
},
"menuName": "网格",
"referencedGridPrefix": "视图"
},
"document": {
"menuName": "文档",
"date": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
},
"slashMenu": {
"board": {
"selectABoardToLinkTo": "选择要链接到的看板",
"createANewBoard": "新建看板"
},
"grid": {
"selectAGridToLinkTo": "选择要链接到的网格",
"createANewGrid": "新建网格"
},
"calendar": {
"selectACalendarToLinkTo": "选择要链接到的日历",
"createANewCalendar": "新建日历"
}
},
"plugins": {
"referencedBoard": "引用的看板",
"referencedGrid": "引用的网格",
"referencedCalendar": "引用的日历",
"autoGeneratorMenuItemName": "OpenAI 创作",
"autoGeneratorTitleName": "OpenAI: 让 AI 写些什么...",
"autoGeneratorLearnMore": "学习更多",
"autoGeneratorGenerate": "生成",
"autoGeneratorHintText": "让 OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "无法获得 OpenAI 密钥",
"autoGeneratorRewrite": "重写",
"smartEdit": "AI 助手",
"openAI": "OpenAI",
"smartEditFixSpelling": "修正拼写",
"warning": "⚠️ AI 可能不准确或具有误导性.",
"smartEditSummarize": "总结",
"smartEditImproveWriting": "提高写作水平",
"smartEditMakeLonger": "丰富内容",
"smartEditCouldNotFetchResult": "无法从 OpenAI 获取到结果",
"smartEditCouldNotFetchKey": "无法获取到 OpenAI 密钥",
"smartEditDisabled": "在设置中连接 OpenAI",
"discardResponse": "您是否要放弃 AI 继续写作?",
"cover": {
"changeCover": "修改封面",
"colors": "颜色",
"images": "图像",
"clearAll": "清除所有",
"abstract": "摘要",
"addCover": "添加封面",
"addLocalImage": "添加本地图像",
"invalidImageUrl": "无效的图像网址",
"failedToAddImageToGallery": "无法将图片添加到库",
"enterImageUrl": "输入图像网址",
"add": "添加",
"back": "返回",
"saveToGallery": "保存至库",
"removeIcon": "移除图标",
"pasteImageUrl": "粘贴图片网址",
"or": "或",
"pickFromFiles": "从文件中选取",
"couldNotFetchImage": "无法获取到图像",
"imageSavingFailed": "图像保存失败",
"addIcon": "添加图标",
"coverRemoveAlert": "删除后将从封面中移除。",
"alertDialogConfirmation": "您确定您要继续吗?"
},
"mathEquation": {
"addMathEquation": "添加数学公式",
"editMathEquation": "编辑数学公式"
},
"optionAction": {
"click": "点击",
"toOpenMenu": " 来打开菜单",
"delete": "删除",
"duplicate": "复制",
"turnInto": "变成",
"moveUp": "上移",
"moveDown": "下移",
"color": "颜色",
"align": "对齐",
"left": "左",
"center": "中心",
"right": "又",
"defaultColor": "默认"
}
}
},
"board": {
"column": {
"create_new_card": "新建"
},
"menuName": "看板"
"menuName": "看板",
"referencedBoardPrefix": "视图"
},
"calendar": {
"menuName": "日历",
"defaultNewCalendarTitle": "未命名",
"navigation": {
"today": "今天",
"jumpToday": "跳转到今天",
"previousMonth": "上一月",
"nextMonth": "下一月"
},
"settings": {
"showWeekNumbers": "显示周数",
"showWeekends": "显示周末",
"firstDayOfWeek": "一周开始于",
"layoutDateField": "以……为日历布局",
"noDateTitle": "没有日期",
"noDateHint": "计划外事件将显示在此处",
"clickToAdd": "单击以添加到日历",
"name": "日历布局"
},
"referencedCalendarPrefix": "视图"
},
"errorDialog": {
"title": "AppFlowy 错误",
"howToFixFallback": "对于给您带来的不便, 我们深表歉意! 请在我们的 GitHub 页面上提交 issue 并描述您遇到的错误。",
"github": "在 GitHub 查看"
}
}
}
@@ -74,5 +74,191 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('create new calendar event using new button', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Scroll until today's date cell is visible
await tester.scrollToToday();
// Hover over today's calendar cell
await tester.hoverOnTodayCalendarCell();
// Tap on create new event button
await tester.tapAddCalendarEventButton();
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(1);
// Create another event on the same day
await tester.hoverOnTodayCalendarCell();
await tester.tapAddCalendarEventButton();
await tester.dismissRowDetailPage();
tester.assertNumberOfEventsInCalendar(2);
tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
});
testWidgets('create new calendar event by double-clicking', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Scroll until today's date cell is visible
await tester.scrollToToday();
// Double click on today's calendar cell to create a new event
await tester.doubleClickCalendarCell(DateTime.now());
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(1);
// Create another event on the same day
await tester.doubleClickCalendarCell(DateTime.now());
await tester.dismissRowDetailPage();
tester.assertNumberOfEventsInCalendar(2);
tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
});
testWidgets('duplicate/delete a calendar event', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(DateTime.now());
// Duplicate the event
await tester.tapRowDetailPageDuplicateRowButton();
await tester.dismissRowDetailPage();
// Check that there are 2 events
tester.assertNumberOfEventsInCalendar(2);
// Delete an event
await tester.openCalendarEvent(index: 0);
await tester.tapRowDetailPageDeleteRowButton();
// Check that there is 1 event
tester.assertNumberOfEventsInCalendar(1);
});
testWidgets('edit the title of a calendar date event', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(DateTime.now());
await tester.dismissRowDetailPage();
// Click on the event
await tester.openCalendarEvent(index: 0);
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Change the title of the event
await tester.editTitleInRowDetailPage('hello world');
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
});
testWidgets(
'edit the date of the date field being used to layout the calendar',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
final today = DateTime.now();
await tester.scrollToToday();
await tester.doubleClickCalendarCell(today);
await tester.dismissRowDetailPage();
// Make sure that the event is today
tester.assertNumberofEventsOnSpecificDay(1, today);
// Click on the event
await tester.openCalendarEvent(index: 0);
// Open the date editor of the event
await tester.tapDateCellInRowDetailPage();
await tester.findDateEditor(findsOneWidget);
// Edit the event's date. To avoid selecting a day outside of the current month, the new date will be one day closer to the middle of the month.
final newDate = today.day < 15
? today.add(const Duration(days: 1))
: today.subtract(const Duration(days: 1));
await tester.selectDay(content: newDate.day);
await tester.dismissCellEditor();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberofEventsOnSpecificDay(1, newDate);
});
testWidgets('reschedule an event by drag-and-drop', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event on the first of this month
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month, 1);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissRowDetailPage();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
// Make sure that the event has been rescheduled to the new date
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberofEventsOnSpecificDay(
1,
firstOfThisMonth.add(const Duration(days: 7)),
);
});
});
}
@@ -1,6 +1,8 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
@@ -46,6 +48,44 @@ void main() {
await tester.pumpAndSettle();
});
// Makesure the text cells are filled with the right content when there are
// multiple text cell
testWidgets('edit multiple text cells', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(ViewLayoutPB.Grid, 'my grid');
await tester.createField(FieldType.RichText, 'description');
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'hello',
);
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'world',
cellIndex: 1,
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'hello',
cellIndex: 0,
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'world',
cellIndex: 1,
);
await tester.pumpAndSettle();
});
testWidgets('edit number cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -176,8 +216,72 @@ void main() {
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Select the date
await tester.selectDay(content: 3);
// Toggle include time
await tester.toggleIncludeTime();
// Dismiss the cell editor
await tester.dismissCellEditor();
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Turn off include time
await tester.toggleIncludeTime();
// Select a date
final today = DateTime.now();
await tester.selectDay(content: today.day);
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM dd, y').format(today),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Toggle include time
final now = DateTime.now();
await tester.toggleIncludeTime();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM dd, y HH:mm').format(now),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Change date format
await tester.changeDateFormat();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('dd/MM/y HH:mm').format(now),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Change time format
await tester.changeTimeFormat();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('dd/MM/y hh:mm a').format(now),
);
await tester.pumpAndSettle();
});
@@ -195,13 +299,151 @@ void main() {
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
await tester.createOption(name: 'hello world');
await tester.dismissSelectOptionEditor();
// Create a new select option
await tester.createOption(name: 'tag 1');
await tester.dismissCellEditor();
// Make sure the option is created and displayed in the cell
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'hello world',
name: 'tag 1',
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create another select option
await tester.createOption(name: 'tag 2');
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 2',
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsOneWidget,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// switch to first option
await tester.selectOption(name: 'tag 1');
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 1',
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsOneWidget,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Deselect the currently-selected option
await tester.selectOption(name: 'tag 1');
await tester.dismissCellEditor();
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNothing,
);
await tester.pumpAndSettle();
});
testWidgets('edit multi select cell', (tester) async {
final tags = [
'tag 1',
'tag 2',
'tag 3',
'tag 4',
];
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
const fieldType = FieldType.MultiSelect;
await tester.createField(fieldType, fieldType.name);
// Tap the cell to invoke the selection option editor
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create a new select option
await tester.createOption(name: tags.first);
await tester.dismissCellEditor();
// Make sure the option is created and displayed in the cell
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags.first,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create some other select options
await tester.createOption(name: tags[1]);
await tester.createOption(name: tags[2]);
await tester.createOption(name: tags[3]);
await tester.dismissCellEditor();
for (final tag in tags) {
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tag,
);
}
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNWidgets(4),
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Deselect all options
for (final tag in tags) {
await tester.selectOption(name: tag);
}
await tester.dismissCellEditor();
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNothing,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Select some options
await tester.selectOption(name: tags[1]);
await tester.selectOption(name: tags[3]);
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags[1],
);
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags[3],
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNWidgets(2),
);
await tester.pumpAndSettle();
@@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/emoji.dart';
import 'util/ime.dart';
import 'util/util.dart';
@@ -28,6 +29,21 @@ void main() {
await TestFolder.cleanTestLocation(null);
});
testWidgets('row details page opens', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Make sure that the row page is opened
tester.assertRowDetailPageOpened();
});
testWidgets('insert emoji in the row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -1,6 +1,8 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/emoji.dart';
import '../util/util.dart';
void main() {
@@ -22,15 +24,112 @@ void main() {
await TestFolder.cleanTestLocation(null);
});
testWidgets(
'hovering on cover image will display change and delete cover image buttons',
(tester) async {
testWidgets('document cover tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.hoverOnCoverPluginAddButton();
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document cover
await tester.editor.tapOnAddCover();
tester.expectToSeeDocumentCover(CoverType.asset);
// Hover over the cover to show the 'Change Cover' and delete buttons
await tester.editor.hoverOnCover();
tester.expectChangeCoverAndDeleteButton();
// Change cover to a solid color background
await tester.editor.tapOnChangeCover();
await tester.editor.switchSolidColorBackground();
await tester.editor.dismissCoverPicker();
tester.expectToSeeDocumentCover(CoverType.color);
// Change cover to a network image
const imageUrl =
"https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy/main/frontend/appflowy_flutter/assets/images/appflowy_launch_splash.jpg";
await tester.editor.hoverOnCover();
await tester.editor.tapOnChangeCover();
await tester.editor.addNetworkImageCover(imageUrl);
await tester.editor.switchNetworkImageCover(imageUrl);
await tester.editor.dismissCoverPicker();
tester.expectToSeeDocumentCover(CoverType.file);
// Remove the cover
await tester.editor.hoverOnCover();
await tester.editor.tapOnRemoveCover();
tester.expectToSeeNoDocumentCover();
});
testWidgets('document icon tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
// Remove the document icon from the cover toolbar
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapRemoveIconButton();
tester.expectToSeeDocumentIcon(null);
// Add the icon back for further testing
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
// Change the document icon
await tester.editor.tapOnIconWidget();
await tester.switchToEmojiList();
await tester.tapEmoji('😅');
tester.expectToSeeDocumentIcon('😅');
// Remove the document icon from the icon picker
await tester.editor.tapOnIconWidget();
await tester.editor.tapRemoveIconButton(isInPicker: true);
tester.expectToSeeDocumentIcon(null);
});
testWidgets('icon and cover at the same time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// Insert a document cover
await tester.editor.tapOnAddCover();
// Expect to see the icon and cover at the same time
tester.expectToSeeDocumentIcon('😀');
tester.expectToSeeDocumentCover(CoverType.asset);
// Hover over the cover toolbar and see that neither icons are shown
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeeEmptyDocumentHeaderToolbar();
});
});
}
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
@@ -56,21 +57,21 @@ void main() {
);
});
// testWidgets('insert a referenced calendar', (tester) async {
// await tester.initializeAppFlowy();
// await tester.tapGoButton();
testWidgets('insert a referenced calendar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
// // validate the referenced grid is inserted
// expect(
// find.descendant(
// of: find.byType(AppFlowyEditor),
// matching: find.byType(CalendarPage),
// ),
// findsOneWidget,
// );
// });
// validate the referenced grid is inserted
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(CalendarPage),
),
findsOneWidget,
);
});
});
}
@@ -93,7 +94,7 @@ Future<void> insertReferenceDatabase(
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a referenced grid
// insert a referenced view
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
layout.referencedMenuName,
@@ -0,0 +1,129 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('inline page view in document', () {
const location = 'inline_page';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('insert a inline page - grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Grid);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - board', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Board);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - calendar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Calendar);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - document', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Document);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page and rename it', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document);
// rename
await tester.hoverOnPageName(pageName);
const newName = 'RenameToNewPageName';
await tester.renamePage(newName);
final finder = find.descendant(
of: find.byType(MentionPageBlock),
matching: find.findTextInFlowyText(newName),
);
expect(finder, findsOneWidget);
});
testWidgets('insert a inline page and delete it', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid);
// rename
await tester.hoverOnPageName(pageName);
await tester.tapDeletePageButton();
final finder = find.descendant(
of: find.byType(MentionPageBlock),
matching: find.findTextInFlowyText(pageName),
);
expect(finder, findsOneWidget);
await tester.tapButton(finder);
expect(find.byType(FlowyErrorPage), findsOneWidget);
});
});
}
/// Insert a referenced database of [layout] into the document
Future<String> insertingInlinePage(
WidgetTester tester,
ViewLayoutPB layout,
) async {
// create a new grid
final id = uuid();
final name = '${layout.name}_$id';
await tester.createNewPageWithName(
layout,
name,
);
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'insert_a_inline_page_${layout.name}',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
await tester.editor.showAtMenu();
await tester.editor.tapAtMenuItemWithName(name);
return name;
}
@@ -118,7 +118,7 @@ const _sample = r'''
---
[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
[] Type / followed by /bullet or /num to create a list.
[] Type followed by bullet or num to create a list.
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
@@ -0,0 +1,114 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/ime.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('outline block test', () {
const location = 'outline_test';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('insert an outline block', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'outline_test',
);
await tester.editor.tapLineOfEditorAt(0);
await insertOutlineInDocument(tester);
// validate the outline is inserted
expect(find.byType(OutlineBlockWidget), findsOneWidget);
});
testWidgets('insert an outline block and check if headings are visible',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'outline_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('# Heading 1\n');
await tester.ime.insertText('## Heading 2\n');
await tester.ime.insertText('### Heading 3\n');
/* Results in:
* # Heading 1
* ## Heading 2
* ### Heading 3
*/
await tester.editor.tapLineOfEditorAt(3);
await insertOutlineInDocument(tester);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 1'),
),
findsOneWidget,
);
// Heading 2 is prefixed with a bullet
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 2'),
),
findsOneWidget,
);
// Heading 3 is prefixed with a dash
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 3'),
),
findsOneWidget,
);
// update the Heading 1 to Heading 1Hello world
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('Hello world');
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 1Hello world'),
),
findsOneWidget,
);
});
});
}
/// Inserts an outline block in the document
Future<void> insertOutlineInDocument(WidgetTester tester) async {
// open the actions menu and insert the outline block
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_selectionMenu_outline.tr(),
);
await tester.pumpAndSettle();
}
@@ -127,6 +127,29 @@ extension AppFlowyTestBase on WidgetTester {
return;
}
Future<void> doubleTapButton(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
bool warnIfMissed = true,
int milliseconds = 500,
}) async {
await tapButton(
finder,
pointer: pointer,
buttons: buttons,
warnIfMissed: warnIfMissed,
milliseconds: kDoubleTapMinTime.inMilliseconds,
);
await tapButton(
finder,
pointer: pointer,
buttons: buttons,
warnIfMissed: warnIfMissed,
milliseconds: milliseconds,
);
}
Future<void> wait(int milliseconds) async {
await pumpAndSettle(Duration(milliseconds: milliseconds));
return;
@@ -2,6 +2,8 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
@@ -20,6 +22,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/cr
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
@@ -35,8 +38,10 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text_input.dart';
@@ -106,22 +111,26 @@ extension AppFlowyDatabaseTest on WidgetTester {
required int rowIndex,
required FieldType fieldType,
required String input,
int cellIndex = 0,
}) async {
final cell = cellFinder(rowIndex, fieldType);
final cell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);
expect(cell, findsOneWidget);
await enterText(cell, input);
await pumpAndSettle();
}
Finder cellFinder(int rowIndex, FieldType fieldType) {
///
Finder cellFinder(int rowIndex, FieldType fieldType, {int cellIndex = 0}) {
final findRow = find.byType(GridRow, skipOffstage: false);
final findCell = finderForFieldType(fieldType);
return find.descendant(
of: findRow.at(rowIndex),
matching: findCell,
skipOffstage: false,
);
return find
.descendant(
of: findRow.at(rowIndex),
matching: findCell,
skipOffstage: false,
)
.at(cellIndex);
}
Future<void> tapCheckboxCellInGrid({
@@ -166,13 +175,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(cell, warnIfMissed: false);
}
/// The [fieldName] must be uqniue in the grid.
/// The [fieldName] must be unique in the grid.
Future<void> assertCellContent({
required int rowIndex,
required FieldType fieldType,
required String content,
int cellIndex = 0,
}) async {
final findCell = cellFinder(rowIndex, fieldType);
final findCell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);
final findContent = find.descendant(
of: findCell,
matching: find.text(content),
@@ -286,9 +296,61 @@ extension AppFlowyDatabaseTest on WidgetTester {
matching: findDay,
);
// if the day is very near the beginning or the end of the month,
// it may overlap with the same day in the next or previous month,
// respectively because it was spilling over. This will lead to 2
// widgets being found and thus cannot be tapped correctly.
if (content < 15) {
// e.g., Jan 2 instead of Feb 2
await tapButton(finder.first);
} else {
// e.g. Jun 28 instead of May 28
await tapButton(finder.last);
}
}
Future<void> toggleIncludeTime() async {
final findDateEditor = find.byType(DateCellEditor);
final findToggle = find.byType(Toggle);
final finder = find.descendant(
of: findDateEditor,
matching: findToggle,
);
await tapButton(finder);
}
Future<void> changeDateFormat() async {
final findDateEditor = find.byType(DateCellEditor);
final findDateTimeOptionButton = find.byType(DateTypeOptionButton);
final finder = find.descendant(
of: findDateEditor,
matching: findDateTimeOptionButton,
);
await tapButton(finder);
final findDateFormatButton = find.byType(DateFormatButton);
await tapButton(findDateFormatButton);
final findNewDateFormat = find.text("Day/Month/Year");
await tapButton(findNewDateFormat);
}
Future<void> changeTimeFormat() async {
final findDateEditor = find.byType(DateCellEditor);
final findDateTimeOptionButton = find.byType(DateTypeOptionButton);
final finder = find.descendant(
of: findDateEditor,
matching: findDateTimeOptionButton,
);
await tapButton(finder);
final findDateFormatButton = find.byType(TimeFormatButton);
await tapButton(findDateFormatButton);
final findNewDateFormat = find.text("12 hour");
await tapButton(findNewDateFormat);
}
Future<void> tapSelectOptionCellInGrid({
required int rowIndex,
required FieldType fieldType,
@@ -325,6 +387,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
Future<void> selectOption({
required String name,
}) async {
final option = find.byWidgetPredicate(
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
);
await tapButton(option);
}
Future<void> findSelectOptionWithNameInGrid({
required int rowIndex,
required String name,
@@ -342,6 +414,24 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(cell, findsOneWidget);
}
Future<void> assertNumberOfSelectedOptionsInGrid({
required int rowIndex,
required Matcher matcher,
}) async {
final findRow = find.byType(GridRow);
final options = find.byWidgetPredicate(
(widget) => widget is SelectOptionTag,
);
final cell = find.descendant(
of: findRow.at(rowIndex),
matching: options,
);
expect(cell, matcher);
}
Future<void> openFirstRowDetailPage() async {
await hoverOnFirstRowOfGrid();
@@ -350,6 +440,22 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(expandButton);
}
void assertRowDetailPageOpened() async {
final findRowDetailPage = find.byType(RowDetailPage);
expect(findRowDetailPage, findsOneWidget);
}
Future<void> dismissRowDetailPage() async {
await sendKeyEvent(LogicalKeyboardKey.escape);
await pumpAndSettle();
}
Future<void> editTitleInRowDetailPage(String title) async {
final titleField = find.byType(GridTextCell);
await enterText(titleField, title);
await pumpAndSettle();
}
Future<void> hoverRowBanner() async {
final banner = find.byType(RowBanner);
expect(banner, findsOneWidget);
@@ -367,15 +473,19 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(find.byType(EmojiSelectionMenu));
}
/// Must call [openEmojiPicker] first
Future<void> switchToEmojiList() async {
final icon = find.byIcon(Icons.tag_faces);
await tapButton(icon);
Future<void> tapDateCellInRowDetailPage() async {
final findDateCell = find.byType(GridDateCell);
await tapButton(findDateCell);
}
Future<void> tapEmoji(String emoji) async {
final emojiWidget = find.text(emoji);
await tapButton(emojiWidget);
Future<void> duplicateRowInRowDetailPage() async {
final duplicateButton = find.byType(RowDetailPageDuplicateButton);
await tapButton(duplicateButton);
}
Future<void> deleteRowInRowDetailPage() async {
final deleteButton = find.byType(RowDetailPageDeleteButton);
await tapButton(deleteButton);
}
Future<void> scrollGridByOffset(Offset offset) async {
@@ -560,7 +670,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(finder, matcher);
}
Future<void> dismissSelectOptionEditor() async {
Future<void> dismissCellEditor() async {
await sendKeyEvent(LogicalKeyboardKey.escape);
await pumpAndSettle();
}
@@ -866,6 +976,105 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(finder, findsOneWidget);
}
Future<void> scrollToToday() async {
final todayCell = find.byWidgetPredicate(
(widget) => widget is CalendarDayCard && widget.isToday,
);
final scrollable = find
.descendant(
of: find.byType(MonthView<CalendarDayEvent>),
matching: find.byWidgetPredicate(
(widget) => widget is Scrollable && widget.axis == Axis.vertical,
),
)
.first;
await scrollUntilVisible(
todayCell,
300,
scrollable: scrollable,
);
await pumpAndSettle(const Duration(milliseconds: 300));
}
Future<void> hoverOnTodayCalendarCell() async {
final todayCell = find.byWidgetPredicate(
(widget) => widget is CalendarDayCard && widget.isToday,
);
await hoverOnWidget(todayCell);
}
Future<void> tapAddCalendarEventButton() async {
final findFlowyButton = find.byType(FlowyIconButton);
final findNewEventButton = find.byType(NewEventButton);
final button = find.descendant(
of: findNewEventButton,
matching: findFlowyButton,
);
await tapButton(button);
}
/// Checks for a certain number of events. Parameters [date] and [title] can
/// also be provided to restrict the scope of the search
void assertNumberOfEventsInCalendar(int number, {String? title}) {
Finder findEvents = find.byType(EventCard);
if (title != null) {
findEvents = find.descendant(of: findEvents, matching: find.text(title));
}
expect(findEvents, findsNWidgets(number));
}
void assertNumberofEventsOnSpecificDay(
int number,
DateTime date, {
String? title,
}) {
final findDayCell = find.byWidgetPredicate(
(widget) =>
widget is CalendarDayCard &&
isSameDay(
widget.date,
date,
),
);
Finder findEvents = find.descendant(
of: findDayCell,
matching: find.byType(EventCard),
);
if (title != null) {
findEvents = find.descendant(of: findEvents, matching: find.text(title));
}
expect(findEvents, findsNWidgets(number));
}
Future<void> doubleClickCalendarCell(DateTime date) async {
final todayCell = find.byWidgetPredicate(
(widget) => widget is CalendarDayCard && isSameDay(date, widget.date),
);
await doubleTapButton(todayCell);
}
Future<void> openCalendarEvent({required index, DateTime? date}) async {
final findDayCell = find.byWidgetPredicate(
(widget) =>
widget is CalendarDayCard &&
isSameDay(widget.date, date ?? DateTime.now()),
);
final cards = find.descendant(
of: findDayCell,
matching: find.byType(EventCard),
);
await tapButton(cards.at(index));
}
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
final findEventCard = find.byType(EventCard);
await drag(findEventCard.first, const Offset(0, 300));
await pumpAndSettle();
}
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
final findAddButton = find.byType(AddDatabaseViewButton);
await tapButton(findAddButton);
@@ -1,4 +1,15 @@
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'ime.dart';
@@ -21,19 +32,109 @@ class EditorOperations {
/// Tap the line of editor at [index]
Future<void> tapLineOfEditorAt(int index) async {
final textBlocks = find.byType(TextBlockComponentWidget);
final textBlocks = find.byType(FlowyRichText);
index = index.clamp(0, textBlocks.evaluate().length - 1);
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
await tester.pumpAndSettle();
}
/// Hover on cover plugin button above the document
Future<void> hoverOnCoverPluginAddButton() async {
final editor = find.byWidgetPredicate(
(widget) => widget is AppFlowyEditor,
Future<void> hoverOnCoverToolbar() async {
final coverToolbar = find.byType(DocumentHeaderToolbar);
await tester.startGesture(
tester.getBottomLeft(coverToolbar).translate(5, -5),
kind: PointerDeviceKind.mouse,
);
await tester.hoverOnWidget(
editor,
offset: tester.getTopLeft(editor).translate(20, 20),
await tester.pumpAndSettle();
}
/// Taps on the 'Add Icon' button in the cover toolbar
Future<void> tapAddIconButton() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(find.byType(EmojiPopover), findsOneWidget);
}
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
Future<void> tapRemoveIconButton({bool isInPicker = false}) async {
Finder button =
find.text(LocaleKeys.document_plugins_cover_removeIcon.tr());
if (isInPicker) {
button = find.descendant(of: find.byType(EmojiPopover), matching: button);
}
await tester.tapButton(button);
}
/// Requires that the document must already have an icon. This opens the icon
/// picker
Future<void> tapOnIconWidget() async {
final iconWidget = find.byType(EmojiIconWidget);
await tester.tapButton(iconWidget);
}
Future<void> tapOnAddCover() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
}
Future<void> tapOnChangeCover() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_changeCover.tr(),
);
}
Future<void> switchSolidColorBackground() async {
final findPurpleButton = find.byWidgetPredicate(
(widget) => widget is ColorItem && widget.option.colorHex == "ffe8e0ff",
);
await tester.tapButton(findPurpleButton);
}
Future<void> addNetworkImageCover(String imageUrl) async {
final findNewImageButton = find.byType(NewCustomCoverButton);
await tester.tapButton(findNewImageButton);
final imageUrlTextField = find.descendant(
of: find.byType(NetworkImageUrlInput),
matching: find.byType(TextField),
);
await tester.enterText(imageUrlTextField, imageUrl);
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_add.tr(),
);
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_saveToGallery.tr(),
);
}
Future<void> switchNetworkImageCover(String imageUrl) async {
final image = find.byWidgetPredicate(
(widget) => widget is ImageGridItem,
);
await tester.tapButton(image);
}
Future<void> tapOnRemoveCover() async {
await tester.tapButton(find.byType(DeleteCoverButton));
}
/// A cover must be present in the document to function properly since this
/// catches all cover types collectively
Future<void> hoverOnCover() async {
final cover = find.byType(DocumentCover);
await tester.startGesture(
tester.getCenter(cover),
kind: PointerDeviceKind.mouse,
);
await tester.pumpAndSettle();
}
Future<void> dismissCoverPicker() async {
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
}
/// trigger the slash command (selection menu)
@@ -41,6 +142,11 @@ class EditorOperations {
await tester.ime.insertCharacter('/');
}
/// trigger the slash command (selection menu)
Future<void> showAtMenu() async {
await tester.ime.insertCharacter('@');
}
/// Tap the slash menu item with [name]
///
/// Must call [showSlashMenu] first.
@@ -48,4 +154,15 @@ class EditorOperations {
final slashMenuItem = find.text(name, findRichText: true);
await tester.tapButton(slashMenuItem);
}
/// Tap the at menu item with [name]
///
/// Must call [showAtMenu] first.
Future<void> tapAtMenuItemWithName(String name) async {
final atMenuItem = find.descendant(
of: find.byType(SelectionMenuWidget),
matching: find.text(name, findRichText: true),
);
await tester.tapButton(atMenuItem);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
extension EmojiTestExtension on WidgetTester {
/// Must call [openEmojiPicker] first
Future<void> switchToEmojiList() async {
final icon = find.byIcon(Icons.tag_faces);
await tapButton(icon);
}
Future<void> tapEmoji(String emoji) async {
final emojiWidget = find.text(emoji);
await tapButton(emojiWidget);
}
}
@@ -1,5 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -47,7 +49,7 @@ extension Expectation on WidgetTester {
expect(exportSuccess, findsOneWidget);
}
/// Expect to see the add button and icon button inside the document.
/// Expect to see the add button and icon button in the cover toolbar
void expectToSeePluginAddCoverAndIconButton() {
final addCover = find.textContaining(
LocaleKeys.document_plugins_cover_addCover.tr(),
@@ -59,6 +61,51 @@ extension Expectation on WidgetTester {
expect(addIcon, findsOneWidget);
}
/// Expect to see the document header toolbar empty
void expectToSeeEmptyDocumentHeaderToolbar() {
final addCover = find.textContaining(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
final addIcon = find.textContaining(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(addCover, findsNothing);
expect(addIcon, findsNothing);
}
void expectToSeeDocumentIcon(String? emoji) {
if (emoji == null) {
final iconWidget = find.byType(EmojiIconWidget);
expect(iconWidget, findsNothing);
return;
}
final iconWidget = find.byWidgetPredicate(
(widget) => widget is EmojiIconWidget && widget.emoji == emoji,
);
expect(iconWidget, findsOneWidget);
}
void expectToSeeDocumentCover(CoverType type) {
final findCover = find.byWidgetPredicate(
(widget) => widget is DocumentCover && widget.coverType == type,
);
expect(findCover, findsOneWidget);
}
void expectToSeeNoDocumentCover() {
final findCover = find.byType(DocumentCover);
expect(findCover, findsNothing);
}
void expectChangeCoverAndDeleteButton() {
final findChangeCover = find.text(
LocaleKeys.document_plugins_cover_changeCover.tr(),
);
final findRemoveIcon = find.byType(DeleteCoverButton);
expect(findChangeCover, findsOneWidget);
expect(findRemoveIcon, findsOneWidget);
}
/// Expect to see the user name on the home page
void expectToSeeUserName(String name) {
final userName = find.byWidgetPredicate(
@@ -9,11 +9,11 @@ extension IME on WidgetTester {
class IMESimulator {
IMESimulator(this.tester) {
client = findDeltaTextInputClient();
client = findTextInputClient();
}
final WidgetTester tester;
late final DeltaTextInputClient client;
late final TextInputClient client;
Future<void> insertText(String text) async {
for (final c in text.characters) {
@@ -27,28 +27,22 @@ class IMESimulator {
assert(false);
return;
}
final deltas = [
TextEditingDeltaInsertion(
textInserted: character,
oldText: value.text.replaceRange(
value.selection.start,
value.selection.end,
'',
),
insertionOffset: value.selection.baseOffset,
selection: TextSelection.collapsed(
offset: value.selection.baseOffset + 1,
),
composing: TextRange.empty,
final text = value.text
.replaceRange(value.selection.start, value.selection.end, character);
final textEditingValue = TextEditingValue(
text: text,
selection: TextSelection.collapsed(
offset: value.selection.baseOffset + 1,
),
];
client.updateEditingValueWithDeltas(deltas);
composing: TextRange.empty,
);
client.updateEditingValue(textEditingValue);
await tester.pumpAndSettle();
}
DeltaTextInputClient findDeltaTextInputClient() {
TextInputClient findTextInputClient() {
final finder = find.byType(KeyboardServiceWidget);
final KeyboardServiceWidgetState state = tester.state(finder);
return state.textInputService as DeltaTextInputClient;
return state.textInputService as TextInputClient;
}
}
@@ -113,7 +113,7 @@ class CalendarDayCard extends StatelessWidget {
.add(CalendarEvent.moveEvent(event, date));
},
),
_NewEventButton(onCreate: () => onCreateEvent(date)),
NewEventButton(onCreate: () => onCreateEvent(date)),
MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
@@ -160,9 +160,10 @@ class _Header extends StatelessWidget {
}
}
class _NewEventButton extends StatelessWidget {
@visibleForTesting
class NewEventButton extends StatelessWidget {
final VoidCallback onCreate;
const _NewEventButton({required this.onCreate, Key? key}) : super(key: key);
const NewEventButton({required this.onCreate, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -254,7 +255,7 @@ class _EventList extends StatelessWidget {
Widget build(BuildContext context) {
return Flexible(
child: ListView.separated(
itemBuilder: (BuildContext context, int index) => _EventCard(
itemBuilder: (BuildContext context, int index) => EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
@@ -270,13 +271,14 @@ class _EventList extends StatelessWidget {
}
}
class _EventCard extends StatelessWidget {
@visibleForTesting
class EventCard extends StatelessWidget {
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
const _EventCard({
const EventCard({
required this.event,
required this.viewId,
required this.rowCache,
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
@@ -241,41 +243,7 @@ class _GridRows extends StatelessWidget {
);
return ScrollConfiguration(
behavior: behavior,
child: ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex =
newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
),
child: _renderList(context, state, rowInfos),
);
},
),
@@ -283,6 +251,66 @@ class _GridRows extends StatelessWidget {
);
}
Widget _renderList(
BuildContext context,
GridState state,
List<RowInfo> rowInfos,
) {
if (Platform.isWindows) {
// Workaround: On Windows, the focusing of the text cell is not working
// properly when the list is reorderable. So using the ListView instead.
return ListView.builder(
controller: scrollController.verticalController,
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: false,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
} else {
return ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
}
}
Widget _renderRow(
BuildContext context,
RowId rowId, {
@@ -138,7 +138,6 @@ class _OptionNameTextField extends StatelessWidget {
return FlowyTextField(
autoFocus: autoFocus,
text: name,
maxLength: 30,
submitOnLeave: true,
onSubmitted: (newName) {
if (name != newName) {
@@ -103,11 +103,11 @@ class BlankCell extends StatelessWidget {
}
abstract class CellEditable {
GridCellFocusListener get beginFocus;
RequestFocusListener get requestFocus;
ValueNotifier<bool> get onCellFocus;
ValueNotifier<bool> get onCellEditing;
// ValueNotifier<bool> get onCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(
@@ -125,11 +125,7 @@ abstract class CellAccessory extends Widget {
abstract class GridCellWidget extends StatefulWidget
implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({Key? key}) : super(key: key) {
onCellEditing.addListener(() {
onCellFocus.value = onCellEditing.value;
});
}
GridCellWidget({super.key});
@override
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
@@ -138,8 +134,8 @@ abstract class GridCellWidget extends StatefulWidget
@override
ValueNotifier<bool> get onAccessoryHover => onCellFocus;
@override
final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
// @override
// final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
@override
List<GridCellAccessoryBuilder> Function(
@@ -147,7 +143,7 @@ abstract class GridCellWidget extends StatefulWidget
)? get accessoryBuilder => null;
@override
final GridCellFocusListener beginFocus = GridCellFocusListener();
final RequestFocusListener requestFocus = RequestFocusListener();
@override
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
@@ -156,7 +152,7 @@ abstract class GridCellWidget extends StatefulWidget
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override
void initState() {
widget.beginFocus.setListener(() => requestBeginFocus());
widget.requestFocus.setListener(requestBeginFocus);
widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
Clipboard.getData("text/plain").then((data) {
@@ -172,17 +168,18 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override
void didUpdateWidget(covariant T oldWidget) {
if (oldWidget != this) {
widget.beginFocus.setListener(() => requestBeginFocus());
widget.requestFocus.setListener(requestBeginFocus);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.beginFocus.removeAllListener();
widget.requestFocus.removeAllListener();
super.dispose();
}
/// Subclass can override this method to request focus.
void requestBeginFocus();
String? onCopy() => null;
@@ -190,9 +187,9 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
void onInsert(String value) {}
}
abstract class GridFocusNodeCellState<T extends GridCellWidget>
abstract class GridEditableTextCell<T extends GridCellWidget>
extends GridCellState<T> {
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
SingleListenerFocusNode get focusNode;
@override
void initState() {
@@ -226,9 +223,9 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget>
}
void _listenOnFocusNodeChanged() {
widget.onCellEditing.value = focusNode.hasFocus;
widget.onCellFocus.value = focusNode.hasFocus;
focusNode.setListener(() {
widget.onCellEditing.value = focusNode.hasFocus;
widget.onCellFocus.value = focusNode.hasFocus;
focusChanged();
});
}
@@ -236,7 +233,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget>
Future<void> focusChanged() async {}
}
class GridCellFocusListener extends ChangeNotifier {
class RequestFocusListener extends ChangeNotifier {
VoidCallback? _listener;
void setListener(VoidCallback listener) {
@@ -51,8 +51,8 @@ class CellContainer extends StatelessWidget {
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => child.beginFocus.notify(),
behavior: HitTestBehavior.opaque,
onTap: () => child.requestFocus.notify(),
child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus),
@@ -45,14 +45,14 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
});
return GridChecklistCellEditor(
cellController:
widget.cellControllerBuilder.build() as ChecklistCellController,
);
},
onClose: () => widget.onCellEditing.value = false,
onClose: () => widget.onCellFocus.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
@@ -90,11 +90,11 @@ class _DateCellState extends GridCellState<GridDateCell> {
return DateCellEditor(
cellController: widget.cellControllerBuilder.build()
as DateCellController,
onDismissed: () => widget.onCellEditing.value = false,
onDismissed: () => widget.onCellFocus.value = false,
);
},
onClose: () {
widget.onCellEditing.value = false;
widget.onCellFocus.value = false;
},
);
}
@@ -115,7 +115,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
_popover.show();
if (widget.editable) {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
}
}
@@ -123,7 +123,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
const TypeOptionSeparator(spacing: 12.0),
const _IncludeTimeButton(),
const TypeOptionSeparator(spacing: 12.0),
_DateTypeOptionButton(popoverMutex: popoverMutex)
DateTypeOptionButton(popoverMutex: popoverMutex)
];
return ListView.builder(
@@ -337,9 +337,10 @@ class _TimeTextFieldState extends State<_TimeTextField> {
}
}
class _DateTypeOptionButton extends StatelessWidget {
@visibleForTesting
class DateTypeOptionButton extends StatelessWidget {
final PopoverMutex popoverMutex;
const _DateTypeOptionButton({
const DateTypeOptionButton({
required this.popoverMutex,
Key? key,
}) : super(key: key);
@@ -16,13 +16,16 @@ class GridNumberCell extends GridCellWidget {
}) : super(key: key);
@override
GridFocusNodeCellState<GridNumberCell> createState() => _NumberCellState();
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
late NumberCellBloc _cellBloc;
late TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
@@ -62,7 +62,7 @@ class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: widget.onCellEditing,
onCellEditing: widget.onCellFocus,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);
@@ -125,7 +125,7 @@ class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: widget.onCellEditing,
onCellEditing: widget.onCellFocus,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);
@@ -157,7 +157,6 @@ class _TextField extends StatelessWidget {
options: state.options,
selectedOptionMap: optionMap,
distanceToText: _editorPanelWidth * 0.7,
maxLength: 30,
tagController: tagController,
textSeparators: const [','],
onClick: () => popoverMutex.close(),
@@ -41,13 +41,16 @@ class GridTextCell extends GridCellWidget {
}
@override
GridFocusNodeCellState<GridTextCell> createState() => _GridTextCellState();
GridEditableTextCell<GridTextCell> createState() => _GridTextCellState();
}
class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
@@ -108,11 +108,14 @@ class GridURLCell extends GridCellWidget {
}
}
class _GridURLCellState extends GridFocusNodeCellState<GridURLCell> {
class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
final _popoverController = PopoverController();
late final URLCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
@@ -182,7 +185,7 @@ class _GridURLCellState extends GridFocusNodeCellState<GridURLCell> {
@override
void requestBeginFocus() {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
_popoverController.show();
}
@@ -85,8 +85,8 @@ class _PropertyCellState extends State<_PropertyCell> {
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
final gesture = GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => cell.beginFocus.notify(),
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
child: cell,
@@ -97,8 +97,8 @@ class _PropertyCellState extends State<_PropertyCell> {
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
AppFlowyPopover(
controller: popover,
@@ -108,6 +108,7 @@ class _PropertyCellState extends State<_PropertyCell> {
popupBuilder: (popoverContext) => buildFieldEditor(),
child: SizedBox(
width: 150,
height: 40,
child: FieldCellButton(
field: widget.cellContext.fieldInfo.field,
onTap: () => popover.show(),
@@ -9,7 +9,7 @@ import 'package:appflowy/plugins/document/application/doc_service.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
show EditorState, LogLevel, TransactionTime;
show EditorState, LogLevel, TransactionTime, Selection, paragraphNode;
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/foundation.dart';
@@ -155,6 +155,9 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
return;
}
await _transactionAdapter.apply(event.$2, editorState);
// check if the document is empty.
applyRules();
});
// output the log from the editor when debug mode
@@ -166,6 +169,39 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
};
}
}
Future<void> applyRules() async {
ensureAtLeastOneParagraphExists();
ensureLastNodeIsEditable();
}
Future<void> ensureLastNodeIsEditable() async {
final editorState = this.editorState;
if (editorState == null) {
return;
}
final document = editorState.document;
final lastNode = document.root.children.lastOrNull;
if (lastNode == null || lastNode.delta == null) {
final transaction = editorState.transaction;
transaction.insertNode([document.root.children.length], paragraphNode());
await editorState.apply(transaction);
}
}
Future<void> ensureAtLeastOneParagraphExists() async {
final editorState = this.editorState;
if (editorState == null) {
return;
}
final document = editorState.document;
if (document.root.children.isEmpty) {
final transaction = editorState.transaction;
transaction.insertNode([0], paragraphNode());
transaction.afterSelection = Selection.collapse([0], 0);
await editorState.apply(transaction);
}
}
}
@freezed
@@ -123,7 +123,7 @@ class _DocumentPageState extends State<DocumentPage> {
return const Placeholder();
}
final page = editorState!.document.root;
return CoverImageNodeWidget(
return DocumentHeaderNodeWidget(
node: page,
editorState: editorState!,
);
@@ -1,10 +1,10 @@
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_list.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/database/referenced_database_menu_tem.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -34,6 +34,8 @@ class AppFlowyEditorPage extends StatefulWidget {
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
late final ScrollController effectiveScrollController;
final inlinePageReferenceService = InlinePageReferenceService();
final List<CommandShortcutEvent> commandShortcutEvents = [
...codeBlockCommands,
...standardCommandShortcutEvents,
@@ -52,23 +54,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
highlightColorItem,
];
late final slashMenuItems = [
inlineGridMenuItem(documentBloc),
referencedGridMenuItem,
inlineBoardMenuItem(documentBloc),
referencedBoardMenuItem,
inlineCalendarMenuItem(documentBloc),
referencedCalendarMenuItem,
calloutItem,
mathEquationItem,
codeBlockItem,
emojiMenuItem,
autoGeneratorMenuItem,
];
late final List<SelectionMenuItem> slashMenuItems;
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
_customAppFlowyBlockComponentBuilders();
List<CharacterShortcutEvent> get characterShortcutEvents => [
// inline page reference list
...inlinePageReferenceShortcuts,
// code block
...codeBlockCharacterEvents,
@@ -87,6 +81,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
), // remove the default slash command.
];
late final inlinePageReferenceShortcuts = [
inlinePageReferenceService.customPageLinkMenu(
character: '@',
style: styleCustomizer.selectionMenuStyleBuilder(),
),
// uncomment this to enable the inline page reference list
// inlinePageReferenceService.customPageLinkMenu(
// character: '+',
// style: styleCustomizer.selectionMenuStyleBuilder(),
// ),
];
late final showSlashMenu = customSlashCommand(
slashMenuItems,
shouldInsertSlash: false,
@@ -99,6 +105,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
@override
void initState() {
super.initState();
slashMenuItems = _customSlashMenuItems();
effectiveScrollController = widget.scrollController ?? ScrollController();
}
@@ -132,6 +141,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
characterShortcutEvents: characterShortcutEvents,
commandShortcutEvents: commandShortcutEvents,
header: widget.header,
footer: const VSpace(200),
);
return Center(
@@ -161,7 +171,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
];
final configuration = BlockComponentConfiguration(
padding: (_) => const EdgeInsets.symmetric(vertical: 4.0),
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
);
final customBlockComponentBuilderMap = {
PageBlockKeys.type: PageBlockComponentBuilder(),
@@ -198,6 +208,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
),
ImageBlockKeys.type: ImageBlockComponentBuilder(
configuration: configuration,
showMenu: true,
menuBuilder: (node, state) => Positioned(
top: 0,
right: 10,
child: ImageMenu(
node: node,
state: state,
),
),
),
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
configuration: configuration,
@@ -211,7 +230,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
configuration: configuration,
),
DividerBlockKeys.type: DividerBlockComponentBuilder(),
DividerBlockKeys.type: DividerBlockComponentBuilder(
configuration: configuration,
height: 28.0,
),
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
configuration: configuration.copyWith(
padding: (_) => const EdgeInsets.symmetric(vertical: 20),
@@ -230,7 +252,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
),
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(),
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
configuration: configuration,
),
OutlineBlockKeys.type: OutlineBlockComponentBuilder(
configuration: configuration.copyWith(
placeholderTextStyle: (_) =>
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
),
),
};
final builders = {
@@ -253,7 +283,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
NumberedListBlockKeys.type,
QuoteBlockKeys.type,
TodoListBlockKeys.type,
CalloutBlockKeys.type
CalloutBlockKeys.type,
OutlineBlockKeys.type,
];
final supportAlignBuilderType = [
@@ -277,7 +308,13 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
];
builder.showActions = (_) => true;
builder.actionBuilder = (context, state) => BlockActionList(
builder.actionBuilder = (context, state) {
final padding = context.node.type == HeadingBlockKeys.type
? const EdgeInsets.only(top: 8.0)
: const EdgeInsets.all(0);
return Padding(
padding: padding,
child: BlockActionList(
blockComponentContext: context,
blockComponentState: state,
editorState: widget.editorState,
@@ -285,12 +322,42 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
showSlashMenu: () => showSlashMenu(
widget.editorState,
),
);
),
);
};
}
return builders;
}
List<SelectionMenuItem> _customSlashMenuItems() {
final items = [...standardSelectionMenuItems];
final imageItem = items.firstWhereOrNull(
(element) => element.name == AppFlowyEditorLocalizations.current.image,
);
if (imageItem != null) {
final imageItemIndex = items.indexOf(imageItem);
if (imageItemIndex != -1) {
items[imageItemIndex] = customImageMenuItem;
}
}
return [
...items,
inlineGridMenuItem(documentBloc),
referencedGridMenuItem,
inlineBoardMenuItem(documentBloc),
referencedBoardMenuItem,
inlineCalendarMenuItem(documentBloc),
referencedCalendarMenuItem,
calloutItem,
outlineItem,
mathEquationItem,
codeBlockItem,
emojiMenuItem,
autoGeneratorMenuItem,
];
}
(bool, Selection?) _computeAutoFocusParameters() {
if (widget.editorState.document.isEmpty) {
return (true, Selection.collapse([0], 0));
@@ -31,14 +31,14 @@ class BlockActionList extends StatelessWidget {
editorState: editorState,
showSlashMenu: showSlashMenu,
),
const SizedBox(width: 8.0),
const SizedBox(width: 4.0),
BlockOptionButton(
blockComponentContext: blockComponentContext,
blockComponentState: blockComponentState,
actions: actions,
editorState: editorState,
),
const SizedBox(width: 6.0),
const SizedBox(width: 4.0),
],
);
}
@@ -87,7 +87,8 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
final Map<int, (ViewPB, ViewPB)> _items = {};
Future<List<(ViewPB, List<ViewPB>)>> fetchItems() async {
final items = await ViewBackendService().fetchViews(widget.layoutType);
final items =
await ViewBackendService().fetchViewsWithLayoutType(widget.layoutType);
int index = 0;
for (final (app, children) in items) {
@@ -150,7 +150,11 @@ class _CalloutBlockComponentWidgetState
children: [
// the emoji picker button for the note
Padding(
padding: const EdgeInsets.all(2.0),
padding: const EdgeInsets.only(
top: 6.0,
left: 2.0,
right: 2.0,
),
child: EmojiPickerButton(
key: ValueKey(
emoji.toString(),
@@ -1,555 +0,0 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
class CoverBlockKeys {
const CoverBlockKeys._();
static const String selectionType = 'cover_selection_type';
static const String selection = 'cover_selection';
static const String iconSelection = 'selected_icon';
}
enum CoverSelectionType {
initial,
color,
file,
asset;
static CoverSelectionType fromString(String? value) {
if (value == null) {
return CoverSelectionType.initial;
}
return CoverSelectionType.values.firstWhere(
(e) => e.toString() == value,
orElse: () => CoverSelectionType.initial,
);
}
}
class CoverNodeWidgetBuilder implements NodeWidgetBuilder {
@override
Widget build(NodeWidgetContext<Node> context) {
return CoverImageNodeWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return true;
};
}
class CoverImageNodeWidget extends StatefulWidget {
const CoverImageNodeWidget({
Key? key,
required this.node,
required this.editorState,
}) : super(key: key);
final Node node;
final EditorState editorState;
@override
State<CoverImageNodeWidget> createState() => _CoverImageNodeWidgetState();
}
class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
CoverSelectionType get selectionType => CoverSelectionType.fromString(
widget.node.attributes[CoverBlockKeys.selectionType],
);
@override
void initState() {
super.initState();
widget.node.addListener(_reload);
}
@override
void dispose() {
widget.node.removeListener(_reload);
super.dispose();
}
void _reload() {
setState(() {});
}
PopoverController iconPopoverController = PopoverController();
@override
Widget build(BuildContext context) {
return _CoverImage(
editorState: widget.editorState,
node: widget.node,
onCoverChanged: (type, value) {
_insertCover(type, value);
},
);
}
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.selectionType: type.toString(),
CoverBlockKeys.selection: cover,
CoverBlockKeys.iconSelection:
widget.node.attributes[CoverBlockKeys.iconSelection]
});
return widget.editorState.apply(transaction);
}
}
class _AddCoverButton extends StatefulWidget {
final Node node;
final EditorState editorState;
final bool hasIcon;
final CoverSelectionType selectionType;
final PopoverController iconPopoverController;
const _AddCoverButton({
required this.onTap,
required this.node,
required this.editorState,
required this.hasIcon,
required this.selectionType,
required this.iconPopoverController,
});
final VoidCallback onTap;
@override
State<_AddCoverButton> createState() => _AddCoverButtonState();
}
bool isPopoverOpen = false;
class _AddCoverButtonState extends State<_AddCoverButton> {
bool isHidden = true;
PopoverMutex mutex = PopoverMutex();
bool isPopoverOpen = false;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setHidden(false);
},
onExit: (event) {
setHidden(isPopoverOpen ? false : true);
},
opaque: false,
child: Container(
height: widget.hasIcon ? 180 : 50.0,
alignment: Alignment.bottomLeft,
width: double.infinity,
padding: const EdgeInsets.only(
left: 80,
top: 20,
bottom: 5,
),
child: isHidden
? Container()
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Add Cover Button.
widget.selectionType != CoverSelectionType.initial
? Container()
: FlowyButton(
key: UniqueKey(),
leftIconSize: const Size.square(18),
onTap: widget.onTap,
useIntrinsicWidth: true,
leftIcon: const FlowySvg(name: 'editor/image'),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addCover.tr(),
),
),
// Add Icon Button.
widget.hasIcon
? FlowyButton(
leftIconSize: const Size.square(18),
onTap: () {
_removeIcon();
},
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
)
: AppFlowyPopover(
mutex: mutex,
asBarrier: true,
onClose: () {
isPopoverOpen = false;
setHidden(true);
},
offset: const Offset(120, 10),
controller: widget.iconPopoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints:
BoxConstraints.loose(const Size(320, 380)),
margin: EdgeInsets.zero,
child: FlowyButton(
leftIconSize: const Size.square(18),
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
),
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return EmojiPopover(
showRemoveButton: widget.hasIcon,
removeIcon: _removeIcon,
node: widget.node,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
_insertIcon(emoji);
widget.iconPopoverController.close();
},
);
},
)
],
),
),
);
}
Future<void> _insertIcon(Emoji emoji) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.selectionType:
widget.node.attributes[CoverBlockKeys.selectionType],
CoverBlockKeys.selection:
widget.node.attributes[CoverBlockKeys.selection],
CoverBlockKeys.iconSelection: emoji.emoji,
});
return widget.editorState.apply(transaction);
}
Future<void> _removeIcon() async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.iconSelection: "",
CoverBlockKeys.selectionType:
widget.node.attributes[CoverBlockKeys.selectionType],
CoverBlockKeys.selection:
widget.node.attributes[CoverBlockKeys.selection],
});
return widget.editorState.apply(transaction);
}
void setHidden(bool value) {
if (isHidden == value) return;
setState(() {
isHidden = value;
});
}
}
class _CoverImage extends StatefulWidget {
const _CoverImage({
required this.editorState,
required this.node,
required this.onCoverChanged,
});
final Node node;
final EditorState editorState;
final Function(
CoverSelectionType selectionType,
dynamic selection,
) onCoverChanged;
@override
State<_CoverImage> createState() => _CoverImageState();
}
class _CoverImageState extends State<_CoverImage> {
final popoverController = PopoverController();
CoverSelectionType get selectionType => CoverSelectionType.fromString(
widget.node.attributes[CoverBlockKeys.selectionType],
);
Color get color {
final hex = widget.node.attributes[CoverBlockKeys.selection] as String?;
return hex?.toColor() ?? Colors.white;
}
bool get hasIcon =>
widget.node.attributes[CoverBlockKeys.iconSelection] == null
? false
: widget.node.attributes[CoverBlockKeys.iconSelection].isNotEmpty;
bool isOverlayButtonsHidden = true;
PopoverController iconPopoverController = PopoverController();
bool get hasCover =>
selectionType == CoverSelectionType.initial ? false : true;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.bottomLeft,
children: [
Container(
alignment: Alignment.topCenter,
height: !hasCover
? 0
: hasIcon
? 320
: 280,
child: _buildCoverImage(context, widget.editorState),
),
hasIcon
? Positioned(
left: 80,
bottom: !hasCover ? 30 : 40,
child: AppFlowyPopover(
offset: const Offset(100, 0),
controller: iconPopoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(320, 380)),
margin: EdgeInsets.zero,
child: EmojiIconWidget(
emoji: widget.node.attributes[CoverBlockKeys.iconSelection],
),
popupBuilder: (BuildContext popoverContext) {
return EmojiPopover(
node: widget.node,
showRemoveButton: hasIcon,
removeIcon: _removeIcon,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
_insertIcon(emoji);
iconPopoverController.close();
},
);
},
),
)
: Container(),
hasIcon && selectionType != CoverSelectionType.initial
? Container()
: _AddCoverButton(
onTap: () {
_insertCover(
CoverSelectionType.asset,
builtInAssetImages.first,
);
},
node: widget.node,
editorState: widget.editorState,
hasIcon: hasIcon,
selectionType: selectionType,
iconPopoverController: iconPopoverController,
),
],
);
}
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.selectionType: type.toString(),
CoverBlockKeys.selection: cover,
CoverBlockKeys.iconSelection:
widget.node.attributes[CoverBlockKeys.iconSelection]
});
return widget.editorState.apply(transaction);
}
Future<void> _insertIcon(Emoji emoji) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.selectionType:
widget.node.attributes[CoverBlockKeys.selectionType],
CoverBlockKeys.selection:
widget.node.attributes[CoverBlockKeys.selection],
CoverBlockKeys.iconSelection: emoji.emoji,
});
return widget.editorState.apply(transaction);
}
Future<void> _removeIcon() async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.iconSelection: "",
CoverBlockKeys.selectionType:
widget.node.attributes[CoverBlockKeys.selectionType],
CoverBlockKeys.selection:
widget.node.attributes[CoverBlockKeys.selection],
});
return widget.editorState.apply(transaction);
}
Widget _buildCoverOverlayButtons(BuildContext context) {
return Positioned(
bottom: 20,
right: 50,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AppFlowyPopover(
onClose: () {
setOverlayButtonsHidden(true);
},
offset: const Offset(-125, 10),
controller: popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(380, 450)),
margin: EdgeInsets.zero,
child: Visibility(
maintainState: true,
maintainAnimation: true,
maintainSize: true,
visible: !isOverlayButtonsHidden,
child: RoundedTextButton(
onPressed: () {
popoverController.show();
setOverlayButtonsHidden(true);
},
hoverColor: Theme.of(context).colorScheme.surface,
textColor: Theme.of(context).colorScheme.tertiary,
fillColor:
Theme.of(context).colorScheme.surface.withOpacity(0.5),
width: 120,
height: 28,
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
),
),
popupBuilder: (BuildContext popoverContext) {
return ChangeCoverPopover(
node: widget.node,
editorState: widget.editorState,
onCoverChanged: widget.onCoverChanged,
);
},
),
const SizedBox(width: 10),
Visibility(
maintainAnimation: true,
maintainSize: true,
maintainState: true,
visible: !isOverlayButtonsHidden,
child: FlowyIconButton(
hoverColor: Theme.of(context).colorScheme.surface,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
iconPadding: const EdgeInsets.all(5),
width: 28,
icon: svgWidget(
'editor/delete',
color: Theme.of(context).colorScheme.tertiary,
),
onPressed: () {
widget.onCoverChanged(CoverSelectionType.initial, null);
},
),
),
],
),
);
}
Widget _buildCoverImage(BuildContext context, EditorState editorState) {
const height = 250.0;
final Widget coverImage;
switch (selectionType) {
case CoverSelectionType.file:
final imageFile =
File(widget.node.attributes[CoverBlockKeys.selection]);
if (!imageFile.existsSync()) {
// reset cover state
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCoverChanged(CoverSelectionType.initial, null);
});
coverImage = const SizedBox();
break;
}
coverImage = Image.file(
imageFile,
fit: BoxFit.cover,
);
break;
case CoverSelectionType.asset:
coverImage = Image.asset(
widget.node.attributes[CoverBlockKeys.selection],
fit: BoxFit.cover,
);
break;
case CoverSelectionType.color:
coverImage = Container(
decoration: BoxDecoration(
color: color,
borderRadius: Corners.s6Border,
),
alignment: Alignment.center,
);
break;
case CoverSelectionType.initial:
coverImage = const SizedBox();
break;
}
// OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
return MouseRegion(
onEnter: (event) {
setOverlayButtonsHidden(false);
},
onExit: (event) {
setOverlayButtonsHidden(true);
},
child: SizedBox(
height: height,
child: Stack(
children: [
Container(
padding: const EdgeInsets.only(bottom: 10),
height: double.infinity,
width: double.infinity,
child: coverImage,
),
hasCover
? _buildCoverOverlayButtons(context)
: const SizedBox.shrink()
],
),
),
);
}
void setOverlayButtonsHidden(bool value) {
if (isOverlayButtonsHidden == value) return;
setState(() {
isOverlayButtonsHidden = value;
});
}
}
@@ -1,84 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra/image.dart';
import 'package:flutter/material.dart';
class EmojiPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final void Function(Emoji emoji) onEmojiChanged;
final VoidCallback removeIcon;
final bool showRemoveButton;
const EmojiPopover({
super.key,
required this.editorState,
required this.node,
required this.onEmojiChanged,
required this.removeIcon,
required this.showRemoveButton,
});
@override
State<EmojiPopover> createState() => _EmojiPopoverState();
}
class _EmojiPopoverState extends State<EmojiPopover> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(15),
child: Column(
children: [
if (widget.showRemoveButton)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Align(
alignment: Alignment.centerRight,
child: DeleteButton(onPressed: widget.removeIcon),
),
),
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
config: Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Theme.of(context).iconTheme.color!,
iconColorSelected: Theme.of(context).colorScheme.onSurface,
selectedHoverColor: Theme.of(context).colorScheme.secondary,
progressIndicatorColor: Theme.of(context).iconTheme.color!,
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
),
],
),
);
}
}
class DeleteButton extends StatelessWidget {
final VoidCallback onPressed;
const DeleteButton({required this.onPressed, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyButton(
onTap: () => onPressed,
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(name: 'editor/delete'),
);
}
}
@@ -11,6 +11,7 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -25,7 +26,7 @@ class ChangeCoverPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final Function(
CoverSelectionType selectionType,
CoverType selectionType,
String selection,
) onCoverChanged;
@@ -83,7 +84,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
child: BlocBuilder<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.all(15),
padding: const EdgeInsets.all(12),
child: SingleChildScrollView(
child: isAddingImage
? CoverImagePicker(
@@ -116,18 +117,18 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
LocaleKeys.document_plugins_cover_colors.tr(),
color: Theme.of(context).colorScheme.tertiary,
),
const SizedBox(height: 10),
const VSpace(10),
_buildColorPickerList(),
const SizedBox(height: 10),
const VSpace(10),
_buildImageHeader(),
const SizedBox(height: 10),
const VSpace(10),
_buildFileImagePicker(),
const SizedBox(height: 10),
const VSpace(10),
FlowyText.semibold(
LocaleKeys.document_plugins_cover_abstract.tr(),
color: Theme.of(context).colorScheme.tertiary,
),
const SizedBox(height: 10),
const VSpace(10),
_buildAbstractImagePicker(),
],
);
@@ -149,10 +150,10 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
LocaleKeys.document_plugins_cover_clearAll.tr(),
fontColor: Theme.of(context).colorScheme.tertiary,
onPressed: () async {
final hasFileImageCover = CoverSelectionType.fromString(
widget.node.attributes[CoverBlockKeys.selectionType],
final hasFileImageCover = CoverType.fromString(
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
) ==
CoverSelectionType.file;
CoverType.file;
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
if (hasFileImageCover) {
await showDialog(
@@ -196,7 +197,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
return InkWell(
onTap: () {
widget.onCoverChanged(
CoverSelectionType.asset,
CoverType.asset,
builtInAssetImages[index],
);
},
@@ -220,14 +221,14 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
pickerBackgroundColor: theme.cardColor,
pickerItemHoverColor: theme.hoverColor,
selectedBackgroundColorHex:
widget.node.attributes[CoverBlockKeys.selectionType] ==
CoverSelectionType.color.toString()
? widget.node.attributes[CoverBlockKeys.selection]
widget.node.attributes[DocumentHeaderBlockKeys.coverType] ==
CoverType.color.toString()
? widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]
: 'ffffff',
backgroundColorOptions:
_generateBackgroundColorOptions(widget.editorState),
onSubmittedBackgroundColorHex: (color) {
widget.onCoverChanged(CoverSelectionType.color, color);
widget.onCoverChanged(CoverType.color, color);
setState(() {});
},
);
@@ -249,43 +250,25 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
itemCount: images.length + 1,
itemBuilder: (BuildContext ctx, index) {
if (index == 0) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: Corners.s8Border,
),
child: FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.primary,
),
hoverColor:
Theme.of(context).colorScheme.primary.withOpacity(0.15),
width: 20,
onPressed: () {
setState(() {
isAddingImage = true;
});
},
return NewCustomCoverButton(
onPressed: () => setState(
() => isAddingImage = true,
),
);
}
return ImageGridItem(
onImageSelect: () {
widget.onCoverChanged(
CoverSelectionType.file,
CoverType.file,
images[index - 1],
);
},
onImageDelete: () async {
final changeCoverBloc =
context.read<ChangeCoverPopoverBloc>();
final deletingCurrentCover =
widget.node.attributes[CoverBlockKeys.selection] ==
images[index - 1];
final deletingCurrentCover = widget.node
.attributes[DocumentHeaderBlockKeys.coverDetails] ==
images[index - 1];
if (deletingCurrentCover) {
await showDialog(
context: context,
@@ -328,6 +311,32 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
}
}
@visibleForTesting
class NewCustomCoverButton extends StatelessWidget {
final VoidCallback onPressed;
const NewCustomCoverButton({super.key, required this.onPressed});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: Corners.s8Border,
),
child: FlowyIconButton(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.primary,
),
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.15),
onPressed: onPressed,
),
);
}
}
class DeleteImageAlertDialog extends StatelessWidget {
const DeleteImageAlertDialog({
Key? key,
@@ -481,36 +490,63 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
scrollController.dispose();
}
Widget _buildColorItem(ColorOption option, bool isChecked) {
Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: options
.map(
(e) => ColorItem(
option: e,
isChecked: e.colorHex == selectedColor,
hoverColor: widget.pickerItemHoverColor,
onTap: widget.onSubmittedBackgroundColorHex,
),
)
.toList(),
);
}
}
@visibleForTesting
class ColorItem extends StatelessWidget {
final ColorOption option;
final bool isChecked;
final Color hoverColor;
final void Function(String) onTap;
const ColorItem({
required this.option,
required this.isChecked,
required this.hoverColor,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return InkWell(
customBorder: const RoundedRectangleBorder(
borderRadius: Corners.s6Border,
),
hoverColor: widget.pickerItemHoverColor,
onTap: () {
widget.onSubmittedBackgroundColorHex(option.colorHex);
},
hoverColor: hoverColor,
onTap: () => onTap(option.colorHex),
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: SizedBox.square(
dimension: isChecked ? 24 : 25,
dimension: 25,
child: Container(
decoration: BoxDecoration(
color: option.colorHex.toColor(),
border: isChecked
? Border.all(
color: const Color(0xFFFFFFFF),
width: 2.0,
)
: null,
shape: BoxShape.circle,
),
child: isChecked
? SizedBox.square(
dimension: 24,
child: Container(
margin: const EdgeInsets.all(4),
margin: const EdgeInsets.all(1),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).cardColor,
width: 3.0,
),
color: option.colorHex.toColor(),
shape: BoxShape.circle,
),
@@ -522,14 +558,4 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
),
);
}
Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: options
.map((e) => _buildColorItem(e, e.colorHex == selectedColor))
.toList(),
);
}
}
@@ -1,14 +1,14 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'change_cover_popover_bloc.freezed.dart';
part 'cover_editor_bloc.freezed.dart';
class ChangeCoverPopoverBloc
extends Bloc<ChangeCoverPopoverEvent, ChangeCoverPopoverState> {
@@ -32,7 +32,7 @@ class ChangeCoverPopoverBloc
deleteImage: (DeleteImage deleteImage) async {
final currentState = state;
final currentlySelectedImage =
node.attributes[CoverBlockKeys.selection];
node.attributes[DocumentHeaderBlockKeys.coverDetails];
if (currentState is Loaded) {
await _deleteImageInStorage(deleteImage.path);
if (currentlySelectedImage == deleteImage.path) {
@@ -48,7 +48,7 @@ class ChangeCoverPopoverBloc
clearAllImages: (ClearAllImages clearAllImages) async {
final currentState = state;
final currentlySelectedImage =
node.attributes[CoverBlockKeys.selection];
node.attributes[DocumentHeaderBlockKeys.coverDetails];
if (currentState is Loaded) {
for (final image in currentState.imageNames) {
@@ -90,9 +90,9 @@ class ChangeCoverPopoverBloc
Future<void> _removeCoverImageFromNode() async {
final transaction = editorState.transaction;
transaction.updateNode(node, {
CoverBlockKeys.selectionType: CoverSelectionType.initial.toString(),
CoverBlockKeys.iconSelection:
node.attributes[CoverBlockKeys.iconSelection]
DocumentHeaderBlockKeys.coverType: CoverType.none.toString(),
DocumentHeaderBlockKeys.icon:
node.attributes[DocumentHeaderBlockKeys.icon]
});
return editorState.apply(transaction);
}
@@ -1,8 +1,9 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flowy_infra/image.dart';
@@ -67,17 +68,13 @@ class _CoverImagePickerState extends State<CoverImagePicker> {
),
)
: CoverImagePreviewWidget(state: state),
const SizedBox(
height: 10,
),
const VSpace(10),
NetworkImageUrlInput(
onAdd: (url) {
context.read<CoverImagePickerBloc>().add(UrlSubmit(url));
},
),
const SizedBox(
height: 10,
),
const VSpace(10),
ImagePickerActionButtons(
onBackPressed: () {
widget.onBackPressed();
@@ -233,16 +230,12 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
),
],
),
const SizedBox(
height: 10,
),
const VSpace(10),
FlowyText(
LocaleKeys.document_plugins_cover_or.tr(),
fontWeight: FontWeight.w300,
),
const SizedBox(
height: 10,
),
const VSpace(10),
FlowyButton(
hoverColor: Theme.of(context).hoverColor,
onTap: () {
@@ -13,9 +13,10 @@ import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as p;
import 'change_cover_popover.dart';
part 'cover_image_picker_bloc.freezed.dart';
import 'cover_editor.dart';
part 'custom_cover_picker_bloc.freezed.dart';
class CoverImagePickerBloc
extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
@@ -0,0 +1,507 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'cover_editor.dart';
import 'emoji_icon_widget.dart';
import 'emoji_popover.dart';
const double kCoverHeight = 250.0;
const double kIconHeight = 60.0;
const double kToolbarHeight = 40.0; // with padding to the top
class DocumentHeaderBlockKeys {
const DocumentHeaderBlockKeys._();
static const String coverType = 'cover_selection_type';
static const String coverDetails = 'cover_selection';
static const String icon = 'selected_icon';
}
enum CoverType {
none,
color,
file,
asset;
static CoverType fromString(String? value) {
if (value == null) {
return CoverType.none;
}
return CoverType.values.firstWhere(
(e) => e.toString() == value,
orElse: () => CoverType.none,
);
}
}
class DocumentHeaderNodeWidgetBuilder implements NodeWidgetBuilder {
@override
Widget build(NodeWidgetContext<Node> context) {
return DocumentHeaderNodeWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (_) => true;
}
class DocumentHeaderNodeWidget extends StatefulWidget {
const DocumentHeaderNodeWidget({
required this.node,
required this.editorState,
super.key,
});
final Node node;
final EditorState editorState;
@override
State<DocumentHeaderNodeWidget> createState() =>
_DocumentHeaderNodeWidgetState();
}
class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
CoverType get coverType => CoverType.fromString(
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
);
String? get coverDetails =>
widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];
String get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon];
bool get hasIcon =>
widget.node.attributes[DocumentHeaderBlockKeys.icon]?.isNotEmpty ?? false;
bool get hasCover => coverType != CoverType.none;
@override
void initState() {
super.initState();
widget.node.addListener(_reload);
}
@override
void dispose() {
widget.node.removeListener(_reload);
super.dispose();
}
void _reload() => setState(() {});
@override
Widget build(BuildContext context) {
return Stack(
children: [
SizedBox(
height: _calculateOverallHeight(),
child: DocumentHeaderToolbar(
onCoverChanged: _saveCover,
node: widget.node,
editorState: widget.editorState,
hasCover: hasCover,
hasIcon: hasIcon,
),
),
if (hasCover)
DocumentCover(
editorState: widget.editorState,
node: widget.node,
coverType: coverType,
coverDetails: coverDetails,
onCoverChanged: (type, details) =>
_saveCover(cover: (type, details)),
),
if (hasIcon)
Positioned(
left: 80,
// if hasCover, there shouldn't be icons present so the icon can
// be closer to the bottom.
bottom:
hasCover ? kToolbarHeight - kIconHeight / 2 : kToolbarHeight,
child: DocumentIcon(
editorState: widget.editorState,
node: widget.node,
icon: icon,
onIconChanged: (icon) => _saveCover(icon: icon),
),
),
],
);
}
double _calculateOverallHeight() {
switch ((hasIcon, hasCover)) {
case (true, true):
return kCoverHeight + kToolbarHeight;
case (true, false):
return 50 + kIconHeight + kToolbarHeight;
case (false, true):
return kCoverHeight + kToolbarHeight;
case (false, false):
return kToolbarHeight;
}
}
Future<void> _saveCover({(CoverType, String?)? cover, String? icon}) {
final transaction = widget.editorState.transaction;
final Map<String, dynamic> attributes = {
DocumentHeaderBlockKeys.coverType:
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
DocumentHeaderBlockKeys.coverDetails:
widget.node.attributes[DocumentHeaderBlockKeys.coverDetails],
DocumentHeaderBlockKeys.icon:
widget.node.attributes[DocumentHeaderBlockKeys.icon]
};
if (cover != null) {
attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString();
attributes[DocumentHeaderBlockKeys.coverDetails] = cover.$2;
}
if (icon != null) {
attributes[DocumentHeaderBlockKeys.icon] = icon;
}
transaction.updateNode(widget.node, attributes);
return widget.editorState.apply(transaction);
}
}
@visibleForTesting
class DocumentHeaderToolbar extends StatefulWidget {
final Node node;
final EditorState editorState;
final bool hasCover;
final bool hasIcon;
final Future<void> Function({(CoverType, String?)? cover, String? icon})
onCoverChanged;
const DocumentHeaderToolbar({
required this.node,
required this.editorState,
required this.hasCover,
required this.hasIcon,
required this.onCoverChanged,
super.key,
});
@override
State<DocumentHeaderToolbar> createState() => _DocumentHeaderToolbarState();
}
class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
bool isHidden = true;
bool isPopoverOpen = false;
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) => setHidden(false),
onExit: (event) {
if (!isPopoverOpen) {
setHidden(true);
}
},
opaque: false,
child: Container(
alignment: Alignment.bottomLeft,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 80),
child: SizedBox(
height: 28,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: buildRowChildren(),
),
),
),
);
}
List<Widget> buildRowChildren() {
if (isHidden || widget.hasCover && widget.hasIcon) {
return [];
}
final List<Widget> children = [];
if (!widget.hasCover) {
children.add(
FlowyButton(
leftIconSize: const Size.square(18),
onTap: () => widget.onCoverChanged(
cover: (CoverType.asset, builtInAssetImages.first),
),
useIntrinsicWidth: true,
leftIcon: const FlowySvg(name: 'editor/image'),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addCover.tr(),
),
),
);
}
if (widget.hasIcon) {
children.add(
FlowyButton(
leftIconSize: const Size.square(18),
onTap: () => widget.onCoverChanged(icon: ""),
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
),
);
} else {
children.add(
AppFlowyPopover(
onClose: () => isPopoverOpen = false,
controller: _popoverController,
offset: const Offset(0, 8),
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(320, 380)),
child: FlowyButton(
leftIconSize: const Size.square(18),
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
),
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return EmojiPopover(
showRemoveButton: widget.hasIcon,
removeIcon: () {
widget.onCoverChanged(icon: "");
_popoverController.close();
},
node: widget.node,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
widget.onCoverChanged(icon: emoji.emoji);
_popoverController.close();
},
);
},
),
);
}
return children;
}
void setHidden(bool value) {
if (isHidden == value) return;
setState(() {
isHidden = value;
});
}
}
@visibleForTesting
class DocumentCover extends StatefulWidget {
final Node node;
final EditorState editorState;
final CoverType coverType;
final String? coverDetails;
final Future<void> Function(CoverType type, String? details) onCoverChanged;
const DocumentCover({
required this.editorState,
required this.node,
required this.coverType,
required this.onCoverChanged,
this.coverDetails,
super.key,
});
@override
State<DocumentCover> createState() => DocumentCoverState();
}
class DocumentCoverState extends State<DocumentCover> {
bool isOverlayButtonsHidden = true;
bool isPopoverOpen = false;
@override
Widget build(BuildContext context) {
return SizedBox(
height: kCoverHeight,
child: MouseRegion(
onEnter: (event) => setOverlayButtonsHidden(false),
onExit: (event) =>
setOverlayButtonsHidden(isPopoverOpen ? false : true),
child: Stack(
children: [
SizedBox(
height: double.infinity,
width: double.infinity,
child: _buildCoverImage(),
),
if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context)
],
),
),
);
}
Widget _buildCoverImage() {
switch (widget.coverType) {
case CoverType.file:
final imageFile = File(widget.coverDetails ?? "");
if (!imageFile.existsSync()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCoverChanged(CoverType.none, null);
});
return const SizedBox.shrink();
}
return Image.file(
imageFile,
fit: BoxFit.cover,
);
case CoverType.asset:
return Image.asset(
widget.coverDetails!,
fit: BoxFit.cover,
);
case CoverType.color:
final color = widget.coverDetails?.toColor() ?? Colors.white;
return Container(color: color);
case CoverType.none:
return const SizedBox.shrink();
}
}
Widget _buildCoverOverlayButtons(BuildContext context) {
return Positioned(
bottom: 20,
right: 50,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AppFlowyPopover(
offset: const Offset(0, 8),
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(380, 450)),
margin: EdgeInsets.zero,
onClose: () => isPopoverOpen = false,
child: RoundedTextButton(
hoverColor: Theme.of(context).colorScheme.surface,
textColor: Theme.of(context).colorScheme.tertiary,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
width: 120,
height: 28,
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
),
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return ChangeCoverPopover(
node: widget.node,
editorState: widget.editorState,
onCoverChanged: (cover, selection) =>
widget.onCoverChanged(cover, selection),
);
},
),
const HSpace(10),
DeleteCoverButton(
onTap: () => widget.onCoverChanged(CoverType.none, null),
),
],
),
);
}
void setOverlayButtonsHidden(bool value) {
if (isOverlayButtonsHidden == value) return;
setState(() {
isOverlayButtonsHidden = value;
});
}
}
@visibleForTesting
class DeleteCoverButton extends StatelessWidget {
final VoidCallback onTap;
const DeleteCoverButton({required this.onTap, super.key});
@override
Widget build(BuildContext context) {
return FlowyIconButton(
hoverColor: Theme.of(context).colorScheme.surface,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
iconPadding: const EdgeInsets.all(5),
width: 28,
icon: svgWidget(
'editor/delete',
color: Theme.of(context).colorScheme.tertiary,
),
onPressed: onTap,
);
}
}
@visibleForTesting
class DocumentIcon extends StatefulWidget {
final Node node;
final EditorState editorState;
final String icon;
final Future<void> Function(String icon) onIconChanged;
const DocumentIcon({
required this.node,
required this.editorState,
required this.icon,
required this.onIconChanged,
super.key,
});
@override
State<DocumentIcon> createState() => _DocumentIconState();
}
class _DocumentIconState extends State<DocumentIcon> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: BoxConstraints.loose(const Size(320, 380)),
child: EmojiIconWidget(emoji: widget.icon),
popupBuilder: (BuildContext popoverContext) {
return EmojiPopover(
node: widget.node,
showRemoveButton: true,
removeIcon: () {
widget.onIconChanged("");
_popoverController.close();
},
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
widget.onIconChanged(emoji.emoji);
_popoverController.close();
},
);
},
);
}
}
@@ -5,12 +5,10 @@ class EmojiIconWidget extends StatefulWidget {
const EmojiIconWidget({
super.key,
required this.emoji,
this.size = 80,
this.emojiSize = 60,
});
final String emoji;
final double size;
final double emojiSize;
@override
@@ -25,12 +23,11 @@ class _EmojiIconWidgetState extends State<EmojiIconWidget> {
return MouseRegion(
onEnter: (_) => setHidden(false),
onExit: (_) => setHidden(true),
cursor: SystemMouseCursors.click,
child: Container(
height: widget.size,
width: widget.size,
decoration: BoxDecoration(
color: !hover
? Theme.of(context).colorScheme.inverseSurface
? Theme.of(context).colorScheme.inverseSurface.withOpacity(0.5)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
@@ -0,0 +1,84 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra/image.dart';
import 'package:flutter/material.dart';
class EmojiPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final void Function(Emoji emoji) onEmojiChanged;
final VoidCallback removeIcon;
final bool showRemoveButton;
const EmojiPopover({
super.key,
required this.editorState,
required this.node,
required this.onEmojiChanged,
required this.removeIcon,
required this.showRemoveButton,
});
@override
State<EmojiPopover> createState() => _EmojiPopoverState();
}
class _EmojiPopoverState extends State<EmojiPopover> {
@override
Widget build(BuildContext context) {
return Column(
children: [
if (widget.showRemoveButton)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Align(
alignment: Alignment.centerRight,
child: DeleteButton(onTap: widget.removeIcon),
),
),
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
config: Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Theme.of(context).iconTheme.color!,
iconColorSelected: Theme.of(context).colorScheme.onSurface,
selectedHoverColor: Theme.of(context).colorScheme.secondary,
progressIndicatorColor: Theme.of(context).iconTheme.color!,
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
),
],
);
}
}
class DeleteButton extends StatelessWidget {
final VoidCallback onTap;
const DeleteButton({required this.onTap, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 28,
child: FlowyButton(
onTap: onTap,
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(name: 'editor/delete'),
),
);
}
}
@@ -0,0 +1,290 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class ImageMenu extends StatefulWidget {
const ImageMenu({
super.key,
required this.node,
required this.state,
});
final Node node;
final ImageBlockComponentWidgetState state;
@override
State<ImageMenu> createState() => _ImageMenuState();
}
class _ImageMenuState extends State<ImageMenu> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
height: 32,
decoration: BoxDecoration(
color: theme.cardColor,
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withOpacity(0.1),
),
],
borderRadius: BorderRadius.circular(4.0),
),
child: Row(
children: [
const HSpace(4),
_ImageCopyLinkButton(
onTap: copyImageLink,
),
const HSpace(4),
_ImageAlignButton(
node: widget.node,
state: widget.state,
),
const _Divider(),
_ImageDeleteButton(
onTap: () => deleteImage(),
),
const HSpace(4),
],
),
);
}
void copyImageLink() {
final url = widget.node.attributes[ImageBlockKeys.url];
if (url != null) {
Clipboard.setData(ClipboardData(text: url));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: FlowyText(
LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
),
),
);
}
}
Future<void> deleteImage() async {
final node = widget.node;
final editorState = context.read<EditorState>();
final transaction = editorState.transaction;
transaction.deleteNode(node);
transaction.afterSelection = null;
await editorState.apply(transaction);
}
}
class _ImageCopyLinkButton extends StatelessWidget {
const _ImageCopyLinkButton({
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: const FlowySvg(
name: 'editor/copy',
size: Size.square(16),
),
);
}
}
class _ImageAlignButton extends StatefulWidget {
const _ImageAlignButton({
required this.node,
required this.state,
});
final Node node;
final ImageBlockComponentWidgetState state;
@override
State<_ImageAlignButton> createState() => _ImageAlignButtonState();
}
const interceptorKey = 'image-align';
class _ImageAlignButtonState extends State<_ImageAlignButton> {
final gestureInterceptor = SelectionGestureInterceptor(
key: interceptorKey,
canTap: (details) => false,
);
String get align => widget.node.attributes['align'] ?? 'center';
final popoverController = PopoverController();
late final EditorState editorState;
@override
void initState() {
super.initState();
editorState = context.read<EditorState>();
}
@override
void dispose() {
allowMenuClose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return IgnoreParentGestureWidget(
child: AppFlowyPopover(
onClose: allowMenuClose,
controller: popoverController,
windowPadding: const EdgeInsets.all(0),
margin: const EdgeInsets.all(0),
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 10),
child: buildAlignIcon(),
popupBuilder: (_) {
preventMenuClose();
return _AlignButtons(
onAlignChanged: onAlignChanged,
);
},
),
);
}
void onAlignChanged(String align) {
popoverController.close();
final transaction = editorState.transaction;
transaction.updateNode(widget.node, {
ImageBlockKeys.align: align,
});
editorState.apply(transaction);
allowMenuClose();
}
void preventMenuClose() {
widget.state.alwaysShowMenu = true;
editorState.service.selectionService.registerGestureInterceptor(
gestureInterceptor,
);
}
void allowMenuClose() {
widget.state.alwaysShowMenu = false;
editorState.service.selectionService.unregisterGestureInterceptor(
interceptorKey,
);
}
Widget buildAlignIcon() {
return FlowySvg(
name: 'editor/align/$align',
size: const Size.square(16),
);
}
}
class _AlignButtons extends StatelessWidget {
const _AlignButtons({
required this.onAlignChanged,
});
final Function(String align) onAlignChanged;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
_AlignButton(
align: 'left',
onTap: () => onAlignChanged('left'),
),
const _Divider(),
_AlignButton(
align: 'left',
onTap: () => onAlignChanged('center'),
),
const _Divider(),
_AlignButton(
align: 'left',
onTap: () => onAlignChanged('right'),
),
const HSpace(4),
],
),
);
}
}
class _AlignButton extends StatelessWidget {
const _AlignButton({
required this.align,
required this.onTap,
});
final String align;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: FlowySvg(
name: 'editor/align/$align',
size: const Size.square(16),
),
);
}
}
class _ImageDeleteButton extends StatelessWidget {
const _ImageDeleteButton({
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: const FlowySvg(
name: 'editor/delete',
size: Size.square(16),
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Container(
width: 1,
color: Colors.grey,
),
);
}
}
@@ -0,0 +1,59 @@
import 'dart:io';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
final customImageMenuItem = SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.image,
icon: (editorState, isSelected, style) => SelectionMenuIconWidget(
name: 'image',
isSelected: isSelected,
style: style,
),
keywords: ['image', 'picture', 'img', 'photo'],
handler: (editorState, menuService, context) {
final container = Overlay.of(context);
showImageMenu(
container,
editorState,
menuService,
onInsertImage: (url) async {
// if the url is http, we can insert it directly
// otherwise, if it's a file url, we need to copy the file to the app's document directory
final regex = RegExp('^(http|https)://');
if (regex.hasMatch(url)) {
await editorState.insertImageNode(url);
} else {
final path = await getIt<ApplicationDataStorage>().getPath();
final imagePath = p.join(
path,
'images',
);
try {
// create the directory if not exists
final directory = Directory(imagePath);
if (!directory.existsSync()) {
await directory.create(recursive: true);
}
final copyToPath = p.join(
imagePath,
'${uuid()}${p.extension(url)}',
);
await File(url).copy(
copyToPath,
);
await editorState.insertImageNode(copyToPath);
} catch (e) {
Log.error('cannot copy image file', e);
}
}
},
);
},
);
@@ -0,0 +1,164 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
enum MentionType {
page;
static MentionType fromString(String value) {
switch (value) {
case 'page':
return page;
default:
throw UnimplementedError();
}
}
}
class MentionBlockKeys {
const MentionBlockKeys._();
static const mention = 'mention';
static const type = 'type'; // MentionType, String
static const pageId = 'page_id';
static const pageType = 'page_type';
static const pageName = 'page_name';
}
class InlinePageReferenceService {
customPageLinkMenu({
bool shouldInsertKeyword = false,
SelectionMenuStyle style = SelectionMenuStyle.light,
String character = '@',
}) {
return CharacterShortcutEvent(
key: 'show page link menu',
character: character,
handler: (editorState) async {
final items = await generatePageItems(character);
return _showPageSelectionMenu(
editorState,
items,
shouldInsertKeyword: shouldInsertKeyword,
style: style,
character: character,
);
},
);
}
SelectionMenuService? _selectionMenuService;
Future<bool> _showPageSelectionMenu(
EditorState editorState,
List<SelectionMenuItem> items, {
bool shouldInsertKeyword = true,
SelectionMenuStyle style = SelectionMenuStyle.light,
String character = '@',
}) async {
if (PlatformExtension.isMobile) {
return false;
}
final selection = editorState.selection;
if (selection == null) {
return false;
}
// delete the selection
await editorState.deleteSelection(selection);
final afterSelection = editorState.selection;
if (afterSelection == null || !afterSelection.isCollapsed) {
assert(false, 'the selection should be collapsed');
return true;
}
await editorState.insertTextAtPosition(
character,
position: selection.start,
);
() {
final context = editorState.getNodeAtPath(selection.start.path)?.context;
if (context != null) {
_selectionMenuService = SelectionMenu(
context: context,
editorState: editorState,
selectionMenuItems: items,
deleteSlashByDefault: false,
style: style,
itemCountFilter: 5,
);
_selectionMenuService?.show();
}
}();
return true;
}
Future<List<SelectionMenuItem>> generatePageItems(String character) async {
final service = ViewBackendService();
final List<(ViewPB, List<ViewPB>)> pbViews = await service.fetchViews(
(_, __) => true,
);
if (pbViews.isEmpty) {
return [];
}
final List<SelectionMenuItem> pages = [];
final List<ViewPB> views = [];
for (final element in pbViews) {
views.addAll(element.$2);
}
views.sort(((a, b) => b.createTime.compareTo(a.createTime)));
for (final view in views) {
final SelectionMenuItem pageSelectionMenuItem = SelectionMenuItem(
icon: (editorState, isSelected, style) => SelectableSvgWidget(
name: view.iconName,
isSelected: isSelected,
style: style,
),
keywords: [
view.name.toLowerCase(),
],
name: view.name,
handler: (editorState, menuService, context) async {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final node = editorState.getNodeAtPath(selection.end.path);
final delta = node?.delta;
if (node == null || delta == null) {
return;
}
final index = selection.endIndex;
final lastKeywordIndex =
delta.toPlainText().substring(0, index).lastIndexOf(character);
// @page name -> $
// preload the page infos
pageMemorizer[view.id] = view;
final transaction = editorState.transaction
..replaceText(
node,
lastKeywordIndex,
index - lastKeywordIndex,
'\$',
attributes: {
MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionType.page.name,
MentionBlockKeys.pageId: view.id,
}
},
);
await editorState.apply(transaction);
},
);
pages.add(pageSelectionMenuItem);
}
return pages;
}
}
@@ -0,0 +1,22 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:flutter/material.dart';
class MentionBlock extends StatelessWidget {
const MentionBlock({
super.key,
required this.mention,
});
final Map mention;
@override
Widget build(BuildContext context) {
final type = MentionType.fromString(mention[MentionBlockKeys.type]);
if (type == MentionType.page) {
final pageId = mention[MentionBlockKeys.pageId];
return MentionPageBlock(key: ValueKey(pageId), pageId: pageId);
}
throw UnimplementedError();
}
}
@@ -0,0 +1,143 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
show EditorState, SelectionUpdateReason;
import 'package:collection/collection.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
final pageMemorizer = <String, ViewPB?>{};
class MentionPageBlock extends StatefulWidget {
const MentionPageBlock({
super.key,
required this.pageId,
});
final String pageId;
@override
State<MentionPageBlock> createState() => _MentionPageBlockState();
}
class _MentionPageBlockState extends State<MentionPageBlock> {
late final EditorState editorState;
late final Future<ViewPB?> viewPBFuture;
ViewListener? viewListener;
@override
void initState() {
super.initState();
editorState = context.read<EditorState>();
viewPBFuture = fetchView(widget.pageId);
viewListener = ViewListener(viewId: widget.pageId)
..start(
onViewUpdated: (p0) {
pageMemorizer[p0.id] = p0;
editorState.reload();
},
);
}
@override
void dispose() {
viewListener?.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return FutureBuilder<ViewPB?>(
initialData: pageMemorizer[widget.pageId],
future: viewPBFuture,
builder: (context, state) {
final view = state.data;
// memorize the result
pageMemorizer[widget.pageId] = view;
if (view == null) {
return const SizedBox.shrink();
}
updateSelection();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: FlowyHover(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => openPage(widget.pageId),
behavior: HitTestBehavior.translucent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
FlowySvg(
name: view.layout.iconName,
size: const Size.square(18.0),
),
const HSpace(2),
FlowyText(
view.name,
decoration: TextDecoration.underline,
fontSize: fontSize,
),
const HSpace(2),
],
),
),
),
);
},
);
}
void openPage(String pageId) async {
final view = await fetchView(pageId);
if (view == null) {
Log.error('Page($pageId) not found');
return;
}
getIt<MenuSharedState>().latestOpenView = view;
}
Future<ViewPB?> fetchView(String pageId) async {
final views = await ViewBackendService().fetchViews((_, __) => true);
final flattenViews = views.expand((e) => [e.$1, ...e.$2]).toList();
final view = flattenViews.firstWhereOrNull(
(element) => element.id == pageId,
);
if (view == null) {
// try to fetch from trash
final trashViews = await TrashService().readTrash();
final trash = trashViews.fold(
(l) => l.items.firstWhereOrNull((element) => element.id == pageId),
(r) => null,
);
if (trash != null) {
return ViewPB()
..id = trash.id
..name = trash.name;
}
}
return view;
}
void updateSelection() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
editorState.updateSelectionWithReason(
editorState.selection,
reason: SelectionUpdateReason.transaction,
);
});
}
}
@@ -0,0 +1,212 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class OutlineBlockKeys {
const OutlineBlockKeys._();
static const String type = 'outline';
static const String backgroundColor = blockComponentBackgroundColor;
}
// defining the callout block menu item for selection
SelectionMenuItem outlineItem = SelectionMenuItem.node(
name: LocaleKeys.document_selectionMenu_outline.tr(),
iconData: Icons.list_alt,
keywords: ['outline', 'table of contents'],
nodeBuilder: (editorState) => outlineBlockNode(),
replace: (_, node) => node.delta?.isEmpty ?? false,
);
Node outlineBlockNode() {
return Node(
type: OutlineBlockKeys.type,
);
}
class OutlineBlockComponentBuilder extends BlockComponentBuilder {
OutlineBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
});
@override
final BlockComponentConfiguration configuration;
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
return OutlineBlockWidget(
key: node.key,
node: node,
configuration: configuration,
showActions: showActions(node),
actionBuilder: (context, state) => actionBuilder(
blockComponentContext,
state,
),
);
}
@override
bool validate(Node node) => node.children.isEmpty;
}
class OutlineBlockWidget extends BlockComponentStatefulWidget {
const OutlineBlockWidget({
super.key,
required super.node,
super.showActions,
super.actionBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@override
State<OutlineBlockWidget> createState() => _OutlineBlockWidgetState();
}
class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
with BlockComponentConfigurable {
@override
BlockComponentConfiguration get configuration => widget.configuration;
@override
Node get node => widget.node;
// get the background color of the note block from the node's attributes
Color get backgroundColor {
final colorString =
node.attributes[OutlineBlockKeys.backgroundColor] as String?;
if (colorString == null) {
return Colors.transparent;
}
return colorString.toColor();
}
late EditorState editorState = context.read<EditorState>();
late Stream<(TransactionTime, Transaction)> stream =
editorState.transactionStream;
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: stream,
builder: (context, snapshot) {
if (widget.showActions && widget.actionBuilder != null) {
return BlockComponentActionWrapper(
node: widget.node,
actionBuilder: widget.actionBuilder!,
child: _buildOutlineBlock(),
);
}
return _buildOutlineBlock();
},
);
}
Widget _buildOutlineBlock() {
final children = getHeadingNodes()
.map(
(e) => Container(
padding: const EdgeInsets.only(
bottom: 4.0,
),
width: double.infinity,
child: OutlineItemWidget(node: e),
),
)
.toList();
if (children.isEmpty) {
return Align(
alignment: Alignment.centerLeft,
child: Text(
LocaleKeys.document_plugins_outline_addHeadingToCreateOutline.tr(),
style: configuration.placeholderTextStyle(node),
),
);
}
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: backgroundColor,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: children,
),
);
}
Iterable<Node> getHeadingNodes() {
final children = editorState.document.root.children;
return children.where((element) => element.type == HeadingBlockKeys.type);
}
}
class OutlineItemWidget extends StatelessWidget {
OutlineItemWidget({
super.key,
required this.node,
}) {
assert(node.type == HeadingBlockKeys.type);
}
final Node node;
@override
Widget build(BuildContext context) {
final editorState = context.read<EditorState>();
final textStyle = editorState.editorStyle.textStyleConfiguration;
final style = textStyle.href.combine(textStyle.text);
return FlowyHover(
style: HoverStyle(
hoverColor: Colors.grey.withOpacity(0.2), // TODO: use theme color.
),
child: GestureDetector(
onTap: () => updateBlockSelection(context),
child: Container(
padding: EdgeInsets.only(left: node.leftIndent),
child: Text(
node.outlineItemText,
style: style,
),
),
),
);
}
void updateBlockSelection(BuildContext context) {
final editorState = context.read<EditorState>();
editorState.selectionType = SelectionType.block;
editorState.selection = Selection.collapse(
node.path,
node.delta?.length ?? 0,
);
editorState.selectionType = null;
}
}
extension on Node {
double get leftIndent {
assert(type == HeadingBlockKeys.type);
if (type != HeadingBlockKeys.type) {
return 0.0;
}
final level = attributes[HeadingBlockKeys.level];
if (level == 2) {
return 20;
} else if (level == 3) {
return 40;
}
return 0;
}
String get outlineItemText {
return delta?.toPlainText() ?? '';
}
}
@@ -1,12 +1,13 @@
export 'callout/callout_block_component.dart';
export 'code_block/code_block_component.dart';
export 'code_block/code_block_shortcut_event.dart';
export 'cover/change_cover_popover_bloc.dart';
export 'cover/cover_node_widget.dart';
export 'cover/cover_image_picker.dart';
export 'header/cover_editor_bloc.dart';
export 'header/document_header_node_widget.dart';
export 'header/custom_cover_picker.dart';
export 'emoji_picker/emoji_menu_item.dart';
export 'extensions/flowy_tint_extension.dart';
export 'database/inline_database_menu_item.dart';
export 'database/referenced_database_menu_item.dart';
export 'database/database_view_block_component.dart';
export 'math_equation/math_equation_block_component.dart';
export 'openai/widgets/auto_completion_node_widget.dart';
@@ -14,3 +15,8 @@ export 'openai/widgets/smart_edit_node_widget.dart';
export 'openai/widgets/smart_edit_toolbar_item.dart';
export 'toggle/toggle_block_component.dart';
export 'toggle/toggle_block_shortcut_event.dart';
export 'outline/outline_block_component.dart';
export 'image/image_menu.dart';
export 'image/image_selection_menu.dart';
export 'actions/option_action.dart';
export 'actions/block_action_list.dart';
@@ -1,5 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg, Log;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -57,6 +59,7 @@ class EditorStyleCustomizer {
),
),
),
textSpanDecorator: customizeAttributeDecorator,
);
}
@@ -124,6 +127,17 @@ class EditorStyleCustomizer {
);
}
TextStyle outlineBlockPlaceholderStyleBuilder() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return TextStyle(
fontFamily: 'poppins',
fontSize: fontSize,
height: 1.5,
color: theme.colorScheme.onBackground.withOpacity(0.6),
);
}
SelectionMenuStyle selectionMenuStyleBuilder() {
final theme = Theme.of(context);
return SelectionMenuStyle(
@@ -142,4 +156,28 @@ class EditorStyleCustomizer {
backgroundColor: theme.colorScheme.onTertiary,
);
}
InlineSpan customizeAttributeDecorator(
TextInsert textInsert,
TextSpan textSpan,
) {
final attributes = textInsert.attributes;
if (attributes == null) {
return textSpan;
}
final mention = attributes[MentionBlockKeys.mention] as Map?;
if (mention != null) {
final type = mention[MentionBlockKeys.type];
if (type == MentionType.page.name) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: MentionBlock(
key: ValueKey(mention[MentionBlockKeys.pageId]),
mention: mention,
),
);
}
}
return textSpan;
}
}
@@ -25,6 +25,11 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
void fetch() async {
final prefs = await SharedPreferences.getInstance();
final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 16.0;
if (isClosed) {
return;
}
emit(
state.copyWith(
fontSize: fontSize,
@@ -35,6 +40,11 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
void syncFontSize(double fontSize) async {
final prefs = await SharedPreferences.getInstance();
prefs.setDouble(_kDocumentAppearanceFontSize, fontSize);
if (isClosed) {
return;
}
emit(
state.copyWith(
fontSize: fontSize,
@@ -118,7 +118,8 @@ class ShareActionListState extends State<ShareActionList> {
case ShareAction.markdown:
final exportPath = await getIt<FilePickerService>().saveFile(
dialogTitle: '',
fileName: '$name.md',
// encode the file name in case it contains special characters
fileName: '${Uri.encodeComponent(name)}.md',
);
if (exportPath != null) {
docShareBloc.add(DocShareEvent.shareMarkdown(exportPath));
@@ -94,13 +94,21 @@ extension ViewExtension on ViewPB {
}
String get iconName {
switch (layout) {
return layout.iconName;
}
}
extension ViewLayoutExtension on ViewLayoutPB {
String get iconName {
switch (this) {
case ViewLayoutPB.Grid:
return 'editor/grid';
case ViewLayoutPB.Board:
return 'editor/board';
case ViewLayoutPB.Calendar:
return 'editor/calendar';
case ViewLayoutPB.Document:
return 'editor/documents';
default:
throw Exception('Unknown layout type');
}
@@ -154,8 +154,19 @@ class ViewBackendService {
return FolderEventMoveView(payload).send();
}
Future<List<(ViewPB, List<ViewPB>)>> fetchViewsWithLayoutType(
ViewLayoutPB? layoutType,
) async {
return fetchViews((workspace, view) {
if (layoutType != null) {
return view.layout == layoutType;
}
return true;
});
}
Future<List<(ViewPB, List<ViewPB>)>> fetchViews(
ViewLayoutPB layoutType,
bool Function(WorkspaceSettingPB workspace, ViewPB view) filter,
) async {
final result = <(ViewPB, List<ViewPB>)>[];
return FolderEventGetCurrentWorkspace().send().then((value) async {
@@ -166,7 +177,7 @@ class ViewBackendService {
final childViews = await getChildViews(viewId: view.id).then(
(value) => value
.getLeftOrNull<List<ViewPB>>()
?.where((e) => e.layout == layoutType)
?.where((e) => filter(workspaces, e))
.toList(),
);
if (childViews != null && childViews.isNotEmpty) {
@@ -87,6 +87,7 @@ class MenuAppHeader extends StatelessWidget {
case AppDisclosureAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
autoSelectAllText: true,
value: context.read<AppBloc>().state.view.name,
confirm: (newValue) {
context.read<AppBloc>().add(AppEvent.rename(newValue));
@@ -99,6 +99,7 @@ class ViewSectionItem extends StatelessWidget {
case ViewDisclosureAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.disclosureAction_rename.tr(),
autoSelectAllText: true,
value: blocContext.read<ViewBloc>().state.view.name,
confirm: (newValue) {
blocContext
@@ -12,29 +12,40 @@ export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
class NavigatorTextFieldDialog extends StatefulWidget {
const NavigatorTextFieldDialog({
super.key,
required this.title,
this.autoSelectAllText = false,
required this.value,
required this.confirm,
this.cancel,
});
final String value;
final String title;
final void Function()? cancel;
final void Function(String) confirm;
const NavigatorTextFieldDialog({
required this.title,
required this.value,
required this.confirm,
this.cancel,
Key? key,
}) : super(key: key);
final bool autoSelectAllText;
@override
State<NavigatorTextFieldDialog> createState() => _CreateTextFieldDialog();
State<NavigatorTextFieldDialog> createState() =>
_NavigatorTextFieldDialogState();
}
class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
class _NavigatorTextFieldDialogState extends State<NavigatorTextFieldDialog> {
String newValue = "";
final controller = TextEditingController();
@override
void initState() {
newValue = widget.value;
controller.text = newValue;
if (widget.autoSelectAllText) {
controller.selection = TextSelection(
baseOffset: 0,
extentOffset: newValue.length,
);
}
super.initState();
}
@@ -52,7 +63,7 @@ class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
FlowyFormTextInput(
textAlign: TextAlign.center,
hintText: LocaleKeys.dialogCreatePageNameHint.tr(),
initialValue: widget.value,
controller: controller,
textStyle: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: FontSizes.s16,
),
@@ -3,6 +3,7 @@ import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -78,6 +79,11 @@ class BubbleActionList extends StatelessWidget {
"https://appflowy.gitbook.io/docs/essential-documentation/markdown",
);
break;
case BubbleAction.github:
_launchURL(
'https://github.com/AppFlowy-IO/AppFlowy/issues/new/choose',
);
break;
}
}
@@ -169,14 +175,14 @@ class FlowyVersionDescription extends CustomActionCell {
}
}
enum BubbleAction { whatsNews, help, debug, shortcuts, markdown }
enum BubbleAction { whatsNews, help, debug, shortcuts, markdown, github }
class BubbleActionWrapper extends ActionCell {
final BubbleAction inner;
BubbleActionWrapper(this.inner);
@override
Widget? leftIcon(Color iconColor) => FlowyText.regular(inner.emoji);
Widget? leftIcon(Color iconColor) => inner.emoji;
@override
String get name => inner.name;
@@ -195,21 +201,31 @@ extension QuestionBubbleExtension on BubbleAction {
return LocaleKeys.questionBubble_shortcuts.tr();
case BubbleAction.markdown:
return LocaleKeys.questionBubble_markdown.tr();
case BubbleAction.github:
return LocaleKeys.questionBubble_feedback.tr();
}
}
String get emoji {
Widget get emoji {
switch (this) {
case BubbleAction.whatsNews:
return '🆕';
return const FlowyText.regular('🆕');
case BubbleAction.help:
return '👥';
return const FlowyText.regular('👥');
case BubbleAction.debug:
return '🐛';
return const FlowyText.regular('🐛');
case BubbleAction.shortcuts:
return '📋';
return const FlowyText.regular('📋');
case BubbleAction.markdown:
return '✨';
return const FlowyText.regular('✨');
case BubbleAction.github:
return Padding(
padding: const EdgeInsets.all(3.0),
child: svgWidget(
'login/github-mark',
size: const Size.square(12),
),
);
}
}
}
@@ -78,10 +78,10 @@ class FlowyIconButton extends StatelessWidget {
onPressed: onPressed,
child: FlowyHover(
style: HoverStyle(
hoverColor: hoverColor,
hoverColor: Colors.transparent,
foregroundColorOnHover:
iconColorOnHover ?? Theme.of(context).iconTheme.color,
backgroundColor: fillColor ?? Colors.transparent,
backgroundColor: Colors.transparent,
),
child: Padding(
padding: iconPadding,
@@ -9,12 +9,14 @@ class FlowyErrorPage extends StatelessWidget {
Error e, {
required String howToFix,
Key? key,
List<Widget>? actions,
}) =>
FlowyErrorPage._(
e.toString(),
stackTrace: e.stackTrace?.toString(),
howToFix: howToFix,
key: key,
actions: actions,
);
factory FlowyErrorPage.message(
@@ -22,12 +24,14 @@ class FlowyErrorPage extends StatelessWidget {
required String howToFix,
String? stackTrace,
Key? key,
List<Widget>? actions,
}) =>
FlowyErrorPage._(
message,
key: key,
stackTrace: stackTrace,
howToFix: howToFix,
actions: actions,
);
factory FlowyErrorPage.exception(
@@ -35,12 +39,14 @@ class FlowyErrorPage extends StatelessWidget {
required String howToFix,
String? stackTrace,
Key? key,
List<Widget>? actions,
}) =>
FlowyErrorPage._(
e.toString(),
stackTrace: stackTrace,
key: key,
howToFix: howToFix,
actions: actions,
);
const FlowyErrorPage._(
@@ -48,14 +54,16 @@ class FlowyErrorPage extends StatelessWidget {
required this.howToFix,
this.stackTrace,
super.key,
this.actions,
});
static const _titleFontSize = 24.0;
static const _titleToMessagePadding = 8.0;
final List<Widget>? actions;
final String howToFix;
final String message;
final String? stackTrace;
final String howToFix;
@override
Widget build(BuildContext context) {
@@ -87,6 +95,11 @@ class FlowyErrorPage extends StatelessWidget {
height: _titleToMessagePadding,
),
if (stackTrace != null) StackTracePreview(stackTrace!),
if (actions != null)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: actions!,
),
],
),
);
+2 -2
View File
@@ -53,8 +53,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "4f83b6f"
resolved-ref: "4f83b6feb92963f104f3f1f77a473a06aa4870f5"
ref: "572a174"
resolved-ref: "572a174892267e2f78f9c3d7f1fe4ca71c9be0db"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "1.0.4"
+2 -2
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# 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: 0.2.3
version: 0.2.5
environment:
sdk: ">=3.0.0 <4.0.0"
@@ -46,7 +46,7 @@ dependencies:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: 4f83b6f
ref: 572a174
appflowy_popover:
path: packages/appflowy_popover
@@ -8,6 +8,16 @@
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command)
{
HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"AppFlowyMutex");
HWND handle = FindWindowA(NULL, "AppFlowy");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
GetWindowPlacement(handle, &place);
ShowWindow(handle, SW_NORMAL);
return 0;
}
// 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())
@@ -43,5 +53,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
}
::CoUninitialize();
ReleaseMutex(hMutexInstance);
return EXIT_SUCCESS;
}
+5
View File
@@ -27,12 +27,14 @@
"@tauri-apps/api": "^1.2.0",
"dayjs": "^1.11.7",
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.2.1",
"events": "^3.3.0",
"google-protobuf": "^3.21.2",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
"is-hotkey": "^0.2.0",
"jest": "^29.5.0",
"katex": "^0.16.7",
"nanoid": "^4.0.0",
"prismjs": "^1.29.0",
"protoc-gen-ts": "^0.8.5",
@@ -44,6 +46,7 @@
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"react-i18next": "^12.2.0",
"react-katex": "^3.0.1",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.0",
"react18-input-otp": "^1.1.2",
@@ -60,12 +63,14 @@
"@tauri-apps/cli": "^1.2.2",
"@types/google-protobuf": "^3.15.6",
"@types/is-hotkey": "^0.1.7",
"@types/katex": "^0.16.0",
"@types/node": "^18.7.10",
"@types/prismjs": "^1.26.0",
"@types/quill": "^2.0.10",
"@types/react": "^18.0.15",
"@types/react-beautiful-dnd": "^13.1.3",
"@types/react-dom": "^18.0.6",
"@types/react-katex": "^3.0.0",
"@types/utf8": "^3.0.1",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.51.0",
+52
View File
@@ -37,6 +37,9 @@ dependencies:
emoji-mart:
specifier: ^5.5.2
version: 5.5.2
emoji-regex:
specifier: ^10.2.1
version: 10.2.1
events:
specifier: ^3.3.0
version: 3.3.0
@@ -55,6 +58,9 @@ dependencies:
jest:
specifier: ^29.5.0
version: 29.5.0(@types/node@18.16.9)
katex:
specifier: ^0.16.7
version: 0.16.7
nanoid:
specifier: ^4.0.0
version: 4.0.2
@@ -88,6 +94,9 @@ dependencies:
react-i18next:
specifier: ^12.2.0
version: 12.2.2(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0)
react-katex:
specifier: ^3.0.1
version: 3.0.1(prop-types@15.8.1)(react@18.2.0)
react-redux:
specifier: ^8.0.5
version: 8.0.5(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
@@ -132,6 +141,9 @@ devDependencies:
'@types/is-hotkey':
specifier: ^0.1.7
version: 0.1.7
'@types/katex':
specifier: ^0.16.0
version: 0.16.0
'@types/node':
specifier: ^18.7.10
version: 18.16.9
@@ -150,6 +162,9 @@ devDependencies:
'@types/react-dom':
specifier: ^18.0.6
version: 18.2.4
'@types/react-katex':
specifier: ^3.0.0
version: 3.0.0
'@types/utf8':
specifier: ^3.0.1
version: 3.0.1
@@ -1632,6 +1647,10 @@ packages:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
/@types/katex@0.16.0:
resolution: {integrity: sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==}
dev: true
/@types/lodash.memoize@4.1.7:
resolution: {integrity: sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==}
dependencies:
@@ -1684,6 +1703,12 @@ packages:
'@types/react': 17.0.59
dev: false
/@types/react-katex@3.0.0:
resolution: {integrity: sha512-AiHHXh71a2M7Z6z1wj6iA23SkiRF9r0neHUdu8zjU/cT3MyLxDefYHbcceKhV/gjDEZgF3YaiNHyPNtoGUjPvg==}
dependencies:
'@types/react': 18.2.6
dev: true
/@types/react-redux@7.1.25:
resolution: {integrity: sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==}
dependencies:
@@ -2280,6 +2305,11 @@ packages:
engines: {node: '>= 6'}
dev: true
/commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
dev: false
/compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
dev: false
@@ -2437,6 +2467,10 @@ packages:
resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==}
dev: false
/emoji-regex@10.2.1:
resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==}
dev: false
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: false
@@ -3799,6 +3833,13 @@ packages:
object.assign: 4.1.4
dev: true
/katex@0.16.7:
resolution: {integrity: sha512-Xk9C6oGKRwJTfqfIbtr0Kes9OSv6IFsuhFGc7tW4urlpMJtuh+7YhzU6YEG9n8gmWKcMAFzkp7nr+r69kV0zrA==}
hasBin: true
dependencies:
commander: 8.3.0
dev: false
/kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
@@ -4483,6 +4524,17 @@ packages:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: false
/react-katex@3.0.1(prop-types@15.8.1)(react@18.2.0):
resolution: {integrity: sha512-wIUW1fU5dHlkKvq4POfDkHruQsYp3fM8xNb/jnc8dnQ+nNCnaj0sx5pw7E6UyuEdLRyFKK0HZjmXBo+AtXXy0A==}
peerDependencies:
prop-types: ^15.8.1
react: '>=15.3.2 <=18'
dependencies:
katex: 0.16.7
prop-types: 15.8.1
react: 18.2.0
dev: false
/react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
peerDependencies:
+160 -150
View File
@@ -105,7 +105,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "appflowy-integrate"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"collab",
@@ -145,9 +145,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8868f09ff8cea88b079da74ae569d9b8c62a23c68c746240b704ee6f7525c89c"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "async-stream"
@@ -168,7 +168,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -179,7 +179,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -203,7 +203,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -600,7 +600,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -827,7 +827,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
dependencies = [
"glib-sys",
"libc",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -837,7 +837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838"
dependencies = [
"serde",
"toml 0.7.4",
"toml 0.7.5",
]
[[package]]
@@ -886,9 +886,9 @@ dependencies = [
[[package]]
name = "cfg-expr"
version = "0.15.2"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70d3ad08698a0568b0562f22710fe6bfc1f4a61a367c77d0398c562eadd453a"
checksum = "215c0072ecc28f92eeb0eea38ba63ddfcb65c2828c46311d646f1a3ff5f9841c"
dependencies = [
"smallvec",
"target-lexicon",
@@ -935,7 +935,7 @@ checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552"
dependencies = [
"chrono",
"chrono-tz-build 0.1.0",
"phf 0.11.1",
"phf 0.11.2",
]
[[package]]
@@ -956,8 +956,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
dependencies = [
"parse-zoneinfo",
"phf 0.11.1",
"phf_codegen 0.11.1",
"phf 0.11.2",
"phf_codegen 0.11.2",
]
[[package]]
@@ -1030,7 +1030,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"bytes",
@@ -1048,7 +1048,7 @@ dependencies = [
[[package]]
name = "collab-client-ws"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"bytes",
"collab-sync",
@@ -1066,7 +1066,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"async-trait",
@@ -1092,7 +1092,7 @@ dependencies = [
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"proc-macro2",
"quote",
@@ -1104,7 +1104,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"collab",
@@ -1122,7 +1122,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"chrono",
@@ -1142,7 +1142,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"bincode",
"chrono",
@@ -1162,7 +1162,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"async-trait",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]]
name = "collab-sync"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"bytes",
"collab",
@@ -1292,21 +1292,20 @@ dependencies = [
[[package]]
name = "core-graphics-types"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b"
checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33"
dependencies = [
"bitflags",
"core-foundation",
"foreign-types",
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c"
dependencies = [
"libc",
]
@@ -1397,7 +1396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -1452,7 +1451,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -1463,7 +1462,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [
"darling_core",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -1629,7 +1628,7 @@ checksum = "80663502655af01a2902dff3f06869330782267924bf1788410b74edcd93770a"
dependencies = [
"cc",
"rustc_version",
"toml 0.7.4",
"toml 0.7.5",
"vswhom",
"winreg 0.11.0",
]
@@ -1655,6 +1654,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "equivalent"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
[[package]]
name = "errno"
version = "0.3.1"
@@ -1868,7 +1873,7 @@ dependencies = [
"flowy-notification",
"flowy-task",
"futures",
"indexmap",
"indexmap 1.9.3",
"lazy_static",
"lib-dispatch",
"lib-infra",
@@ -1916,7 +1921,7 @@ dependencies = [
"flowy-derive",
"flowy-error",
"flowy-notification",
"indexmap",
"indexmap 1.9.3",
"lib-dispatch",
"nanoid",
"parking_lot 0.12.1",
@@ -2213,7 +2218,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -2294,7 +2299,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -2311,7 +2316,7 @@ dependencies = [
"libc",
"pango-sys",
"pkg-config",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -2325,7 +2330,7 @@ dependencies = [
"gobject-sys",
"libc",
"pkg-config",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -2337,15 +2342,15 @@ dependencies = [
"gdk-sys",
"glib-sys",
"libc",
"system-deps 6.1.0",
"system-deps 6.1.1",
"x11",
]
[[package]]
name = "generator"
version = "0.7.4"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3e123d9ae7c02966b4d892e550bdc32164f05853cd40ab570650ad600596a8a"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
@@ -2400,9 +2405,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.27.2"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]]
name = "gio"
@@ -2430,7 +2435,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps 6.1.0",
"system-deps 6.1.1",
"winapi",
]
@@ -2476,7 +2481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
dependencies = [
"libc",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -2517,7 +2522,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
dependencies = [
"glib-sys",
"libc",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -2558,7 +2563,7 @@ dependencies = [
"gobject-sys",
"libc",
"pango-sys",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -2577,9 +2582,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782"
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
dependencies = [
"bytes",
"fnv",
@@ -2587,7 +2592,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@@ -2612,6 +2617,12 @@ dependencies = [
"ahash 0.8.3",
]
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "heck"
version = "0.3.3"
@@ -2732,9 +2743,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "0.14.26"
version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
"bytes",
"futures-channel",
@@ -2834,17 +2845,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.4.0"
@@ -2896,6 +2896,16 @@ dependencies = [
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
name = "infer"
version = "0.12.0"
@@ -2927,9 +2937,9 @@ dependencies = [
[[package]]
name = "ipnet"
version = "2.7.2"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "itertools"
@@ -3145,9 +3155,9 @@ dependencies = [
[[package]]
name = "lib0"
version = "0.16.5"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf23122cb1c970b77ea6030eac5e328669415b65d2ab245c99bfb110f9d62dc"
checksum = "49d27ae71668a38ad135d463703ce0c5d9cf5a29f9a02add7a0dac6ebb523196"
dependencies = [
"serde",
"serde_json",
@@ -3156,9 +3166,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.146"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libloading"
@@ -3631,9 +3641,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.54"
version = "0.10.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [
"bitflags",
"cfg-if",
@@ -3652,7 +3662,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -3663,9 +3673,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.88"
version = "0.9.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
dependencies = [
"cc",
"libc",
@@ -3717,7 +3727,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -3797,9 +3807,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pest"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70"
checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9"
dependencies = [
"thiserror",
"ucd-trie",
@@ -3807,9 +3817,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb"
checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b"
dependencies = [
"pest",
"pest_generator",
@@ -3817,22 +3827,22 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e"
checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
name = "pest_meta"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411"
checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0"
dependencies = [
"once_cell",
"pest",
@@ -3863,11 +3873,11 @@ dependencies = [
[[package]]
name = "phf"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_shared 0.11.1",
"phf_shared 0.11.2",
]
[[package]]
@@ -3892,12 +3902,12 @@ dependencies = [
[[package]]
name = "phf_codegen"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
"phf_generator 0.11.1",
"phf_shared 0.11.1",
"phf_generator 0.11.2",
"phf_shared 0.11.2",
]
[[package]]
@@ -3922,11 +3932,11 @@ dependencies = [
[[package]]
name = "phf_generator"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared 0.11.1",
"phf_shared 0.11.2",
"rand 0.8.5",
]
@@ -3979,9 +3989,9 @@ dependencies = [
[[package]]
name = "phf_shared"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher",
]
@@ -4003,7 +4013,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -4031,7 +4041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [
"base64 0.21.2",
"indexmap",
"indexmap 1.9.3",
"line-wrap",
"quick-xml",
"serde",
@@ -4074,12 +4084,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.6"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1"
checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282"
dependencies = [
"proc-macro2",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -4133,9 +4143,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.60"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
dependencies = [
"unicode-ident",
]
@@ -4582,11 +4592,11 @@ dependencies = [
[[package]]
name = "rust_decimal"
version = "1.29.1"
version = "1.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bd36b60561ee1fb5ec2817f198b6fd09fa571c897a5e86d1487cfc2b096dfc"
checksum = "d0446843641c69436765a35a5a77088e28c2e6a12da93e84aa3ab1cd4aa5a042"
dependencies = [
"arrayvec 0.7.3",
"arrayvec 0.7.4",
"borsh",
"bytecheck",
"byteorder",
@@ -4600,9 +4610,9 @@ dependencies = [
[[package]]
name = "rust_decimal_macros"
version = "1.29.1"
version = "1.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e773fd3da1ed42472fdf3cfdb4972948a555bc3d73f5e0bdb99d17e7b54c687"
checksum = "7ca5c398d85f83b9a44de754a2048625a8c5eafcf070da7b8f116b685e2f6608"
dependencies = [
"quote",
"rust_decimal",
@@ -4861,14 +4871,14 @@ checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
name = "serde_json"
version = "1.0.96"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3"
dependencies = [
"itoa 1.0.6",
"ryu",
@@ -4883,14 +4893,14 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
name = "serde_spanned"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
@@ -4916,7 +4926,7 @@ dependencies = [
"base64 0.21.2",
"chrono",
"hex",
"indexmap",
"indexmap 1.9.3",
"serde",
"serde_json",
"serde_with_macros",
@@ -4932,7 +4942,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -4999,9 +5009,9 @@ checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sha2"
version = "0.10.6"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -5245,9 +5255,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.18"
version = "2.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616"
dependencies = [
"proc-macro2",
"quote",
@@ -5269,14 +5279,14 @@ dependencies = [
[[package]]
name = "system-deps"
version = "6.1.0"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2"
checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3"
dependencies = [
"cfg-expr 0.15.2",
"cfg-expr 0.15.3",
"heck 0.4.1",
"pkg-config",
"toml 0.7.4",
"toml 0.7.5",
"version-compare 0.1.1",
]
@@ -5357,15 +5367,15 @@ dependencies = [
[[package]]
name = "target-lexicon"
version = "0.12.7"
version = "0.12.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5"
checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac"
[[package]]
name = "tauri"
version = "1.4.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc35893c7e08d9564a9206bd52182dce031b0d5132dc946b3e166e00d03f8cfe"
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
dependencies = [
"anyhow",
"cocoa",
@@ -5542,7 +5552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb"
dependencies = [
"embed-resource",
"toml 0.7.4",
"toml 0.7.5",
]
[[package]]
@@ -5626,7 +5636,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -5729,7 +5739,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -5836,9 +5846,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.4"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec"
checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240"
dependencies = [
"serde",
"serde_spanned",
@@ -5848,20 +5858,20 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.10"
version = "0.19.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739"
checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
dependencies = [
"indexmap",
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -5922,13 +5932,13 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.24"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
]
[[package]]
@@ -6189,7 +6199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
dependencies = [
"form_urlencoded",
"idna 0.4.0",
"idna",
"percent-encoding",
"serde",
]
@@ -6218,11 +6228,11 @@ dependencies = [
[[package]]
name = "validator"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591"
checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd"
dependencies = [
"idna 0.2.3",
"idna",
"lazy_static",
"regex",
"serde",
@@ -6345,7 +6355,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
"wasm-bindgen-shared",
]
@@ -6379,7 +6389,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.22",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -6444,7 +6454,7 @@ dependencies = [
"pango-sys",
"pkg-config",
"soup2-sys",
"system-deps 6.1.0",
"system-deps 6.1.1",
]
[[package]]
@@ -6887,9 +6897,9 @@ dependencies = [
[[package]]
name = "yrs"
version = "0.16.5"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2aef2bf89b4f7c003f9c73f1c8097427ca32e1d006443f3f607f11e79a797b"
checksum = "04e5192da97bd1621497ddf66b42475fb0cc44b6ebcf64510f0d3827c0b15cad"
dependencies = [
"atomic_refcell",
"lib0",
+6 -6
View File
@@ -34,12 +34,12 @@ default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io]
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" }
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" }
#collab = { path = "../../AppFlowy-Collab/collab" }
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
@@ -201,7 +201,7 @@ export const EditRow = ({
}}
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white `}
>
<div onClick={() => onCloseClick()} className={'absolute top-1 right-1'}>
<div onClick={() => onCloseClick()} className={'absolute right-1 top-1'}>
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
<CloseSvg></CloseSvg>
</button>
@@ -209,7 +209,7 @@ export const EditRow = ({
<div className={'flex h-full'}>
<div className={'flex h-full flex-1 flex-col border-r border-shade-6 pb-4 pt-6'}>
<div className={'pl-12 pb-4'}>
<div className={'pb-4 pl-12'}>
<button className={'flex items-center gap-2 p-4'}>
<i className={'h-5 w-5'}>
<ImageSvg></ImageSvg>
@@ -229,7 +229,9 @@ export const EditRow = ({
}`}
>
{cells
.filter((cell) => databaseStore.fields[cell.cellIdentifier.fieldId].visible)
.filter((cell) => {
return databaseStore.fields[cell.cellIdentifier.fieldId]?.visible;
})
.map((cell, cellIndex) => (
<EditCellWrapper
index={cellIndex}
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DatabaseController } from '$app/stores/effects/database/database_controller';
import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '$app/stores/reducers/database/slice';
import { useAppDispatch } from '$app/stores/store';
@@ -8,6 +8,7 @@ import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { ViewLayoutPB } from '@/services/backend';
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
import { OnDragEndResponder } from 'react-beautiful-dnd';
import { AsyncQueue } from '$app/utils/async_queue';
export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
const dispatch = useAppDispatch();
@@ -24,25 +25,30 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
return () => void c.dispose();
}, [viewId]);
const loadFields = async (fieldInfos: readonly FieldInfo[]) => {
const fields: DatabaseFieldMap = {};
const columns: IDatabaseColumn[] = [];
const loadFields = useCallback(
async (fieldInfos: readonly FieldInfo[]) => {
const fields: DatabaseFieldMap = {};
const columns: IDatabaseColumn[] = [];
for (const fieldInfo of fieldInfos) {
const fieldPB = fieldInfo.field;
columns.push({
fieldId: fieldPB.id,
sort: 'none',
visible: fieldPB.visibility,
});
for (const fieldInfo of fieldInfos) {
const fieldPB = fieldInfo.field;
columns.push({
fieldId: fieldPB.id,
sort: 'none',
visible: fieldPB.visibility,
});
const field = await loadField(viewId, fieldInfo, dispatch);
fields[field.fieldId] = field;
}
dispatch(databaseActions.updateFields({ fields }));
dispatch(databaseActions.updateColumns({ columns }));
},
[viewId, dispatch]
);
const field = await loadField(viewId, fieldInfo, dispatch);
fields[field.fieldId] = field;
}
dispatch(databaseActions.updateFields({ fields }));
dispatch(databaseActions.updateColumns({ columns }));
};
const queue = useMemo(() => {
return new AsyncQueue<readonly FieldInfo[]>(loadFields);
}, [loadFields]);
useEffect(() => {
void (async () => {
@@ -53,7 +59,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
setRows([...rowInfos]);
},
onFieldsChanged: (fieldInfos) => {
void loadFields(fieldInfos);
queue.enqueue(fieldInfos);
},
});
@@ -76,7 +82,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
return () => {
void controller?.dispose();
};
}, [controller]);
}, [controller, queue]);
const onNewRowClick = async (index: number) => {
if (!groups) return;
@@ -95,7 +101,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
if (source.droppableId === destination?.droppableId) {
// move inside the block (group)
await controller.exchangeRow(
await controller.exchangeGroupRow(
group.rows[source.index].id,
destination.droppableId,
group.rows[destination.index].id
@@ -103,7 +109,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
} else {
// move to different block (group)
if (!destination?.droppableId) return;
await controller.moveRow(group.rows[source.index].id, destination.droppableId);
await controller.moveGroupRow(group.rows[source.index].id, destination.droppableId);
}
};
@@ -7,7 +7,6 @@ import { Draggable } from 'react-beautiful-dnd';
import { MouseEventHandler, useState } from 'react';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
import { useTranslation } from 'react-i18next';
import { useAppSelector } from '$app/stores/store';
@@ -52,8 +51,7 @@ export const BoardCard = ({
const onDeleteRowClick = async () => {
setShowCardPopup(false);
const svc = new RowBackendService(viewId);
await svc.deleteRow(rowInfo.row.id);
await controller.deleteRow(rowInfo.row.id);
};
return (
@@ -73,7 +71,7 @@ export const BoardCard = ({
<div className={'flex flex-col gap-3'}>
{cells
.filter(
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId].visible
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId]?.visible
)
.map((cell, cellIndex) => (
<BoardCell
@@ -11,8 +11,13 @@ import {
import { useRangeKeyDown } from '$app/components/document/BlockSelection/RangeKeyDown.hooks';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { useSubscribeRanges } from '$app/components/document/_shared/SubscribeSelection.hooks';
import { isApple } from '$app/utils/env';
const onFrameTime = 1000 / 60;
export function useBlockRangeSelection(container: HTMLDivElement) {
const timeStampRef = useRef(0);
const dispatch = useAppDispatch();
const onKeyDown = useRangeKeyDown();
const { docId } = useSubscribeDocument();
@@ -36,10 +41,12 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
useEffect(() => {
if (!range) return;
const { anchor, focus } = range;
if (!anchor || !focus) {
container.classList.remove('caret-transparent');
return;
}
// if the focus block is different from the anchor block, we need to set the caret transparent
if (focus.id !== anchor.id) {
container.classList.add('caret-transparent');
@@ -50,18 +57,21 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
useEffect(() => {
const anchor = anchorRef.current;
if (!anchor || !focus) return;
const selection = window.getSelection();
if (!selection) return;
// update focus point
dispatch(
rangeActions.setFocusPoint({
...focus,
focusPoint: focus,
docId,
})
);
const focused = isFocused(focus.id);
// if the focus block is not focused, we need to set the cursor position
if (!focused) {
// if the focus block is the same as the anchor block, we just update the anchor's range
@@ -70,14 +80,17 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
anchor.point.x - container.scrollLeft,
anchor.point.y - container.scrollTop
);
if (!range) return;
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
return;
}
const node = getNodeTextBoxByBlockId(focus.id);
if (!node) return;
// if the selection is forward, we set the cursor position to the start of the focus block
if (isForward) {
@@ -89,15 +102,33 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
}
}, [container, dispatch, docId, focus, isForward]);
const handleDragStart = useCallback(
const handleDragEnd = useCallback(() => {
timeStampRef.current = Date.now();
if (!isDragging) return;
setFocus(null);
anchorRef.current = null;
dispatch(
rangeActions.setDragging({
isDragging: false,
docId,
})
);
}, [docId, dispatch, isDragging]);
const handleMouseDown = useCallback(
(e: MouseEvent) => {
setForward(true);
const isTapToClick = isApple() && timeStampRef.current > 0 && Date.now() - timeStampRef.current < onFrameTime;
// skip if the target is not a block
const blockId = getBlockIdByPoint(e.target as HTMLElement);
if (!blockId) {
dispatch(rangeActions.initialState(docId));
return;
}
setForward(true);
dispatch(rangeActions.clearRanges({ docId, exclude: blockId }));
const startX = e.clientX + container.scrollLeft;
const startY = e.clientY + container.scrollTop;
@@ -113,18 +144,25 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
anchorRef.current = {
...anchor,
};
// set the anchor point and focus point
dispatch(rangeActions.setAnchorPoint({ ...anchor, docId }));
dispatch(rangeActions.setFocusPoint({ ...anchor, docId }));
dispatch(rangeActions.setAnchorPoint({ docId, anchorPoint: anchor }));
dispatch(rangeActions.setFocusPoint({ docId, focusPoint: anchor }));
// This is a workaround for a bug in Safari where the mouseup event is not fired
if (isTapToClick) {
handleDragEnd();
return;
}
dispatch(
rangeActions.setDragging({
isDragging: true,
docId,
})
);
return;
},
[container.scrollLeft, container.scrollTop, dispatch, docId]
[container.scrollLeft, container.scrollTop, dispatch, docId, handleDragEnd]
);
const handleDraging = useCallback(
@@ -133,12 +171,14 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
// skip if the target is not a block
const blockId = getBlockIdByPoint(e.target as HTMLElement);
if (!blockId) {
return;
}
const endX = e.clientX + container.scrollLeft;
const endY = e.clientY + container.scrollTop;
// set the focus point
setFocus({
id: blockId,
@@ -149,42 +189,35 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
});
// set forward
const anchorId = anchorRef.current.id;
if (anchorId === blockId) {
const startX = anchorRef.current.point.x;
setForward(startX < endX);
return;
}
const startY = anchorRef.current.point.y;
setForward(startY < endY);
},
[container.scrollLeft, container.scrollTop, isDragging]
);
const handleDragEnd = useCallback(() => {
if (!isDragging) return;
setFocus(null);
anchorRef.current = null;
dispatch(
rangeActions.setDragging({
isDragging: false,
docId,
})
);
}, [docId, dispatch, isDragging]);
useEffect(() => {
document.addEventListener('mousedown', handleDragStart);
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleDraging);
document.addEventListener('mouseup', handleDragEnd);
container.addEventListener('keydown', onKeyDown, true);
return () => {
document.removeEventListener('mousedown', handleDragStart);
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mousemove', handleDraging);
document.removeEventListener('mouseup', handleDragEnd);
container.removeEventListener('keydown', onKeyDown, true);
};
}, [handleDragStart, handleDragEnd, handleDraging, container, onKeyDown]);
}, [handleMouseDown, handleDragEnd, handleDraging, container, onKeyDown]);
return null;
}
@@ -6,6 +6,8 @@ import BlockMenuTurnInto from '$app/components/document/BlockSideToolbar/BlockMe
import TextField from '@mui/material/TextField';
import { Keyboard } from '$app/constants/document/keyboard';
import { selectOptionByUpDown } from '$app/utils/document/menu';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
import { BlockType } from '$app/interfaces/document';
enum BlockMenuOption {
Duplicate = 'Duplicate',
@@ -22,6 +24,7 @@ interface Option {
function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
const { handleDelete, handleDuplicate } = useBlockMenu(id);
const { node } = useSubscribeNode(id);
const [subMenuOpened, setSubMenuOpened] = useState(false);
const [hovered, setHovered] = useState<BlockMenuOption | null>(null);
@@ -39,29 +42,36 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
[onClose]
);
const excludeTurnIntoBlock = useMemo(() => {
return [BlockType.DividerBlock].includes(node.type);
}, [node.type]);
const options: Option[] = useMemo(
() => [
{
operate: () => {
return handleClick({ operate: handleDelete });
() =>
[
{
operate: () => {
return handleClick({ operate: handleDelete });
},
title: 'Delete',
icon: <Delete />,
key: BlockMenuOption.Delete,
},
title: 'Delete',
icon: <Delete />,
key: BlockMenuOption.Delete,
},
{
operate: () => {
return handleClick({ operate: handleDuplicate });
{
operate: () => {
return handleClick({ operate: handleDuplicate });
},
title: 'Duplicate',
icon: <ContentCopy />,
key: BlockMenuOption.Duplicate,
},
title: 'Duplicate',
icon: <ContentCopy />,
key: BlockMenuOption.Duplicate,
},
{
key: BlockMenuOption.TurnInto,
},
],
[handleClick, handleDelete, handleDuplicate]
excludeTurnIntoBlock
? null
: {
key: BlockMenuOption.TurnInto,
},
].filter((item) => item !== null) as Option[],
[excludeTurnIntoBlock, handleClick, handleDelete, handleDuplicate]
);
const onKeyDown = useCallback(
@@ -131,7 +141,10 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) {
}}
menuOpened={subMenuOpened}
isHovered={hovered === BlockMenuOption.TurnInto}
onClose={() => setSubMenuOpened(false)}
onClose={() => {
setSubMenuOpened(false);
onClose();
}}
id={id}
/>
);
@@ -1,4 +1,4 @@
import React, { MouseEvent, useRef } from 'react';
import React, { MouseEvent, useEffect, useRef } from 'react';
import { ArrowRight, Transform } from '@mui/icons-material';
import MenuItem from '$app/components/document/_shared/MenuItem';
import TurnIntoPopover from '$app/components/document/_shared/TurnInto';
@@ -17,8 +17,22 @@ function BlockMenuTurnInto({
menuOpened: boolean;
}) {
const ref = useRef<HTMLDivElement | null>(null);
const open = isHovered && menuOpened && Boolean(ref.current);
const [anchorPosition, setAnchorPosition] = React.useState<{ top: number; left: number }>();
const open = Boolean(anchorPosition);
useEffect(() => {
if (isHovered && menuOpened) {
const rect = ref.current?.getBoundingClientRect();
if (!rect) return;
setAnchorPosition({
top: rect.top + rect.height / 2,
left: rect.left + rect.width,
});
} else {
setAnchorPosition(undefined);
}
}, [isHovered, menuOpened]);
return (
<>
<MenuItem
@@ -45,11 +59,8 @@ function BlockMenuTurnInto({
},
}}
onClose={onClose}
anchorEl={ref.current}
anchorOrigin={{
vertical: 'center',
horizontal: 'right',
}}
anchorReference={'anchorPosition'}
anchorPosition={anchorPosition}
transformOrigin={{
vertical: 'center',
horizontal: 'left',
@@ -40,6 +40,10 @@ export function useBlockSideToolbar({ container }: { container: HTMLDivElement }
top = headingBlockTopOffset[nodeData.level];
}
if (node.type === BlockType.DividerBlock) {
top = -3;
}
setStyle({
opacity: '1',
pointerEvents: 'auto',
@@ -99,39 +103,47 @@ function getNodeIdByPoint(x: number, y: number) {
: null;
}
const origin: {
anchorOrigin: PopoverOrigin;
transformOrigin: PopoverOrigin;
} = {
anchorOrigin: {
vertical: 'bottom',
horizontal: 'right',
},
transformOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
const transformOrigin: PopoverOrigin = {
vertical: 'bottom',
horizontal: 'left',
};
export function usePopover() {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const [anchorPosition, setAnchorPosition] = React.useState<{
top: number;
left: number;
}>();
const onClose = useCallback(() => {
setAnchorEl(null);
setAnchorPosition(undefined);
}, []);
const handleOpen = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setAnchorEl(e.currentTarget);
const rect = e.currentTarget.getBoundingClientRect();
setAnchorPosition({
top: rect.top + rect.height,
left: rect.left + rect.width,
});
}, []);
const open = Boolean(anchorEl);
const open = Boolean(anchorPosition);
const onMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
}, []);
return {
anchorEl,
anchorPosition,
onClose,
open,
handleOpen,
...origin,
anchorReference: 'anchorPosition' as const,
transformOrigin,
onMouseDown,
disableRestoreFocus: true,
disableAutoFocus: true,
disableEnforceFocus: true,
};
}
@@ -10,6 +10,8 @@ import ToolbarButton from './ToolbarButton';
import { rectSelectionActions } from '$app_reducers/document/slice';
import { addBlockBelowClickThunk } from '$app_reducers/document/async-actions/menu';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { RANGE_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
export default function BlockSideToolbar({ container }: { container: HTMLDivElement }) {
const dispatch = useAppDispatch();
@@ -17,13 +19,10 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
const { nodeId, style, ref } = useBlockSideToolbar({ container });
const isDragging = useAppSelector(
(state) => state.documentRange[docId]?.isDragging || state.documentRectSelection[docId]?.isDragging
(state) => state[RANGE_NAME][docId]?.isDragging || state[RECT_RANGE_NAME][docId]?.isDragging
);
const { handleOpen, ...popoverProps } = usePopover();
// prevent popover from showing when anchorEl is not in DOM
const showPopover = popoverProps.anchorEl ? document.contains(popoverProps.anchorEl) : true;
if (!nodeId || isDragging) return null;
return (
@@ -64,11 +63,12 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
if (!nodeId) return;
dispatch(
rectSelectionActions.setSelectionById({
setRectSelectionThunk({
docId,
blockId: nodeId,
selection: [nodeId],
})
);
handleOpen(e);
}}
>
@@ -77,11 +77,9 @@ export default function BlockSideToolbar({ container }: { container: HTMLDivElem
</div>
</Portal>
{showPopover && (
<Popover {...popoverProps}>
<BlockMenu id={nodeId} onClose={popoverProps.onClose} />
</Popover>
)}
<Popover {...popoverProps}>
<BlockMenu id={nodeId} onClose={popoverProps.onClose} />
</Popover>
</>
);
}
@@ -11,7 +11,10 @@ export function useBlockSlash() {
const { docId } = useSubscribeDocument();
const { blockId, visible, slashText, hoverOption } = useSubscribeSlash();
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const [anchorPosition, setAnchorPosition] = React.useState<{
top: number;
left: number;
}>();
useEffect(() => {
if (blockId && visible) {
@@ -19,11 +22,17 @@ export function useBlockSlash() {
const el = blockEl.querySelector(`[role="textbox"]`) as HTMLElement;
if (el) {
setAnchorEl(el);
const rect = el.getBoundingClientRect();
setAnchorPosition({
top: rect.top + rect.height,
left: rect.left,
});
return;
}
}
setAnchorEl(null);
setAnchorPosition(undefined);
}, [blockId, visible]);
useEffect(() => {
@@ -43,11 +52,11 @@ export function useBlockSlash() {
dispatch(slashCommandActions.closeSlashCommand(docId));
}, [dispatch, docId]);
const open = Boolean(anchorEl);
const open = Boolean(anchorPosition);
return {
open,
anchorEl,
anchorPosition,
onClose,
blockId,
searchText,
@@ -5,18 +5,15 @@ import { useBlockSlash } from '$app/components/document/BlockSlash/index.hooks';
import { Keyboard } from '$app/constants/document/keyboard';
function BlockSlash({ container }: { container: HTMLDivElement }) {
const { blockId, open, onClose, anchorEl, searchText, hoverOption } = useBlockSlash();
const { blockId, open, onClose, anchorPosition, searchText, hoverOption } = useBlockSlash();
if (!blockId) return null;
return (
<Popover
open={open}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
anchorReference={'anchorPosition'}
anchorPosition={anchorPosition}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
@@ -0,0 +1,54 @@
import React from 'react';
import { BlockType, NestedBlock } from '$app/interfaces/document';
import KatexMath from '$app/components/document/_shared/KatexMath';
import Popover from '@mui/material/Popover';
import EquationEditContent from '$app/components/document/_shared/TemporaryInput/EquationEditContent';
import { useEquationBlock } from '$app/components/document/EquationBlock/useEquationBlock';
import { Functions } from '@mui/icons-material';
function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> }) {
const { ref, value, onChange, onOpenPopover, open, anchorPosition, onConfirm, onClosePopover } =
useEquationBlock(node);
const formula = open ? value : node.data.formula;
return (
<>
<div
ref={ref}
onClick={onOpenPopover}
className={'flex min-h-[59px] cursor-pointer items-center justify-center overflow-hidden hover:bg-main-selector'}
>
{formula ? (
<KatexMath latex={formula} />
) : (
<span className={'flex text-shade-2'}>
<Functions />
<span>Add a TeX equation</span>
</span>
)}
</div>
<Popover
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
onMouseDown={(e) => e.stopPropagation()}
onClose={onClosePopover}
open={open}
anchorReference={'anchorPosition'}
anchorPosition={anchorPosition}
>
<EquationEditContent
placeholder={'c = \\pm\\sqrt{a^2 + b^2\\text{ if }a\\neq 0\\text{ or }b\\neq 0}'}
multiline={true}
value={value}
onChange={onChange}
onConfirm={onConfirm}
/>
</Popover>
</>
);
}
export default EquationBlock;
@@ -0,0 +1,71 @@
import { useCallback, useRef, useState } from 'react';
import { BlockType, NestedBlock } from '$app/interfaces/document';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { useAppDispatch } from '$app/stores/store';
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
import { rectSelectionActions } from '$app_reducers/document/slice';
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
export function useEquationBlock(node: NestedBlock<BlockType.EquationBlock>) {
const { controller, docId } = useSubscribeDocument();
const id = node.id;
const dispatch = useAppDispatch();
const formula = node.data.formula;
const ref = useRef<HTMLDivElement>(null);
const [value, setValue] = useState(formula);
const [anchorPosition, setAnchorPosition] = useState<{
top: number;
left: number;
}>();
const open = Boolean(anchorPosition);
const onChange = useCallback((newVal: string) => {
setValue(newVal);
}, []);
const onOpenPopover = useCallback(() => {
setValue(formula);
const rect = ref.current?.getBoundingClientRect();
if (!rect) return;
setAnchorPosition({
top: rect.top + rect.height,
left: rect.left + rect.width / 2,
});
}, [formula]);
const onClosePopover = useCallback(() => {
setAnchorPosition(undefined);
dispatch(
setRectSelectionThunk({
docId,
selection: [id],
})
);
}, [dispatch, id, docId]);
const onConfirm = useCallback(async () => {
await dispatch(
updateNodeDataThunk({
id,
data: {
formula: value,
},
controller,
})
);
onClosePopover();
}, [dispatch, id, value, controller, onClosePopover]);
return {
open,
ref,
value,
onChange,
onOpenPopover,
onClosePopover,
onConfirm,
anchorPosition,
};
}
@@ -17,6 +17,7 @@ import CalloutBlock from '$app/components/document/CalloutBlock';
import BlockOverlay from '$app/components/document/Overlay/BlockOverlay';
import CodeBlock from '$app/components/document/CodeBlock';
import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
import EquationBlock from '$app/components/document/EquationBlock';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
const { node, childIds, isSelected, ref } = useNode(id);
@@ -26,38 +27,50 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
case BlockType.TextBlock: {
return <TextBlock node={node} childIds={childIds} />;
}
case BlockType.HeadingBlock: {
return <HeadingBlock node={node} />;
}
case BlockType.TodoListBlock: {
return <TodoListBlock node={node} childIds={childIds} />;
}
case BlockType.QuoteBlock: {
return <QuoteBlock node={node} childIds={childIds} />;
}
case BlockType.BulletedListBlock: {
return <BulletedListBlock node={node} childIds={childIds} />;
}
case BlockType.NumberedListBlock: {
return <NumberedListBlock node={node} childIds={childIds} />;
}
case BlockType.ToggleListBlock: {
return <ToggleListBlock node={node} childIds={childIds} />;
}
case BlockType.DividerBlock: {
return <DividerBlock />;
}
case BlockType.CalloutBlock: {
return <CalloutBlock node={node} childIds={childIds} />;
}
case BlockType.CodeBlock:
return <CodeBlock node={node} />;
case BlockType.EquationBlock:
return <EquationBlock node={node} />;
default:
return <UnSupportedBlock />;
}
}, [node, childIds]);
const className = props.className ? ` ${props.className}` : '';
if (!node) return null;
return (
@@ -7,6 +7,7 @@ import { useCopy } from '$app/components/document/_shared/CopyPasteHooks/useCopy
import { usePaste } from '$app/components/document/_shared/CopyPasteHooks/usePaste';
import LinkEditPopover from '$app/components/document/_shared/TextLink/LinkEditPopover';
import { useUndoRedo } from '$app/components/document/_shared/UndoHooks/useUndoRedo';
import TemporaryPopover from '$app/components/document/_shared/TemporaryInput/TemporaryPopover';
export default function Overlay({ container }: { container: HTMLDivElement }) {
useCopy(container);
@@ -19,6 +20,7 @@ export default function Overlay({ container }: { container: HTMLDivElement }) {
<BlockSelection container={container} />
<BlockSlash container={container} />
<LinkEditPopover />
<TemporaryPopover />
</>
);
}
@@ -0,0 +1,31 @@
import { TextAction, TextActionMenuProps } from '$app/interfaces/document';
export const defaultTextActionItems = [
TextAction.Turn,
TextAction.Link,
TextAction.Bold,
TextAction.Italic,
TextAction.Underline,
TextAction.Strikethrough,
TextAction.Code,
TextAction.Equation,
];
const groupKeys = {
comment: [],
format: [
TextAction.Bold,
TextAction.Italic,
TextAction.Underline,
TextAction.Strikethrough,
TextAction.Code,
TextAction.Equation,
],
link: [TextAction.Link],
turn: [TextAction.Turn],
};
export const multiLineTextActionProps: TextActionMenuProps = {
customItems: [TextAction.Bold, TextAction.Italic, TextAction.Underline, TextAction.Strikethrough, TextAction.Code],
};
export const multiLineTextActionGroups = [groupKeys.format];
export const textActionGroups = [groupKeys.turn, groupKeys.format, groupKeys.link];
@@ -1,6 +1,5 @@
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { calcToolbarPosition } from '$app/utils/document/toolbar';
import { useAppSelector } from '$app/stores/store';
import { getNode } from '$app/utils/document/node';
import { debounce } from '$app/utils/tool';
import { useSubscribeCaret } from '$app/components/document/_shared/SubscribeSelection.hooks';
@@ -15,9 +14,11 @@ export function useMenuStyle(container: HTMLDivElement) {
const reCalculatePosition = useCallback(() => {
const el = ref.current;
if (!el || !id) return;
const node = getNode(id);
if (!node) return;
const position = calcToolbarPosition(el, node, container);
@@ -50,6 +51,7 @@ export function useMenuStyle(container: HTMLDivElement) {
setIsScrolling(true);
debounceScrollEnd();
};
container.addEventListener('scroll', handleScroll);
return () => {
debounceScrollEnd.cancel();
@@ -1,8 +1,11 @@
import { useMenuStyle } from './index.hooks';
import TextActionMenuList from '$app/components/document/TextActionMenu/menu';
import BlockPortal from '$app/components/document/BlockPortal';
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useSubscribeRanges } from '$app/components/document/_shared/SubscribeSelection.hooks';
import { debounce } from '$app/utils/tool';
import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
const { ref, id } = useMenuStyle(container);
@@ -27,26 +30,57 @@ const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
</BlockPortal>
);
};
const TextActionMenu = ({ container }: { container: HTMLDivElement }) => {
const range = useSubscribeRanges();
const { docId } = useSubscribeDocument();
const [show, setShow] = useState(false);
const debounceShow = useMemo(() => {
return debounce(() => {
setShow(true);
}, 100);
}, []);
const canShow = useMemo(() => {
const { isDragging, focus, anchor, ranges, caret } = range;
// don't show if dragging
if (isDragging) return false;
// don't show if no focus or anchor
if (!caret) return false;
const isSameLine = anchor?.id === focus?.id;
if (!anchor || !focus) return false;
const anchorNode = getBlock(docId, anchor.id);
const focusNode = getBlock(docId, focus.id);
// include document title
if (!anchorNode.parent || !focusNode.parent) return false;
const isSameLine = anchor.id === focus.id;
// show toolbar if range has multiple nodes
if (!isSameLine) return true;
const caretRange = ranges[caret.id];
// don't show if no caret range
const caretRange = ranges?.[caret.id];
if (!caretRange) return false;
// show toolbar if range is not collapsed
return caretRange.length > 0;
}, [range]);
}, [docId, range]);
if (!canShow) return null;
useEffect(() => {
if (!canShow) {
debounceShow.cancel();
setShow(false);
return;
}
debounceShow();
}, [canShow, debounceShow]);
if (!show) return null;
return <TextActionComponent container={container} />;
};
@@ -1,37 +1,36 @@
import IconButton from '@mui/material/IconButton';
import FormatIcon from './FormatIcon';
import React, { useCallback, useEffect, useMemo, useContext } from 'react';
import { TextAction } from '$app/interfaces/document';
import React, { useCallback, useEffect, useMemo } from 'react';
import { TemporaryType, TextAction } from '$app/interfaces/document';
import MenuTooltip from '$app/components/document/TextActionMenu/menu/MenuTooltip';
import { getFormatActiveThunk, toggleFormatThunk } from '$app_reducers/document/async-actions/format';
import { useAppDispatch, useAppSelector } from '$app/stores/store';
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
import { newLinkThunk } from '$app_reducers/document/async-actions/link';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { RANGE_NAME } from '$app/constants/document/name';
import { createTemporary } from '$app_reducers/document/async-actions/temporary';
import {
CodeOutlined,
FormatBold,
FormatItalic,
FormatUnderlined,
Functions,
StrikethroughSOutlined,
} from '@mui/icons-material';
import LinkIcon from '@mui/icons-material/AddLink';
export const iconSize = { width: 18, height: 18 };
const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => {
const dispatch = useAppDispatch();
const { docId, controller } = useSubscribeDocument();
const focusId = useAppSelector((state) => state.documentRange[docId]?.focus?.id || '');
const focusId = useAppSelector((state) => state[RANGE_NAME][docId]?.focus?.id || '');
const { node: focusNode } = useSubscribeNode(focusId);
const [isActive, setIsActive] = React.useState(false);
const color = useMemo(() => (isActive ? '#00BCF0' : 'white'), [isActive]);
const formatTooltips: Record<string, string> = useMemo(
() => ({
[TextAction.Bold]: 'Bold',
[TextAction.Italic]: 'Italic',
[TextAction.Underline]: 'Underline',
[TextAction.Strikethrough]: 'Strike through',
[TextAction.Code]: 'Mark as Code',
[TextAction.Link]: 'Add Link',
}),
[]
);
const isFormatActive = useCallback(async () => {
if (!focusNode) return false;
const { payload: isActive } = await dispatch(
@@ -40,6 +39,7 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
docId,
})
);
return !!isActive;
}, [docId, dispatch, format, focusNode]);
@@ -65,6 +65,34 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
);
}, [dispatch, docId]);
const addTemporaryInput = useCallback(
(type: TemporaryType) => {
dispatch(createTemporary({ type, docId }));
},
[dispatch, docId]
);
useEffect(() => {
void (async () => {
const isActive = await isFormatActive();
setIsActive(isActive);
})();
}, [isFormatActive]);
const formatTooltips: Record<string, string> = useMemo(
() => ({
[TextAction.Bold]: 'Bold',
[TextAction.Italic]: 'Italic',
[TextAction.Underline]: 'Underline',
[TextAction.Strikethrough]: 'Strike through',
[TextAction.Code]: 'Mark as Code',
[TextAction.Link]: 'Add Link',
[TextAction.Equation]: 'Create equation',
}),
[]
);
const formatClick = useCallback(
(format: TextAction) => {
switch (format) {
@@ -76,22 +104,48 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
return toggleFormat(format);
case TextAction.Link:
return addLink();
case TextAction.Equation:
return addTemporaryInput(TemporaryType.Equation);
}
},
[addLink, toggleFormat]
[addLink, addTemporaryInput, toggleFormat]
);
useEffect(() => {
void (async () => {
const isActive = await isFormatActive();
setIsActive(isActive);
})();
}, [isFormatActive]);
const formatIcon = useMemo(() => {
switch (icon) {
case TextAction.Bold:
return <FormatBold sx={iconSize} />;
case TextAction.Underline:
return <FormatUnderlined sx={iconSize} />;
case TextAction.Italic:
return <FormatItalic sx={iconSize} />;
case TextAction.Code:
return <CodeOutlined sx={iconSize} />;
case TextAction.Strikethrough:
return <StrikethroughSOutlined sx={iconSize} />;
case TextAction.Link:
return (
<div className={'flex items-center justify-center px-1 text-[0.8rem]'}>
<LinkIcon
sx={{
fontSize: '1.2rem',
marginRight: '0.25rem',
}}
/>
<div className={'underline'}>Link</div>
</div>
);
case TextAction.Equation:
return <Functions sx={iconSize} />;
default:
return null;
}
}, [icon]);
return (
<MenuTooltip title={formatTooltips[format]}>
<IconButton size='small' sx={{ color }} onClick={() => formatClick(format)}>
<FormatIcon icon={icon} />
{formatIcon}
</IconButton>
</MenuTooltip>
);
@@ -1,34 +0,0 @@
import React from 'react';
import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
import { TextAction } from '$app/interfaces/document';
import LinkIcon from '@mui/icons-material/AddLink';
export const iconSize = { width: 18, height: 18 };
export default function FormatIcon({ icon }: { icon: string }) {
switch (icon) {
case TextAction.Bold:
return <FormatBold sx={iconSize} />;
case TextAction.Underline:
return <FormatUnderlined sx={iconSize} />;
case TextAction.Italic:
return <FormatItalic sx={iconSize} />;
case TextAction.Code:
return <CodeOutlined sx={iconSize} />;
case TextAction.Strikethrough:
return <StrikethroughSOutlined sx={iconSize} />;
case TextAction.Link:
return (
<div className={'flex items-center justify-center px-1 text-[0.8rem]'}>
<LinkIcon
sx={{
fontSize: '1.2rem',
marginRight: '0.25rem',
}}
/>
<div className={'underline'}>Link</div>
</div>
);
default:
return null;
}
}
@@ -6,18 +6,26 @@ import MenuTooltip from './MenuTooltip';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
function TurnIntoSelect({ id }: { id: string }) {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const [anchorPosition, setAnchorPosition] = React.useState<{
top: number;
left: number;
}>();
const { node } = useSubscribeNode(id);
const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
const rect = event.currentTarget.getBoundingClientRect();
setAnchorPosition({
top: rect.top + rect.height + 5,
left: rect.left,
});
}, []);
const handleClose = useCallback(() => {
setAnchorEl(null);
setAnchorPosition(undefined);
}, []);
const open = Boolean(anchorEl);
const open = Boolean(anchorPosition);
return (
<>
@@ -33,14 +41,11 @@ function TurnIntoSelect({ id }: { id: string }) {
id={id}
open={open}
onClose={handleClose}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'center',
horizontal: 'center',
}}
anchorReference={'anchorPosition'}
anchorPosition={anchorPosition}
transformOrigin={{
vertical: 'center',
horizontal: 'center',
vertical: 'top',
horizontal: 'left',
}}
/>
</>
@@ -1,14 +1,13 @@
import { useMemo } from 'react';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
import { BlockType, TextAction } from '$app/interfaces/document';
import { useSubscribeRanges } from '$app/components/document/_shared/SubscribeSelection.hooks';
import {
blockConfig,
defaultTextActionProps,
defaultTextActionItems,
multiLineTextActionGroups,
multiLineTextActionProps,
textActionGroups,
} from '$app/constants/document/config';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
import { TextAction } from '$app/interfaces/document';
import { useSubscribeRanges } from '$app/components/document/_shared/SubscribeSelection.hooks';
} from '$app/components/document/TextActionMenu/config';
export function useTextActionMenu() {
const range = useSubscribeRanges();
@@ -22,12 +21,9 @@ export function useTextActionMenu() {
const items = useMemo(() => {
if (!node) return [];
if (isSingleLine) {
const config = blockConfig[node.type];
const { customItems, excludeItems } = {
...defaultTextActionProps,
...config.textActionMenuProps,
};
return customItems?.filter((item) => !excludeItems?.includes(item)) || [];
const excludeItems = node.type === BlockType.CodeBlock ? [TextAction.Code] : [];
return defaultTextActionItems?.filter((item) => !excludeItems?.includes(item)) || [];
} else {
return multiLineTextActionProps.customItems || [];
}
@@ -36,6 +32,7 @@ export function useTextActionMenu() {
// the groups have default items, so we need to filter the items if this node has excluded items
const groupItems: TextAction[][] = useMemo(() => {
const groups = node ? textActionGroups : multiLineTextActionGroups;
return groups.map((group) => {
return group.filter((item) => items.includes(item));
});
@@ -17,6 +17,7 @@ function TextActionMenuList() {
case TextAction.Underline:
case TextAction.Strikethrough:
case TextAction.Code:
case TextAction.Equation:
return <FormatButton format={action} icon={action} />;
default:
return null;
Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more