Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c513be305 | ||
|
|
11d05b303d | ||
|
|
c870dbeac4 | ||
|
|
3acd36e580 | ||
|
|
95d4fb6865 | ||
|
|
30b52a29fd | ||
|
|
a17d6b7eec | ||
|
|
18ed553296 | ||
|
|
7f74fd6149 | ||
|
|
a3e09f6021 | ||
|
|
5e0a0f92ff | ||
|
|
2a2b5fe246 | ||
|
|
f914e7aa33 | ||
|
|
386d3f2ba7 | ||
|
|
88faa0f02a | ||
|
|
f3d6c66f5c | ||
|
|
d665153a9f | ||
|
|
eee32110f4 | ||
|
|
9834eccc7b | ||
|
|
bef7fe87aa | ||
|
|
feceb430cf | ||
|
|
a29c8ab27a | ||
|
|
f1bfcb6066 | ||
|
|
99cfda0a5c |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
+15
-14
@@ -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,
|
||||
|
||||
+129
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
-6
@@ -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,
|
||||
|
||||
+63
-35
@@ -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, {
|
||||
|
||||
-1
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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),
|
||||
|
||||
+2
-2
@@ -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>(
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -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);
|
||||
|
||||
+5
-2
@@ -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 =
|
||||
|
||||
+2
-2
@@ -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,
|
||||
);
|
||||
|
||||
-1
@@ -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(),
|
||||
|
||||
+5
-2
@@ -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 =
|
||||
|
||||
+5
-2
@@ -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));
|
||||
|
||||
+2
-2
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+5
-1
@@ -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(),
|
||||
|
||||
-555
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
-84
@@ -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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+90
-64
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -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);
|
||||
}
|
||||
+6
-13
@@ -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: () {
|
||||
+3
-2
@@ -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> {
|
||||
+507
@@ -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();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -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),
|
||||
),
|
||||
+84
@@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+290
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+59
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+164
@@ -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;
|
||||
}
|
||||
}
|
||||
+22
@@ -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();
|
||||
}
|
||||
}
|
||||
+143
@@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+212
@@ -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() ?? '';
|
||||
}
|
||||
}
|
||||
+9
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
+10
@@ -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,
|
||||
),
|
||||
|
||||
+24
-8
@@ -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!,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+52
@@ -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
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
+28
-22
@@ -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
|
||||
|
||||
+55
-22
@@ -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;
|
||||
}
|
||||
+34
-21
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
+18
-7
@@ -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',
|
||||
|
||||
+30
-18
@@ -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,
|
||||
};
|
||||
}
|
||||
+9
-11
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+14
-5
@@ -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;
|
||||
+71
@@ -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];
|
||||
+4
-2
@@ -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();
|
||||
|
||||
+40
-6
@@ -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} />;
|
||||
};
|
||||
|
||||
+79
-25
@@ -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>
|
||||
);
|
||||
|
||||
-34
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
-11
@@ -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',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
+9
-12
@@ -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));
|
||||
});
|
||||
|
||||
+1
@@ -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
Reference in new issue
Block a user