Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c669f1b85 | ||
|
|
b9976b072e | ||
|
|
409e3bf1e3 | ||
|
|
ff9b3c56c5 | ||
|
|
4c17298432 | ||
|
|
0401fc4b22 | ||
|
|
4805d0c568 | ||
|
|
edc7933c66 | ||
|
|
89ff44b035 | ||
|
|
793884427c | ||
|
|
e0ad364fa3 | ||
|
|
5d76a7864d | ||
|
|
323cb3b60f | ||
|
|
9fb8f221cf | ||
|
|
8dfbfe3c42 | ||
|
|
453be8352a | ||
|
|
452d7eb6d0 | ||
|
|
f0d5f51703 | ||
|
|
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
|
||||
@@ -130,6 +134,6 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
attempt_limit: 5
|
||||
attempt_limit: 20
|
||||
attempt_delay: 10000
|
||||
|
||||
@@ -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,19 +24,29 @@ env:
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
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:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: windows-latest
|
||||
flutter_profile: development-windows-x86
|
||||
target: x86_64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "stable-2022-04-07"
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
@@ -60,6 +70,7 @@ jobs:
|
||||
prefix-key: ${{ matrix.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
cache-all-crates: true
|
||||
|
||||
- uses: davidB/rust-cargo-make@v1
|
||||
with:
|
||||
@@ -83,42 +94,35 @@ jobs:
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
shell: bash
|
||||
|
||||
- name: Config Flutter
|
||||
- name: Enable Flutter Desktop
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
flutter config --enable-linux-desktop
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter config --enable-macos-desktop
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
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 Test lib
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
cargo make --profile development-linux-x86_64 appflowy-dev
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
cargo make --profile development-mac-x86_64 appflowy-dev
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
cargo make --profile development-windows-x86 appflowy-dev
|
||||
fi
|
||||
shell: bash
|
||||
cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
|
||||
|
||||
- name: Run AppFlowy tests
|
||||
- name: Run Flutter integration tests
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
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
|
||||
|
||||
@@ -132,5 +136,5 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
attempt_limit: 5
|
||||
attempt_limit: 20
|
||||
attempt_delay: 10000
|
||||
@@ -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)
|
||||
|
||||
+50
-2
@@ -1,27 +1,65 @@
|
||||
# Release Notes
|
||||
## Version 0.2.3 - 06/21/2023
|
||||
|
||||
## Version 0.2.6 - 07/11/2023
|
||||
|
||||
### New Features
|
||||
- Added support for creating multiple database views for existing database
|
||||
|
||||
- Dynamic load themes
|
||||
- Inline math equation
|
||||
|
||||
|
||||
## Version 0.2.5 - 07/02/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Insert local images
|
||||
- Mention a page
|
||||
- Outlines (Table of contents)
|
||||
- Added support for aligning the image by image menu
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- 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
|
||||
|
||||
## 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 +67,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 +81,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.
|
||||
|
||||
|
||||
Vendored
+31
-10
@@ -118,15 +118,11 @@
|
||||
{
|
||||
"label": "AF: Generate Freezed Files",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner build -d",
|
||||
"command": "sh ./scripts/code_generation/freezed/generate_freezed.sh",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Language Files",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/generate_language_files.sh",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"group": "build",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
@@ -134,7 +130,24 @@
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\generate_language_files.cmd"
|
||||
".\\scripts\\code_generation\\freezed\\generate_freezed.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Language Files",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/code_generation/language_files/generate_language_files.sh",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\code_generation\\language_files\\generate_language_files.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -221,5 +234,13 @@
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Env",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs ",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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.6"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -70,6 +70,7 @@ windows/flutter/dart_ffi/
|
||||
**/.vscode/
|
||||
|
||||
*.env
|
||||
*.env.*
|
||||
|
||||
coverage/
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" style="width: 16px; height: 16px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="30" height="30" ><path d="M6.04883 27.3232C7.24219 27.3232 7.86426 26.4854 8.20703 25.3936L13.9834 6.79492H27.5039C28.2275 6.79492 28.7227 6.35059 28.7227 5.65234C28.7227 4.97949 28.2275 4.53516 27.5039 4.53516H13.958C12.6631 4.53516 12.0791 5.01758 11.7363 6.13477L6.18848 24.3525H5.97266L3.58594 15.9355C3.38281 15.2373 3.00195 14.9072 2.40527 14.9072C1.73242 14.9072 1.2373 15.3896 1.2373 16.0117C1.2373 16.2656 1.30078 16.4941 1.35156 16.6846L4.04297 25.5332C4.36035 26.5615 4.93164 27.3232 6.04883 27.3232ZM16.3955 24.7334C16.8652 24.7334 17.1064 24.5684 17.4619 24.0732L20.4707 19.8203H20.5215L23.5049 24.0732C23.873 24.5684 24.1143 24.7334 24.5713 24.7334C25.2061 24.7334 25.6758 24.3018 25.6758 23.7051C25.6758 23.4258 25.6123 23.1973 25.4219 22.9561L21.9307 18.208L25.4473 13.4346C25.6377 13.168 25.7139 12.9395 25.7139 12.6855C25.7139 12.127 25.2568 11.6953 24.6475 11.6953C24.2031 11.6953 23.9365 11.8477 23.6064 12.3174L20.7246 16.5957H20.6611L17.6904 12.3047C17.373 11.8477 17.0938 11.6953 16.624 11.6953C16.0146 11.6953 15.5068 12.165 15.5068 12.7363C15.5068 13.0537 15.583 13.2568 15.8115 13.5488L19.1504 18.1445L15.6084 23.0322C15.418 23.2861 15.3672 23.4639 15.3672 23.7559C15.3672 24.3018 15.8115 24.7334 16.3955 24.7334Z" fill="#2F3030"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -419,4 +419,4 @@
|
||||
"layoutDateField": "تقويم التخطيط بواسطة"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,5 +149,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "petit"
|
||||
}
|
||||
@@ -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",
|
||||
@@ -185,13 +186,29 @@
|
||||
"open": "Open Settings"
|
||||
},
|
||||
"appearance": {
|
||||
"fontFamily": {
|
||||
"label": "Font Family",
|
||||
"search": "Search"
|
||||
},
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Light Mode",
|
||||
"dark": "Dark Mode",
|
||||
"system": "Adapt to System"
|
||||
},
|
||||
"theme": "Theme"
|
||||
"themeUpload": {
|
||||
"button": "Upload",
|
||||
"description": "Upload your own AppFlowy theme using the button below.",
|
||||
"failure": "The theme that was uploaded had an invalid format.",
|
||||
"loading": "Please wait while we validate and upload your theme...",
|
||||
"uploadSuccess": "Your theme was uploaded successfully",
|
||||
"deletionFailure": "Failed to delete the theme. Try to delete it manually.",
|
||||
"filePickerDialogTitle": "Choose a .flowy_plugin file",
|
||||
"urlUploadFailure": "Failed to open url: {}"
|
||||
},
|
||||
"theme": "Theme",
|
||||
"builtInsLabel": "Built-in Themes",
|
||||
"pluginsLabel": "Plugins"
|
||||
},
|
||||
"files": {
|
||||
"copy": "Copy",
|
||||
@@ -384,6 +401,9 @@
|
||||
"createANewCalendar": "Create a new Calendar"
|
||||
}
|
||||
},
|
||||
"selectionMenu": {
|
||||
"outline": "Outline"
|
||||
},
|
||||
"plugins": {
|
||||
"referencedBoard": "Referenced Board",
|
||||
"referencedGrid": "Referenced Grid",
|
||||
@@ -406,6 +426,7 @@
|
||||
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
|
||||
"smartEditDisabled": "Connect OpenAI in Settings",
|
||||
"discardResponse": "Do you want to discard the AI responses?",
|
||||
"createInlineMathEquation": "Create equation",
|
||||
"cover": {
|
||||
"changeCover": "Change Cover",
|
||||
"colors": "Colors",
|
||||
@@ -448,6 +469,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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -227,5 +227,6 @@
|
||||
"column": {
|
||||
"create_new_card": "Nuevo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "pequeño"
|
||||
}
|
||||
@@ -149,5 +149,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "petit"
|
||||
}
|
||||
@@ -235,5 +235,6 @@
|
||||
"column": {
|
||||
"create_new_card": "Nouveau"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "petit"
|
||||
}
|
||||
@@ -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,17 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "kicsi"
|
||||
}
|
||||
|
||||
@@ -222,5 +222,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "kecil"
|
||||
}
|
||||
@@ -155,5 +155,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "piccolo"
|
||||
}
|
||||
@@ -203,5 +203,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "小さい"
|
||||
}
|
||||
@@ -231,5 +231,6 @@
|
||||
"column": {
|
||||
"create_new_card": "추가"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "작은"
|
||||
}
|
||||
@@ -149,5 +149,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "mały"
|
||||
}
|
||||
@@ -145,5 +145,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "pequeno"
|
||||
}
|
||||
@@ -235,5 +235,6 @@
|
||||
"column": {
|
||||
"create_new_card": "Nytt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "små"
|
||||
}
|
||||
@@ -149,5 +149,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "küçük"
|
||||
}
|
||||
@@ -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,132 @@
|
||||
"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": "视图"
|
||||
},
|
||||
"moreAction.small": "小的",
|
||||
"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 查看"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,22 +9,7 @@ import 'util/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
group('calendar database view', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -74,5 +59,131 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('creating and editing calendar events', (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();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
// 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(2);
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Change the title of the event
|
||||
await tester.editTitleInRowDetailPage('hello world');
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Duplicate the event
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Check that there are 2 events
|
||||
tester.assertNumberOfEventsInCalendar(2, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(3, DateTime.now());
|
||||
|
||||
// Delete an event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Check that there is 1 event
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
|
||||
});
|
||||
|
||||
testWidgets('rescheduling events', (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
|
||||
final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, sameDayNextWeek);
|
||||
|
||||
// Delete the event
|
||||
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Create a new event in today's calendar cell
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
@@ -9,21 +11,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid cell', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('edit text cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -46,6 +33,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 +201,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 +284,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();
|
||||
|
||||
@@ -10,21 +10,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid page', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('rename existing field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -5,27 +5,11 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'import_files';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('add text filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -13,19 +14,19 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'appflowy';
|
||||
testWidgets('row details page opens', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
// Make sure that the row page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
});
|
||||
|
||||
testWidgets('insert emoji in the row detail page', (tester) async {
|
||||
@@ -171,9 +172,10 @@ void main() {
|
||||
// Focus on the editor
|
||||
final textBlock = find.byType(TextBlockComponentWidget);
|
||||
await tester.tapAt(tester.getCenter(textBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Input some text
|
||||
const inputText = 'Hello world';
|
||||
const inputText = 'Hello World';
|
||||
await tester.ime.insertText(inputText);
|
||||
expect(
|
||||
find.textContaining(inputText, findRichText: true),
|
||||
|
||||
@@ -8,21 +8,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('create row of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -9,21 +9,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('update layout', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -1,66 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'import_files';
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('import v0.2.0 database data', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
tester.expectToSeePageName(readme);
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapImportButton();
|
||||
|
||||
final testFileNames = ['v020.afdb'];
|
||||
final fileLocation = await tester.currentFileLocation();
|
||||
for (final fileName in testFileNames) {
|
||||
final str = await rootBundle.loadString(
|
||||
p.join(
|
||||
'assets/test/workspaces/database',
|
||||
fileName,
|
||||
),
|
||||
);
|
||||
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
|
||||
}
|
||||
// mock get files
|
||||
await mockPickFilePaths(testFileNames, name: location);
|
||||
await tester.tapDatabaseRawDataButton();
|
||||
await tester.openPage('v020');
|
||||
|
||||
// check the import content
|
||||
// await tester.assertCellContent(
|
||||
// rowIndex: 7,
|
||||
// fieldType: FieldType.RichText,
|
||||
// // fieldName: 'Name',
|
||||
// content: '',
|
||||
// );
|
||||
await tester.openV020database();
|
||||
// wait the database data is loaded
|
||||
await tester.pumpAndSettle(const Duration(microseconds: 500));
|
||||
|
||||
// check the text cell
|
||||
final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];
|
||||
|
||||
@@ -3,27 +3,11 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'import_files';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('add text sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
|
||||
@@ -11,21 +11,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('create linked view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('cover image', () {
|
||||
const location = 'cover_image';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'hovering on cover image will display change and delete cover image buttons',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
await tester.editor.hoverOnCoverPluginAddButton();
|
||||
|
||||
tester.expectToSeePluginAddCoverAndIconButton();
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-16
@@ -9,22 +9,7 @@ import '../util/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
group('create and delete the document', () {
|
||||
testWidgets('create a new document when launching app in first time',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_create_and_delete_test.dart'
|
||||
as document_create_and_delete_test;
|
||||
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
||||
import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'document_with_inline_math_equation_test.dart'
|
||||
as document_with_inline_math_equation_test;
|
||||
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
|
||||
import 'edit_document_test.dart' as document_edit_test;
|
||||
|
||||
void startTesting() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
document_create_and_delete_test.main();
|
||||
document_edit_test.main();
|
||||
document_with_database_test.main();
|
||||
document_with_inline_page_test.main();
|
||||
document_with_inline_math_equation_test.main();
|
||||
document_with_cover_image_test.main();
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
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() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('cover image', () {
|
||||
testWidgets('document cover tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
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
-25
@@ -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';
|
||||
@@ -13,17 +14,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database view in document', () {
|
||||
const location = 'database_view';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('insert a referenced grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -56,21 +46,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 +83,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,
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flutter/material.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();
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('inline math equation in document', () {
|
||||
testWidgets('insert an inline math equation', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
ViewLayoutPB.Document,
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a inline page
|
||||
const formula = 'E = MC ^ 2';
|
||||
await tester.ime.insertText(formula);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton = find.byTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
final inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsOneWidget);
|
||||
|
||||
// tap it and update the content
|
||||
await tester.tapButton(inlineMathEquation);
|
||||
final textFormField = find.descendant(
|
||||
of: find.byType(MathInputTextField),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
const newFormula = 'E = MC ^ 3';
|
||||
await tester.enterText(textFormField, newFormula);
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(MathInputTextField),
|
||||
matching: find.byType(FlowyButton),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('remove the inline math equation format', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
ViewLayoutPB.Document,
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a inline page
|
||||
const formula = 'E = MC ^ 2';
|
||||
await tester.ime.insertText(formula);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the inline math equation button
|
||||
var inlineMathEquationButton = find.byTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
var inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsOneWidget);
|
||||
|
||||
// highlight the math equation block
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: 1),
|
||||
);
|
||||
|
||||
// expect to the see the inline math equation button is highlighted
|
||||
inlineMathEquationButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is IconItemWidget &&
|
||||
widget.tooltip ==
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
expect(
|
||||
tester.widget<IconItemWidget>(inlineMathEquationButton).isHighlight,
|
||||
isTrue,
|
||||
);
|
||||
|
||||
// cancel the format
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block is removed
|
||||
inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsNothing);
|
||||
|
||||
tester.expectToSeeText(formula);
|
||||
});
|
||||
});
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
+118
@@ -0,0 +1,118 @@
|
||||
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', () {
|
||||
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;
|
||||
}
|
||||
@@ -13,17 +13,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('edit document', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('redo & undo', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -118,7 +107,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,95 @@
|
||||
import 'dart:io';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/keyboard.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('hotkeys test', () {
|
||||
testWidgets('toggle theme mode', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.expectToSeeText(
|
||||
LocaleKeys.settings_appearance_themeMode_system.tr(),
|
||||
);
|
||||
|
||||
await tester.tapButton(
|
||||
find.bySemanticsLabel(
|
||||
LocaleKeys.settings_appearance_themeMode_system.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tapButton(
|
||||
find.bySemanticsLabel(
|
||||
LocaleKeys.settings_appearance_themeMode_dark.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
await tester.tap(find.byType(SettingsDialog));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
Platform.isMacOS
|
||||
? LogicalKeyboardKey.meta
|
||||
: LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
LogicalKeyboardKey.keyL,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.expectToSeeText(
|
||||
LocaleKeys.settings_appearance_themeMode_light.tr(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('show or hide home menu', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(HomeMenu), findsOneWidget);
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
Platform.isMacOS
|
||||
? LogicalKeyboardKey.meta
|
||||
: LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.backslash,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(HomeMenu), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -12,23 +12,8 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('import files', () {
|
||||
const location = 'import_files';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('import multiple markdown files', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
final context = await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
@@ -38,18 +23,21 @@ void main() {
|
||||
await tester.tapImportButton();
|
||||
|
||||
final testFileNames = ['test1.md', 'test2.md'];
|
||||
final fileLocation = await tester.currentFileLocation();
|
||||
final paths = <String>[];
|
||||
for (final fileName in testFileNames) {
|
||||
final str = await rootBundle.loadString(
|
||||
p.join(
|
||||
'assets/test/workspaces/markdowns',
|
||||
fileName,
|
||||
),
|
||||
'assets/test/workspaces/markdowns/$fileName',
|
||||
);
|
||||
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
|
||||
final path = p.join(context.applicationDataDirectory, fileName);
|
||||
paths.add(path);
|
||||
File(path).writeAsStringSync(str);
|
||||
}
|
||||
// mock get files
|
||||
await mockPickFilePaths(testFileNames, name: location);
|
||||
await mockPickFilePaths(
|
||||
paths: testFileNames
|
||||
.map((e) => p.join(context.applicationDataDirectory, e))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
await tester.tapTextAndMarkdownButton();
|
||||
|
||||
|
||||
@@ -8,16 +8,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUpAll(() async {
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'change the language successfully when launching the app for the first time',
|
||||
(tester) async {
|
||||
|
||||
@@ -17,7 +17,8 @@ void main() {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
|
||||
testWidgets('testing selection on open-ai smart menu replace', (tester) async {
|
||||
testWidgets('testing selection on open-ai smart menu replace',
|
||||
(tester) async {
|
||||
final appFlowyEditor = await setUpOpenAITesting(tester);
|
||||
final editorState = appFlowyEditor.editorState;
|
||||
|
||||
@@ -38,7 +39,8 @@ void main() {
|
||||
await tester.tap(find.text('Summarize'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
|
||||
await tester
|
||||
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -49,7 +51,8 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
testWidgets('testing selection on open-ai smart menu insert', (tester) async {
|
||||
testWidgets('testing selection on open-ai smart menu insert',
|
||||
(tester) async {
|
||||
final appFlowyEditor = await setUpOpenAITesting(tester);
|
||||
final editorState = appFlowyEditor.editorState;
|
||||
|
||||
@@ -69,7 +72,8 @@ void main() {
|
||||
await tester.tap(find.text('Summarize'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
|
||||
await tester
|
||||
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
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', () {
|
||||
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();
|
||||
}
|
||||
@@ -1,23 +1,19 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'switch_folder_test.dart' as switch_folder_test;
|
||||
import 'document/document_test.dart' as document_test;
|
||||
import 'document/cover_image_test.dart' as cover_image_test;
|
||||
import 'share_markdown_test.dart' as share_markdown_test;
|
||||
import 'import_files_test.dart' as import_files_test;
|
||||
import 'document/document_with_database_test.dart'
|
||||
as document_with_database_test;
|
||||
import 'document/edit_document_test.dart' as edit_document_test;
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
import 'database_field_test.dart' as database_field_test;
|
||||
import 'database_share_test.dart' as database_share_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database_row_test.dart' as database_row_test;
|
||||
import 'database_setting_test.dart' as database_setting_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_view_test.dart' as database_view_test;
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_share_test.dart' as database_share_test;
|
||||
import 'database_sort_test.dart' as database_sort_test;
|
||||
import 'database_view_test.dart' as database_view_test;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'import_files_test.dart' as import_files_test;
|
||||
import 'share_markdown_test.dart' as share_markdown_test;
|
||||
import 'switch_folder_test.dart' as switch_folder_test;
|
||||
|
||||
/// The main task runner for all integration tests in AppFlowy.
|
||||
///
|
||||
@@ -33,10 +29,7 @@ void main() {
|
||||
import_files_test.main();
|
||||
|
||||
// Document integration tests
|
||||
cover_image_test.main();
|
||||
document_test.main();
|
||||
document_with_database_test.main();
|
||||
edit_document_test.main();
|
||||
document_test_runner.startTesting();
|
||||
|
||||
// Database integration tests
|
||||
database_cell_test.main();
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
@@ -11,30 +11,17 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('share markdown in document page', () {
|
||||
const location = 'markdown';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('click the share button in document page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
final context = await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
tester.expectToSeePageName(readme);
|
||||
|
||||
// mock the file picker
|
||||
final path = await mockSaveFilePath(location, 'test.md');
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(context.applicationDataDirectory, 'test.md'),
|
||||
);
|
||||
// click the share button and select markdown
|
||||
await tester.tapShareButton();
|
||||
await tester.tapMarkdownButton();
|
||||
@@ -52,7 +39,7 @@ void main() {
|
||||
testWidgets(
|
||||
'share the markdown after renaming the document name',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
final context = await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
@@ -65,8 +52,12 @@ void main() {
|
||||
final shareButton = find.byType(ShareActionList);
|
||||
final shareButtonState =
|
||||
tester.state(shareButton) as ShareActionListState;
|
||||
final path =
|
||||
await mockSaveFilePath(location, '${shareButtonState.name}.md');
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(
|
||||
context.applicationDataDirectory,
|
||||
'${shareButtonState.name}.md',
|
||||
),
|
||||
);
|
||||
|
||||
// click the share button and select markdown
|
||||
await tester.tapShareButton();
|
||||
|
||||
@@ -1,48 +1,36 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('customize the folder path', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
final userA = uuid();
|
||||
final userB = uuid();
|
||||
const userA = 'UserA';
|
||||
const userB = 'UserB';
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
await TestFolder.cleanTestLocation(userB);
|
||||
await TestFolder.setTestLocation(p.join(userA, appFlowyDataFolder));
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
final initialPath = p.join(userA, appFlowyDataFolder);
|
||||
final context = await tester.initializeAppFlowy(
|
||||
pathExtension: initialPath,
|
||||
);
|
||||
// remove the last extension
|
||||
final rootPath = context.applicationDataDirectory.replaceFirst(
|
||||
initialPath,
|
||||
'',
|
||||
);
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
// switch to user B
|
||||
{
|
||||
// set user name to userA
|
||||
// set user name for userA
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userA);
|
||||
@@ -51,12 +39,14 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userB);
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userB),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
// set user name to userB
|
||||
// set user name for userB
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userB);
|
||||
@@ -68,7 +58,9 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userA);
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userA),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
@@ -83,16 +75,15 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userB);
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userB),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeHomePage();
|
||||
tester.expectToSeeUserName(userB);
|
||||
}
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
await TestFolder.cleanTestLocation(userB);
|
||||
});
|
||||
|
||||
testWidgets('reset to default location', (tester) async {
|
||||
@@ -109,8 +100,8 @@ void main() {
|
||||
await tester.restoreLocation();
|
||||
|
||||
expect(
|
||||
await TestFolder.defaultDevelopmentLocation(),
|
||||
await TestFolder.currentLocation(),
|
||||
await appFlowyApplicationDataDirectory().then((value) => value.path),
|
||||
await getIt<ApplicationDataStorage>().getPath(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class TestFolder {
|
||||
/// Location / Path
|
||||
class FlowyTestContext {
|
||||
FlowyTestContext({
|
||||
required this.applicationDataDirectory,
|
||||
});
|
||||
|
||||
/// Set a given AppFlowy data storage location under test environment.
|
||||
///
|
||||
/// To pass null means clear the location.
|
||||
///
|
||||
/// The file_picker is a system component and can't be tapped, so using logic instead of tapping.
|
||||
///
|
||||
static Future<void> setTestLocation(String? name) async {
|
||||
final location = await testLocation(name);
|
||||
SharedPreferences.setMockInitialValues({
|
||||
KVKeys.pathLocation: location.path,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/// Clean the location.
|
||||
static Future<void> cleanTestLocation(String? name) async {
|
||||
final dir = await testLocation(name);
|
||||
await dir.delete(recursive: true);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Get current using location.
|
||||
static Future<String> currentLocation() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(KVKeys.pathLocation)!;
|
||||
}
|
||||
|
||||
/// Get default location under development environment.
|
||||
static Future<String> defaultDevelopmentLocation() async {
|
||||
final dir = await appFlowyApplicationDataDirectory();
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
/// Get default location under test environment.
|
||||
static Future<Directory> testLocation(String? name) async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
var path = '${dir.path}/flowy_test';
|
||||
if (name != null) {
|
||||
path += '/$name';
|
||||
}
|
||||
return Directory(path).create(recursive: true);
|
||||
}
|
||||
final String applicationDataDirectory;
|
||||
}
|
||||
|
||||
extension AppFlowyTestBase on WidgetTester {
|
||||
Future<void> initializeAppFlowy() async {
|
||||
Future<FlowyTestContext> initializeAppFlowy({
|
||||
// use to append after the application data directory
|
||||
String? pathExtension,
|
||||
Size windowsSize = const Size(1600, 1200),
|
||||
}) async {
|
||||
binding.setSurfaceSize(windowsSize);
|
||||
|
||||
mockHotKeyManagerHandlers();
|
||||
final directory = await mockApplicationDataStorage(
|
||||
pathExtension: pathExtension,
|
||||
);
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await FlowyRunner.run(
|
||||
FlowyApp(),
|
||||
IntegrationMode.integrationTest,
|
||||
);
|
||||
|
||||
await wait(3000);
|
||||
await pumpAndSettle(const Duration(seconds: 2));
|
||||
return FlowyTestContext(
|
||||
applicationDataDirectory: directory,
|
||||
);
|
||||
}
|
||||
|
||||
void mockHotKeyManagerHandlers() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'unregisterAll') {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await FlowyRunner.run(FlowyApp(), IntegrationMode.integrationTest);
|
||||
Future<String> mockApplicationDataStorage({
|
||||
// use to append after the application data directory
|
||||
String? pathExtension,
|
||||
}) async {
|
||||
final dir = await getTemporaryDirectory();
|
||||
|
||||
await wait(3000);
|
||||
await pumpAndSettle(const Duration(seconds: 2));
|
||||
// Use a random uuid to avoid conflict.
|
||||
String path = p.join(dir.path, 'appflowy_integration_test', uuid());
|
||||
if (pathExtension != null && pathExtension.isNotEmpty) {
|
||||
path = '$path/$pathExtension';
|
||||
}
|
||||
final directory = Directory(path);
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
|
||||
MockApplicationDataStorage.initialPath = directory.path;
|
||||
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<void> tapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
bool warnIfMissed = true,
|
||||
bool warnIfMissed = false,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tap(
|
||||
@@ -127,6 +127,43 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> doubleTapAt(
|
||||
Offset location, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tapAt(location, pointer: pointer, buttons: buttons);
|
||||
await pump(kDoubleTapMinTime);
|
||||
await tapAt(location, pointer: pointer, buttons: buttons);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
}
|
||||
|
||||
Future<void> doubleTapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
bool warnIfMissed = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tap(
|
||||
finder,
|
||||
pointer: pointer,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
|
||||
await pump(kDoubleTapMinTime);
|
||||
|
||||
await tap(
|
||||
finder,
|
||||
buttons: buttons,
|
||||
pointer: pointer,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
}
|
||||
|
||||
Future<void> wait(int milliseconds) async {
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
|
||||
@@ -17,11 +17,6 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'util.dart';
|
||||
|
||||
extension CommonOperations on WidgetTester {
|
||||
/// Get current file location of AppFlowy.
|
||||
Future<String> currentFileLocation() async {
|
||||
return TestFolder.currentLocation();
|
||||
}
|
||||
|
||||
/// Tap the GetStart button on the launch page.
|
||||
Future<void> tapGoButton() async {
|
||||
final goButton = find.byType(GoButton);
|
||||
|
||||
@@ -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';
|
||||
@@ -68,7 +73,7 @@ import 'mock/mock_file_picker.dart';
|
||||
|
||||
extension AppFlowyDatabaseTest on WidgetTester {
|
||||
Future<void> openV020database() async {
|
||||
await initializeAppFlowy();
|
||||
final context = await initializeAppFlowy();
|
||||
await tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
@@ -78,18 +83,25 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapImportButton();
|
||||
|
||||
final testFileNames = ['v020.afdb'];
|
||||
final fileLocation = await currentFileLocation();
|
||||
final paths = <String>[];
|
||||
for (final fileName in testFileNames) {
|
||||
final str = await rootBundle.loadString(
|
||||
p.join(
|
||||
'assets/test/workspaces/database',
|
||||
fileName,
|
||||
),
|
||||
// Don't use the p.join to build the path that used in loadString. It
|
||||
// is not working on windows.
|
||||
final str = await rootBundle
|
||||
.loadString("assets/test/workspaces/database/$fileName");
|
||||
|
||||
// Write the content to the file.
|
||||
final path = p.join(
|
||||
context.applicationDataDirectory,
|
||||
fileName,
|
||||
);
|
||||
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
|
||||
paths.add(path);
|
||||
File(path).writeAsStringSync(str);
|
||||
}
|
||||
// mock get files
|
||||
await mockPickFilePaths(testFileNames, name: 'import_files');
|
||||
await mockPickFilePaths(
|
||||
paths: paths,
|
||||
);
|
||||
await tapDatabaseRawDataButton();
|
||||
await openPage('v020');
|
||||
}
|
||||
@@ -106,22 +118,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 +182,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 +303,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 +394,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 +421,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 +447,29 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(expandButton);
|
||||
}
|
||||
|
||||
void assertRowDetailPageOpened() async {
|
||||
final findRowDetailPage = find.byType(RowDetailPage);
|
||||
expect(findRowDetailPage, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> dismissRowDetailPage() async {
|
||||
// use tap empty area instead of clicking ESC to dismiss the row detail page
|
||||
// sometimes, the ESC key is not working.
|
||||
await simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await pumpAndSettle();
|
||||
final findRowDetailPage = find.byType(RowDetailPage);
|
||||
if (findRowDetailPage.evaluate().isNotEmpty) {
|
||||
await tapAt(const Offset(0, 0));
|
||||
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 +487,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 +684,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(finder, matcher);
|
||||
}
|
||||
|
||||
Future<void> dismissSelectOptionEditor() async {
|
||||
Future<void> dismissCellEditor() async {
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
@@ -866,6 +990,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),
|
||||
);
|
||||
final location = getTopLeft(todayCell).translate(10, 10);
|
||||
await doubleTapAt(location);
|
||||
}
|
||||
|
||||
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(AppFlowyRichText);
|
||||
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,25 @@ 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);
|
||||
}
|
||||
|
||||
/// Update the editor's selection
|
||||
Future<void> updateSelection(Selection selection) async {
|
||||
final editorState = getCurrentEditorState();
|
||||
editorState.updateSelectionWithReason(
|
||||
selection,
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:file_picker/file_picker.dart' as fp;
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../util.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
|
||||
class MockFilePicker implements FilePickerService {
|
||||
MockFilePicker({
|
||||
@@ -25,7 +20,7 @@ class MockFilePicker implements FilePickerService {
|
||||
String? dialogTitle,
|
||||
String? fileName,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
@@ -36,18 +31,17 @@ class MockFilePicker implements FilePickerService {
|
||||
Future<FilePickerResult?> pickFiles({
|
||||
String? dialogTitle,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
Function(fp.FilePickerStatus p1)? onFileLoading,
|
||||
Function(FilePickerStatus p1)? onFileLoading,
|
||||
bool allowCompression = true,
|
||||
bool allowMultiple = false,
|
||||
bool withData = false,
|
||||
bool withReadStream = false,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
final platformFiles = mockPaths
|
||||
.map((e) => fp.PlatformFile(path: e, name: '', size: 0))
|
||||
.toList();
|
||||
final platformFiles =
|
||||
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
|
||||
return Future.value(
|
||||
FilePickerResult(
|
||||
platformFiles,
|
||||
@@ -56,20 +50,21 @@ class MockFilePicker implements FilePickerService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mockGetDirectoryPath(String? name) async {
|
||||
final dir = await TestFolder.testLocation(name);
|
||||
Future<void> mockGetDirectoryPath(
|
||||
String path,
|
||||
) async {
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPath: dir.path,
|
||||
mockPath: path,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<String> mockSaveFilePath(String? name, String fileName) async {
|
||||
final dir = await TestFolder.testLocation(name);
|
||||
final path = p.join(dir.path, fileName);
|
||||
Future<String> mockSaveFilePath(
|
||||
String path,
|
||||
) async {
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
@@ -79,18 +74,16 @@ Future<String> mockSaveFilePath(String? name, String fileName) async {
|
||||
return path;
|
||||
}
|
||||
|
||||
Future<List<String>> mockPickFilePaths(
|
||||
List<String> fileNames, {
|
||||
String? name,
|
||||
String? customPath,
|
||||
Future<List<String>> mockPickFilePaths({
|
||||
required List<String> paths,
|
||||
}) async {
|
||||
late final Directory dir;
|
||||
if (customPath != null) {
|
||||
dir = Directory(customPath);
|
||||
} else {
|
||||
dir = await TestFolder.testLocation(name);
|
||||
}
|
||||
final paths = fileNames.map((e) => p.join(dir.path, e)).toList();
|
||||
// late final Directory dir;
|
||||
// if (customPath != null) {
|
||||
// dir = Directory(customPath);
|
||||
// } else {
|
||||
// dir = await TestFolder.testLocation(applicationDataPath, name);
|
||||
// }
|
||||
// final paths = fileNames.map((e) => p.join(dir.path, e)).toList();
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
|
||||
@@ -7,32 +7,24 @@ class Config {
|
||||
required String anonKey,
|
||||
required String key,
|
||||
required String secret,
|
||||
required String pgUrl,
|
||||
required String pgUser,
|
||||
required String pgPassword,
|
||||
required String pgPort,
|
||||
}) async {
|
||||
final postgresConfig = PostgresConfigurationPB.create()
|
||||
..url = pgUrl
|
||||
..userName = pgUser
|
||||
..password = pgPassword
|
||||
..port = int.parse(pgPort);
|
||||
|
||||
await ConfigEventSetSupabaseConfig(
|
||||
SupabaseConfigPB.create()
|
||||
..supabaseUrl = url
|
||||
..key = key
|
||||
..anonKey = anonKey
|
||||
..jwtSecret = secret,
|
||||
..jwtSecret = secret
|
||||
..postgresConfig = postgresConfig,
|
||||
).send();
|
||||
}
|
||||
|
||||
static Future<void> setSupabaseCollabPluginConfig({
|
||||
required String url,
|
||||
required String key,
|
||||
required String jwtSecret,
|
||||
required String collabTable,
|
||||
}) async {
|
||||
final payload = CollabPluginConfigPB.create();
|
||||
final collabTableConfig = CollabTableConfigPB.create()
|
||||
..tableName = collabTable;
|
||||
|
||||
payload.supabaseConfig = SupabaseDBConfigPB.create()
|
||||
..supabaseUrl = url
|
||||
..key = key
|
||||
..jwtSecret = jwtSecret
|
||||
..collabTableConfig = collabTableConfig;
|
||||
|
||||
await ConfigEventSetCollabPluginConfig(payload).send();
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,9 @@ class KVKeys {
|
||||
/// The value is a json string with the following format:
|
||||
/// {'height': 600.0, 'width': 800.0}
|
||||
static const String windowSize = 'windowSize';
|
||||
|
||||
static const String kDocumentAppearanceFontSize =
|
||||
'kDocumentAppearanceFontSize';
|
||||
static const String kDocumentAppearanceFontFamily =
|
||||
'kDocumentAppearanceFontFamily';
|
||||
}
|
||||
+39
-3
@@ -3,6 +3,17 @@ import 'package:envied/envied.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
|
||||
/// The environment variables are defined in `.env` file that is located in the
|
||||
/// appflowy_flutter.
|
||||
/// Run `dart run build_runner build --delete-conflicting-outputs`
|
||||
/// to generate the keys from the env file.
|
||||
///
|
||||
/// If you want to regenerate the keys, you need to run `dart run
|
||||
/// build_runner clean` before running `dart run build_runner build
|
||||
/// --delete-conflicting-outputs`.
|
||||
|
||||
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
|
||||
///
|
||||
@Envied(path: '.env')
|
||||
abstract class Env {
|
||||
@EnviedField(
|
||||
@@ -32,14 +43,39 @@ abstract class Env {
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_COLLAB_TABLE',
|
||||
varName: 'SUPABASE_DB',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseCollabTable = _Env.supabaseCollabTable;
|
||||
static final String supabaseDb = _Env.supabaseDb;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_DB_USER',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseDbUser = _Env.supabaseDbUser;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_DB_PASSWORD',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseDbPassword = _Env.supabaseDbPassword;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_DB_PORT',
|
||||
defaultValue: '5432',
|
||||
)
|
||||
static final String supabaseDbPort = _Env.supabaseDbPort;
|
||||
}
|
||||
|
||||
bool get isSupabaseEnable =>
|
||||
Env.supabaseUrl.isNotEmpty &&
|
||||
Env.supabaseAnonKey.isNotEmpty &&
|
||||
Env.supabaseKey.isNotEmpty &&
|
||||
Env.supabaseJwtSecret.isNotEmpty;
|
||||
Env.supabaseJwtSecret.isNotEmpty &&
|
||||
Env.supabaseDb.isNotEmpty &&
|
||||
Env.supabaseDbUser.isNotEmpty &&
|
||||
Env.supabaseDbPassword.isNotEmpty &&
|
||||
Env.supabaseDbPort.isNotEmpty;
|
||||
+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();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:appflowy/plugins/database_view/grid/application/row/row_document
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
@@ -86,7 +85,6 @@ class _RowEditorState extends State<RowEditor> {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => DocumentAppearanceCubit()),
|
||||
BlocProvider.value(value: documentBloc),
|
||||
],
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,8 +39,6 @@ class DocumentPluginBuilder extends PluginBuilder {
|
||||
|
||||
class DocumentPlugin extends Plugin<int> {
|
||||
late PluginType _pluginType;
|
||||
final DocumentAppearanceCubit _documentAppearanceCubit =
|
||||
DocumentAppearanceCubit();
|
||||
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
@@ -52,20 +50,12 @@ class DocumentPlugin extends Plugin<int> {
|
||||
Key? key,
|
||||
}) : notifier = ViewPluginNotifier(view: view) {
|
||||
_pluginType = pluginType;
|
||||
_documentAppearanceCubit.fetch();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_documentAppearanceCubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder {
|
||||
return DocumentPluginWidgetBuilder(
|
||||
notifier: notifier,
|
||||
documentAppearanceCubit: _documentAppearanceCubit,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,11 +71,9 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
final ViewPluginNotifier notifier;
|
||||
ViewPB get view => notifier.view;
|
||||
int? deletedViewIndex;
|
||||
DocumentAppearanceCubit documentAppearanceCubit;
|
||||
|
||||
DocumentPluginWidgetBuilder({
|
||||
required this.notifier,
|
||||
required this.documentAppearanceCubit,
|
||||
Key? key,
|
||||
});
|
||||
|
||||
@@ -102,17 +90,14 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
});
|
||||
});
|
||||
|
||||
return BlocProvider.value(
|
||||
value: documentAppearanceCubit,
|
||||
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (_, state) {
|
||||
return DocumentPage(
|
||||
view: view,
|
||||
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
|
||||
key: ValueKey(view.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (_, state) {
|
||||
return DocumentPage(
|
||||
view: view,
|
||||
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
|
||||
key: ValueKey(view.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,10 +113,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
view: view,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
BlocProvider.value(
|
||||
value: documentAppearanceCubit,
|
||||
child: const DocumentMoreButton(),
|
||||
),
|
||||
const DocumentMoreButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/base64_string.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
|
||||
hide DocumentEvent;
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -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,
|
||||
@@ -47,28 +49,21 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
quoteItem,
|
||||
bulletedListItem,
|
||||
numberedListItem,
|
||||
inlineMathEquationItem,
|
||||
linkItem,
|
||||
textColorItem,
|
||||
highlightColorItem,
|
||||
buildTextColorItem(),
|
||||
buildHighlightColorItem(),
|
||||
];
|
||||
|
||||
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 +82,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 +106,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
slashMenuItems = _customSlashMenuItems();
|
||||
|
||||
effectiveScrollController = widget.scrollController ?? ScrollController();
|
||||
}
|
||||
|
||||
@@ -116,7 +126,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
final (bool autoFocus, Selection? selection) =
|
||||
_computeAutoFocusParameters();
|
||||
|
||||
final editor = AppFlowyEditor.custom(
|
||||
final editor = AppFlowyEditor(
|
||||
editorState: widget.editorState,
|
||||
editable: true,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
@@ -132,6 +142,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
characterShortcutEvents: characterShortcutEvents,
|
||||
commandShortcutEvents: commandShortcutEvents,
|
||||
header: widget.header,
|
||||
footer: const VSpace(200),
|
||||
);
|
||||
|
||||
return Center(
|
||||
@@ -161,7 +172,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 +209,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 +231,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 +253,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 +284,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
NumberedListBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
CalloutBlockKeys.type
|
||||
CalloutBlockKeys.type,
|
||||
OutlineBlockKeys.type,
|
||||
];
|
||||
|
||||
final supportAlignBuilderType = [
|
||||
@@ -277,7 +309,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 +323,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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
|
||||
+3
-2
@@ -46,7 +46,7 @@ void showLinkToPageMenu(
|
||||
linkToPageMenuEntry.remove();
|
||||
} on FlowyError catch (e) {
|
||||
Dialogs.show(
|
||||
FlowyErrorPage.message(
|
||||
child: FlowyErrorPage.message(
|
||||
e.msg,
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
@@ -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) {
|
||||
|
||||
+7
-3
@@ -105,7 +105,7 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget {
|
||||
|
||||
class _CalloutBlockComponentWidgetState
|
||||
extends State<CalloutBlockComponentWidget>
|
||||
with SelectableMixin, DefaultSelectable, BlockComponentConfigurable {
|
||||
with SelectableMixin, DefaultSelectableMixin, BlockComponentConfigurable {
|
||||
// the key used to forward focus to the richtext child
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
@@ -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(),
|
||||
@@ -188,7 +192,7 @@ class _CalloutBlockComponentWidgetState
|
||||
Widget buildCalloutBlockComponent(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: FlowyRichText(
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
|
||||
+2
-2
@@ -96,7 +96,7 @@ class CodeBlockComponentWidget extends BlockComponentStatefulWidget {
|
||||
}
|
||||
|
||||
class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
|
||||
with SelectableMixin, DefaultSelectable, BlockComponentConfigurable {
|
||||
with SelectableMixin, DefaultSelectableMixin, BlockComponentConfigurable {
|
||||
// the key used to forward focus to the richtext child
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
@@ -216,7 +216,7 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
|
||||
final codeTextSpans = _convert(codeNodes);
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: FlowyRichText(
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
|
||||
-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: () {
|
||||
+6
-6
@@ -1,21 +1,21 @@
|
||||
import 'dart:io';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart' as fp;
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
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> {
|
||||
@@ -120,7 +120,7 @@ class CoverImagePickerBloc
|
||||
final result = await getIt<FilePickerService>().pickFiles(
|
||||
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
|
||||
allowMultiple: false,
|
||||
type: fp.FileType.image,
|
||||
type: FileType.image,
|
||||
allowedExtensions: allowedExtensions,
|
||||
);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
+493
@@ -0,0 +1,493 @@
|
||||
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';
|
||||
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 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';
|
||||
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';
|
||||
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/application_data_storage.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);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
Loaded 100 of 565 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user