Compare commits

...
60 Commits
Author SHA1 Message Date
Lucas.Xu 0fb004aee0 chore: bump version 0.2.7 (#3022)
* chore: bump version 0.2.7

* chore: update language files using inlang machine translation
2023-07-18 19:59:35 +08:00
Alex Wallen 06bbc06fff fix: focus is clickable if text field is being edited (#3020) 2023-07-18 18:25:25 +08:00
unknown f6edd4b32b fix: pasting a link in a URL grid crashes the app 2023-07-18 17:04:19 +07:00
Lucas.Xu 706a5e784f fix: cannot click on links (#3017) 2023-07-18 15:59:41 +08:00
Mayur Mahajan 1f720efc8a chore: clean unnecessary todos (#3018) 2023-07-18 10:47:55 +08:00
Mathias Mogensen 778e462523 feat: enable reordering properties from settings 2023-07-18 10:47:42 +08:00
Lucas.Xu 2da37122e4 feat: support toggle list (#3016) 2023-07-18 10:45:20 +08:00
Serge Brainin a00dd5498e feat: the application remembers being resized (#2907) 2023-07-18 10:45:06 +08:00
Yatendra Kumar 7dcc7c221f fix: row banner overlay (#3009) 2023-07-17 17:39:44 +08:00
Muhammad Usman Shahid bcd116797d chore: added a tooltip to theme mode for toggling theme 2023-07-17 17:39:20 +08:00
Harmon 70915df5ad fix: add newlines after device info entries for debug info (#3008) 2023-07-17 10:06:43 +08:00
Mayur Mahajan 1b582c3870 chore: change github logo (#3003) 2023-07-15 09:46:08 +08:00
Lucas.Xu 26f078128a test: add an option test (#3006) 2023-07-15 09:45:44 +08:00
Kilu.He c65584d23c feat: support navigator and trash page
* refactor: navigator

* feat: support trash
2023-07-14 20:33:22 +08:00
Lucas.Xu 098c085d96 chore: upgrade editor to 1.2.1 (#2997) 2023-07-14 17:08:40 +08:00
Richard Shiue 39988f6785 fix: field hide/delete icon color in dark mode (#2998) 2023-07-14 17:07:57 +08:00
GitStartandgitstart e69e72e3bd fix: Filter UI popover pixels overflow with dynamic width and height (#2554)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
2023-07-14 11:06:32 +02:00
Nathan.fooo f9e7b5ffa4 feat: reload UI (#2999)
* chore: reload folder

* chore: reload folder

* chore: init sync

* chore: update tables

* chore: update database

* chore: load row

* chore: update

* chore: reload row

* test: fit test

* chore: retry

* chore: support batch fetch

* chore: enable sync

* chore: sync switch

* chore: sync switch

* chore: migration user data

* chore: migrate data

* chore: migrate folder

* chore: save user email

* chore: refresh user profile

* chore: fix test

* chore: delete translation files

* test: clippy format
2023-07-14 13:37:13 +08:00
Mathias Mogensen 5085ea115f feat: alt+click to add block above (#2988) 2023-07-13 18:33:16 +08:00
Daiki Nakashita c12aa66b2a fix: wrong grid row count (#2973) 2023-07-13 11:01:22 +08:00
Mohammad ZolfaghariandLucas.Xu d4a23657d7 fix: send database notification on create row (#2926)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-07-13 09:39:16 +08:00
Mathias Mogensen 5b1afeb85d feat: open apps in tabs (#2962)
* feat: open apps in tabs

Closes: #2942 Relates: #2312

* fix: resolve comments

* fix: unfocus editor to close toolbar on open/change tab

* test: abstract open in a new tab helper
2023-07-12 13:13:18 +02:00
Richard Shiue d0747afbf2 fix: use same base width for all screen sizes (#2961) 2023-07-12 17:30:37 +08:00
Ikko Eltociear Ashimine 88405ad316 chore: fix typo in Dockerfile (#2966) 2023-07-12 17:28:53 +08:00
Roopesh Saravanan 5f3f681591 feat: add installation script for linux users (#2976) 2023-07-12 17:28:17 +08:00
Kilu.He 0dae8cf2f9 feat: support i18n in typescript (#2948) 2023-07-12 11:54:50 +08:00
Lucas.Xu 3d72b6fa12 chore: move language files to the root directory to share them with both Flutter and Tauri. (#2970) 2023-07-11 20:27:40 +08:00
Lucas.Xu 2c669f1b85 chore: bump version 0.2.6 (#2963) 2023-07-11 19:49:48 +08:00
Lucas.Xu b9976b072e fix: unable to cancel the inline math equation format (#2974)
* fix: unable to cancel the inline math equation format

* fix: bold font style used in the document is not suitable

* chore: add flutter clean before executing the release task

* fix: integration test
2023-07-11 19:49:29 +08:00
Felix HäberleandNathan.fooo 409e3bf1e3 feat: update translations (#2823)
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2023-07-10 21:37:22 +08:00
Lucas.Xu ff9b3c56c5 feat:IInline math equation (#2949) 2023-07-09 11:03:22 +08:00
Kilu.He 4c17298432 feat: support dark mode (#2945) 2023-07-07 21:58:15 +08:00
Kilu.He 0401fc4b22 fix: tauri check user failed (#2946) 2023-07-06 16:40:20 +08:00
Mathias Mogensen 4805d0c568 Merge pull request #2939 from MayurSMahajan/fr_toggle_theme_shortcut_2304
feat: toggle theme mode shortcut
2023-07-06 09:34:00 +02:00
Nathan.foooandLucas.Xu edc7933c66 feat: support pg storage (#2935)
* refactor: using tokio-postgres

* chore: update

* chore: update env

* chore: update

* chore: upgrade supabase and add logout button

* refactor: update

* chore: update

* refactor: using message queue to handle the pg connection

* refactor: move test

* refactor: update sql

* chore: create pg database when user login

* chore: update scheme

* chore: generic user service

* chore: update

* chore: create statistics

* chore: create snapshot

* chore: add test

* chore: add database cloud service

* chore: add document cloud service

* chore: update interface

* test: add document test

* refactor: document interface

* chore: fix test

* chore: update

* chore: update test

* test: add test

* test: add test

* test: add test

* chore: update collab rev

* fix: flutter analyzer

* chore: update

* chore: update

* chore: update

* fix: tests

* chore: update

* chore: update collab rev

* ci: rust fmt

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-07-05 20:57:09 +08:00
MayurSMahajan 89ff44b035 test: hotkeys integration test 2023-07-05 17:46:10 +05:30
MayurSMahajan 793884427c feat: toggle theme mode shortcut 2023-07-05 10:58:36 +05:30
Richard Shiue e0ad364fa3 test: improve stability of calendar integration test (#2930) 2023-07-05 11:05:05 +08:00
Richard Shiue 5d76a7864d chore: restore default vscode build task (#2931) 2023-07-05 11:04:18 +08:00
Alex WallenandYijing Huang 323cb3b60f [feat] Allow user to select any Google Font (#2895)
* chore: add label for font selection drop down

* chore: add method to set font family

* feat: add drop down to setting appearance view

* feat: add fontFamily to document appearance cubit

* feat: add bloc provider to root for document appearance style

* feat: syncFont family from setting appearance dialog

* feat: plumbing for font style in editor

* fix: add blocprovider before pushing overlay

* chore: add kv_keys

* fix: use fontFamily in document appearance cubit

* fix: remove bloc providers because bloc is supplied in ancestor

* fix: remove unecessary bloc provider

* chore: add constraints to popover

* chore: add translation for search box

* feat: add levenshtein for string sort

* feat: add search bar view

* refactor: levenshtein

* chore: add tests for levenshtein algorithm

* feat: add unit tests for appearance cubit

* fix: analyzer warnings

* feat: sort by ascending if query is empty

* chore: add test for the font family setting widget

* feat: make comparison case insensitive

* feat: lazy load with listview.builder

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: fonts loaded on open application

* fix: checkmark doesn't show

* fix: try catch before getFont

* fix: clear text editing value on close

* fix: remove autofocus for search text field

* chore: add tests

* feat: use sliver protocol

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: avoid using intrinsic height

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: extra paren caused build failure

* feat: switch order of font family setting

---------

Co-authored-by: Yijing Huang <hyj891204@gmail.com>
2023-07-04 11:30:38 -10:00
Kilu.He 9fb8f221cf chore: add document test (#2932) 2023-07-04 17:17:25 +08:00
8dfbfe3c42 feat: Dynamically Load Themes in AppFlowy (#2670)
* feat: dynamic theme plugin (init)

* feat: provide fallback color if plugin becomes out of date (transparent)

* feat: use applicationDocumentsDirectory to store plugins

* chore: remove json files

* fix: add toJson to resolve analyzer errors

* fix: analyzer (unused imports)

* feat: add code generation scripts for freezed files (call recursively in packages)

* fix: revert changes to dry generation

* feat: call directly into script

* refactor: scripts try to be stateless :)

* fix: path to code generation in toml

* fix: generate script permissions

* fix: path not correct in generate.sh

* feat: modify execution permissions before executing scripts

* chore: switch order of build_runner and easy_localizations

* fix: fs is not valid duckscript cmd

* chore: clean build_runner before executing

* chore: upgrade freezed and build_runner attempt to resolve InvalidType error

* fix: use exec cmd.exe to chmod

* feat: add task to generate all files

* chore: remove redundant task (Code Gen)

* chore: remove json_annoation to dev_dependencies

* fix: dropped & between commands

* chore: rename file and class to FlowyDynamicPlugin

* fix: dependency hell

* fix: json annotation in colorscheme

* fix: analyzer warnings

* fix: duckscript runner for code generator

* fix: try without setting file permissions

* chore: move file picker to infra

* chore: restructure project directory

* feat: add BLoC components for consumers

* chore: update dependencies in pubspec.yaml file

* fix: file picker imports

* feat: add new translations for features

* feat: add new widgets to render upload

* fix: import

* feat: add text overflow

* feat: use animated switcher

* chore: export FileType

* fix: directory was not created, only files were copied

* chore: separate some logic

* feat: add saveFile to FilePickerService

* fix: analyzer error with unused imports

* feat: add translations for uploading

* feat: add builtins property to apptheme

* feat: add theme preview widget

* fix: upload widgets need to fill whole space and account for overflow

* refactor: do not watch file system for changes

* feat: add deletion confirmation dialog

* feat: add form factor resolution for dyanmic layout

* feat: trigger rebuild only when plugins are loaded

* feat: make all methods static

* chore: remove TODO comment, requires further design

* chore: move models to subfolder

* fix: references to plugin service instance

* fix: rebase errors

* fix: more rebasing errors

* feat: remove multiple themes from one plugin

* refactor: use pattern to resolve widget in settings_appearance_view

* refactor: remove commented code

* feat: add translations

* fix: import error

* refactor: separate concerns a bit more

* fix: bug in toJson serialization code

* feat: add package to use represent memory files

* fix: analyzer warnings

* chore: add translation

* chore: remove unused exceptions

* chore: use join

* chore: add documentation

* feat: add tests on theme

* fix: fix scripts for macOS

* feat: use appFlowyDocumentDirectory

* fix: remove unused import

* fix: imports

* feat: allow plugin service to be passed

* fix: theme tests

* feat: separate themes by built-in and plugin

* fix: rebase change name of appFlowyDocumentDirectory

* chore: add test to check that initial state falls back to initial theme

* chore: theme upload preview widget

* chore: rename to brightness setting

* refactor: appearance for settings appearance view

* feat: change show dialog api and use it

* fix: handle plugin compilation exception when incorrect format supplied

* fix: style of theme upload

* fix: always change state so that ui updates

* chore: style of loading widget

* fix: analyzer errors

* feat: add learn more button to documentation

---------

Co-authored-by: Yijing Huang <hyj891204@gmail.com>
Co-authored-by: nathan <nathan@appflowy.io>
2023-07-03 22:07:11 +08:00
Nathan.fooo 453be8352a chore: add ref (#2927) 2023-07-03 11:14:45 +08:00
Kilu.He 452d7eb6d0 feat: support image block (#2912) 2023-07-03 10:04:40 +08:00
f0d5f51703 fix: windows integration test (#2917)
* fix:windows integration test

* fix: load asset

* fix: windows test

* fix: test

* test: refactor the folder test

---------

Co-authored-by: vedon <vedon.fu@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-07-02 23:37:30 +08:00
Lucas.Xu 2c513be305 chore: bump version 0.2.5 (#2924) 2023-07-02 15:15:57 +08:00
Lucas.Xu 11d05b303d feat: support inserting local image (#2913) 2023-07-02 11:46:45 +08:00
Richard Shiue c870dbeac4 test: add more cover integration tests (#2922) 2023-07-01 23:13:09 +08:00
Richard ShiueandLucas.Xu 3acd36e580 test: fix calendar integration test #2919 (#2920)
* test: fix calendar integration test #2919

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

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

* test: fix date cell test

---------

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

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

* chore: refactor cover node widget

* chore: use a constant instead of magic value

* fix: make the size of icon hover effect smaller

* chore: improve appearance of selected color

* test: add cover integration tests

* fix: inner ring of selected color in dark mode

* refactor: cover node to document header node

* test: simplify tests

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

* fix: revert changes in Cargo.toml

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

* feat: translate to feedback
2023-06-27 09:53:38 +08:00
Nathan.fooo 88faa0f02a feat: using cached view (#2877) 2023-06-23 23:19:34 +08:00
758 changed files with 33457 additions and 10318 deletions

No files matched your search

+1 -1
View File
@@ -134,6 +134,6 @@ jobs:
fail_ci_if_error: true
verbose: true
os: ${{ matrix.os }}
attempt_limit: 5
attempt_limit: 20
attempt_delay: 10000
+20 -19
View File
@@ -29,25 +29,31 @@ concurrency:
cancel-in-progress: true
jobs:
tests:
build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
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
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
target: ${{ matrix.target }}
override: true
profile: minimal
@@ -64,6 +70,7 @@ jobs:
prefix-key: ${{ matrix.os }}
workspaces: |
frontend/rust-lib
cache-all-crates: true
- uses: davidB/rust-cargo-make@v1
with:
@@ -87,30 +94,24 @@ 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
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
@@ -135,5 +136,5 @@ jobs:
fail_ci_if_error: true
verbose: true
os: ${{ matrix.os }}
attempt_limit: 5
attempt_limit: 20
attempt_delay: 10000
+28
View File
@@ -1,5 +1,33 @@
# Release Notes
## Version 0.2.7 - 07/18/2023
### New Features
- Dynamic load themes
- Inline math equation
## Version 0.2.6 - 07/11/2023
### New Features
- 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:
+31 -10
View File
@@ -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"
}
},
]
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.2.4"
CURRENT_APP_VERSION = "0.2.7"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
+3
View File
@@ -70,7 +70,10 @@ windows/flutter/dart_ffi/
**/.vscode/
*.env
*.env.*
coverage/
**/failures/*.png
assets/translations/
@@ -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

@@ -0,0 +1,4 @@
<svg width="207" height="205" viewBox="0 0 207 205" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="103.5" cy="102.5" rx="103.5" ry="102.5" fill="white"/>
<path d="M102.752 16C53.1943 16 13.0083 55.0187 13.0083 103.152C13.0083 141.658 38.7225 174.327 74.3807 185.851C78.8658 186.658 80.5127 183.96 80.5127 181.658C80.5127 179.58 80.4286 172.715 80.3907 165.433C55.4239 170.704 50.1557 155.15 50.1557 155.15C46.0731 145.076 40.1907 142.397 40.1907 142.397C32.0486 136.988 40.8049 137.1 40.8049 137.1C49.8163 137.714 54.5615 146.08 54.5615 146.08C62.5662 159.404 75.5565 155.552 80.6775 153.325C81.4831 147.693 83.8087 143.847 86.3755 141.671C66.442 139.468 45.4876 131.994 45.4876 98.6005C45.4876 89.0853 48.9932 81.3105 54.734 75.2072C53.8022 73.012 50.7306 64.1484 55.6034 52.1433C55.6034 52.1433 63.1397 49.8011 80.2898 61.077C87.4482 59.1461 95.1254 58.1779 102.752 58.1445C110.379 58.1779 118.063 59.1454 125.234 61.077C142.363 49.8018 149.889 52.144 149.889 52.144C154.774 64.1478 151.701 73.0127 150.769 75.2072C156.523 81.3105 160.005 89.0853 160.005 98.6005C160.005 132.074 139.011 139.445 119.027 141.602C122.245 144.307 125.114 149.611 125.114 157.742C125.114 169.402 125.01 178.787 125.01 181.658C125.01 183.978 126.625 186.695 131.174 185.838C166.813 174.302 192.495 141.646 192.495 103.153C192.495 55.0187 152.314 16 102.752 16ZM46.6206 140.15C46.4229 140.583 45.7211 140.713 45.0824 140.415C44.431 140.132 44.0658 139.541 44.2761 139.107C44.4696 138.661 45.1728 138.536 45.8221 138.835C46.4741 139.12 46.8457 139.716 46.6206 140.15ZM51.0349 143.975C50.6072 144.36 49.7701 144.181 49.2022 143.573C48.6153 142.965 48.5052 142.152 48.9392 141.762C49.3809 141.376 50.1921 141.557 50.7804 142.164C51.3679 142.779 51.4815 143.586 51.0349 143.975ZM54.0637 148.869C53.5133 149.24 52.6145 148.892 52.0585 148.118C51.5088 147.343 51.5088 146.413 52.0704 146.041C52.6278 145.668 53.5133 146.003 54.0763 146.773C54.6246 147.561 54.6246 148.49 54.063 148.87L54.0637 148.869ZM59.1847 154.537C58.6932 155.064 57.645 154.923 56.878 154.204C56.0935 153.501 55.8754 152.504 56.369 151.977C56.8668 151.449 57.9206 151.597 58.6932 152.311C59.4714 153.012 59.7098 154.017 59.1847 154.537ZM65.8047 156.451C65.5873 157.134 64.5777 157.444 63.5618 157.154C62.5466 156.855 61.8826 156.056 62.0873 155.366C62.2984 154.678 63.3122 154.356 64.3358 154.666C65.3496 154.963 66.0157 155.756 65.8047 156.451ZM73.3375 157.263C73.3627 157.981 72.501 158.577 71.4339 158.591C70.3612 158.614 69.4925 158.032 69.4806 157.324C69.4806 156.599 70.3234 156.009 71.3968 155.991C72.4639 155.971 73.3375 156.548 73.3375 157.263ZM80.7378 156.987C80.8654 157.689 80.1243 158.409 79.0642 158.601C78.023 158.785 77.0583 158.352 76.9258 157.657C76.7968 156.938 77.5519 156.218 78.5917 156.032C79.6532 155.853 80.6025 156.274 80.7378 156.987Z" fill="#161614"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -1,153 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Jo",
"welcomeText": "Benvingut a @:appName",
"githubStarText": "Preferit a Github",
"subscribeNewsletterText": "Subscriu-me al butlletí",
"letsGoButtonText": "Endavant",
"title": "Títol",
"signUp": {
"buttonText": "Registra't",
"title": "Registra't a @:appName",
"getStartedText": "Comencem",
"emptyPasswordError": "La contrasenya no pot ser buida",
"repeatPasswordEmptyError": "La contrasenya repetida no pot ser buida",
"unmatchedPasswordError": "Les contrasenyes no concorden",
"alreadyHaveAnAccount": "Ja tens un compte?",
"emailHint": "Correu electrònic",
"passwordHint": "Contrasenya",
"repeatPasswordHint": "Repeteix la contrasenya"
},
"signIn": {
"loginTitle": "Inicia sessió a @:appName",
"loginButtonText": "Inicia sessió",
"buttonText": "Inicia sessió",
"forgotPassword": "Has oblidat la contrasenya?",
"emailHint": "Correu electrònic",
"passwordHint": "Contrasenya",
"dontHaveAnAccount": "No tens un compte?",
"repeatPasswordEmptyError": "La contrasenya repetida no pot ser buida",
"unmatchedPasswordError": "Les contrasenyes no concorden"
},
"workspace": {
"create": "Crear un espai de treball",
"hint": "espai de treball",
"notFoundError": "No s'ha trobat l'espai de treball"
},
"shareAction": {
"buttonText": "Compartir",
"workInProgress": "Pròximament",
"markdown": "Markdown",
"copyLink": "Copiar l'enllaç"
},
"disclosureAction": {
"rename": "Canviar el nom",
"delete": "Eliminar",
"duplicate": "Duplicar"
},
"blankPageTitle": "Pàgina en blanc",
"newPageText": "Nova pàgina",
"trash": {
"text": "Paperera",
"restoreAll": "Recuperar-ho tot",
"deleteAll": "Eliminar-ho tot",
"pageHeader": {
"fileName": "Nom del fitxer",
"lastModified": "Última modificació",
"created": "Creat"
}
},
"deletePagePrompt": {
"text": "Aquest pàgina es troba a la paperera",
"restore": "Recuperar-la",
"deletePermanent": "Elimina-la"
},
"dialogCreatePageNameHint": "Nom de la pàgina",
"questionBubble": {
"whatsNew": "Què hi ha de nou?",
"help": "Ajuda i Suport",
"debug": {
"name": "Informació de depuració",
"success": "S'ha copiat la informació de depuració!",
"fail": "No es pot copiar la informació de depuració"
}
},
"menuAppHeader": {
"addPageTooltip": "Afegeix ràpidament una pàgina dins",
"defaultNewPageName": "Sense títol",
"renameDialog": "Canviar el nom"
},
"toolbar": {
"undo": "Desfer",
"redo": "Refer",
"bold": "Negreta",
"italic": "Cursiva",
"underline": "Text subratllar",
"strike": "Ratllat",
"numList": "Llista numerada",
"bulletList": "Llista de punts",
"checkList": "Llista de comprovació",
"inlineCode": "Inserir codi",
"quote": "Bloc citat",
"header": "Capçalera",
"highlight": "Subratllar"
},
"tooltip": {
"lightMode": "Canviar a mode clar",
"darkMode": "Canviar a mode fosc"
},
"contactsPage": {
"title": "Contactes",
"whatsHappening": "Que passa aquesta setmana?",
"addContact": "Afegir un contacte",
"editContact": "Editar un contacte"
},
"button": {
"OK": "OK",
"Cancel": "Cancel·lar",
"signIn": "Iniciar sessió",
"signOut": "Tancar sessió",
"complete": "Completar",
"save": "Guardar"
},
"label": {
"welcome": "Benvingut!",
"firstName": "Nom",
"middleName": "Segon Nom",
"lastName": "Cognom",
"stepX": "Pas {X}"
},
"oAuth": {
"err": {
"failedTitle": "No s'ha pogut connectar al teu compte.",
"failedMsg": "Assegureu-vos que heu completat el procés d'inici de sessió al vostre navegador."
},
"google": {
"title": "Iniciar sessió amb Google",
"instruction1": "Per importar els vostres contactes de Google, haureu d'autoritzar aquesta aplicació mitjançant el vostre navegador web.",
"instruction2": "Copia aquest codi clicant la icona o seleccionant el text:",
"instruction3": "Navega al següent enllaç amb el teu navegador i insereix el codi anterior:",
"instruction4": "Pressiona el botó d'avall una vegada hagis completat el registre:"
}
},
"settings": {
"title": "Configuració",
"menu": {
"appearance": "Aparença",
"language": "Idioma",
"open": "Obrir la configuració"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Mode Clar",
"dark": "Mode Fosc",
"system": "Adapt to System"
}
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,159 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Ich",
"welcomeText": "Willkommen bei @:appName",
"githubStarText": "GitHub Star vergeben",
"subscribeNewsletterText": "Abonniere den Newsletter",
"letsGoButtonText": "Los geht's",
"title": "Titel",
"signUp": {
"buttonText": "Registrieren",
"title": "Registriere dich bei @:appName",
"getStartedText": "Erste Schritte",
"emptyPasswordError": "Passwort darf nicht leer sein",
"repeatPasswordEmptyError": "Passwortwiederholung darf nicht leer sein",
"unmatchedPasswordError": "Passwörter stimmen nicht überein",
"alreadyHaveAnAccount": "Bereits registriert?",
"emailHint": "E-Mail",
"passwordHint": "Passwort",
"repeatPasswordHint": "Wiederhole Passwort"
},
"signIn": {
"loginTitle": "Bei @:appName einloggen",
"loginButtonText": "Anmelden",
"buttonText": "Anmelden",
"forgotPassword": "Passwort vergessen?",
"emailHint": "E-Mail",
"passwordHint": "Passwort",
"dontHaveAnAccount": "Du besitzt noch kein Konto?",
"repeatPasswordEmptyError": "Passwortwiederholung darf nicht leer sein",
"unmatchedPasswordError": "Passwörter stimmen nicht überein"
},
"workspace": {
"create": "Arbeitsbereich erstellen",
"hint": "Arbeitsbereich",
"notFoundError": "Arbeitsbereich nicht gefunden"
},
"shareAction": {
"buttonText": "Teilen",
"workInProgress": "Demnächst verfügbar",
"markdown": "Markdown",
"copyLink": "Link kopieren"
},
"disclosureAction": {
"rename": "Umbenennen",
"delete": "Löschen",
"duplicate": "Duplizieren"
},
"blankPageTitle": "Leere Seite",
"newPageText": "Neue Seite",
"trash": {
"text": "Papierkorb",
"restoreAll": "Alles wiederherstellen",
"deleteAll": "Alles löschen",
"pageHeader": {
"fileName": "Dateiname",
"lastModified": "Letzte Änderung",
"created": "Erstellt"
}
},
"deletePagePrompt": {
"text": "Diese Seite ist im Papierkorb",
"restore": "Seite wiederherstellen",
"deletePermanent": "Dauerhaft löschen"
},
"dialogCreatePageNameHint": "Seitenname",
"questionBubble": {
"whatsNew": "Was gibt es Neues?",
"help": "Hilfe & Support",
"debug": {
"name": "Debug-Informationen",
"success": "Debug-Informationen in die Zwischenablage kopiert!",
"fail": "Debug-Informationen können nicht in die Zwischenablage kopiert werden"
},
"shortcuts": "Abkürzungen"
},
"menuAppHeader": {
"addPageTooltip": "Schnell eine Seite innerhalb hinzufügen",
"defaultNewPageName": "Unbenannt",
"renameDialog": "Umbenennen"
},
"toolbar": {
"undo": "Rückgängig",
"redo": "Wiederherstellen",
"bold": "Fett",
"italic": "Kursiv",
"underline": "Unterstreichen",
"strike": "Durchstreichen",
"numList": "Nummerierte Liste",
"bulletList": "Aufzählung",
"checkList": "Checkliste",
"inlineCode": "Inline-Code",
"quote": "Zitat",
"header": "Überschrift",
"highlight": "Hervorhebung",
"color": "Farbe"
},
"tooltip": {
"lightMode": "In den hellen Modus wechseln",
"darkMode": "In den dunklen Modus wechseln",
"openAsPage": "Als Seite öffnen"
},
"contactsPage": {
"title": "Kontakte",
"whatsHappening": "Was geschieht diese Woche?",
"addContact": "Kontakt hinzufügen",
"editContact": "Kontakt bearbeiten"
},
"button": {
"OK": "OK",
"Cancel": "Abbrechen",
"signIn": "Anmelden",
"signOut": "Abmelden",
"complete": "Fertig",
"save": "Speichern"
},
"label": {
"welcome": "Willkommen!",
"firstName": "Vorname",
"middleName": "Zweiter Vorname",
"lastName": "Nachname",
"stepX": "Schritt {X}"
},
"oAuth": {
"err": {
"failedTitle": "Keine Verbindung zu Ihrem Konto möglich.",
"failedMsg": "Bitte vergewissern Sie sich, dass Sie den Anmeldevorgang in Ihrem Browser abgeschlossen haben."
},
"google": {
"title": "GOOGLE ANMELDUNG",
"instruction1": "Um Ihre Google-Kontakte zu importieren, müssen Sie diese Anwendung über Ihren Webbrowser autorisieren.",
"instruction2": "Kopieren Sie diesen Code in Ihre Zwischenablage, indem Sie auf das Symbol klicken oder den Text auswählen:",
"instruction3": "Rufen Sie den folgenden Link in Ihrem Webbrowser auf, und geben Sie den obigen Code ein:",
"instruction4": "Klicken Sie unten auf die Schaltfläche, wenn Sie die Anmeldung abgeschlossen haben:"
}
},
"settings": {
"title": "Einstellungen",
"menu": {
"appearance": "Aussehen",
"language": "Sprache",
"open": "Einstellungen öffnen"
},
"appearance": {
"lightLabel": "Heller Modus",
"darkLabel": "Dunkler Modus"
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
},
"moreAction": {
"small": "klein",
"medium": "mittel",
"large": "groß",
"fontSize": "Schriftgröße",
"import": "Importieren"
}
}
@@ -1,231 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Mi",
"welcomeText": "Bienvenido a @:appName",
"githubStarText": "Favorito en GitHub",
"subscribeNewsletterText": "Suscribir al boletín",
"letsGoButtonText": "Vamos",
"title": "Título",
"signUp": {
"buttonText": "Registrar",
"title": "Registrar en @:appName",
"getStartedText": "Empezar",
"emptyPasswordError": "La contraseña no puede estar en blanco",
"repeatPasswordEmptyError": "La contraseña no puede estar en blanco",
"unmatchedPasswordError": "Las contraseñas no coinciden",
"alreadyHaveAnAccount": "¿Posee credenciales?",
"emailHint": "Correo",
"passwordHint": "Contraseña",
"repeatPasswordHint": "Repetir contraseña"
},
"signIn": {
"loginTitle": "Ingresa a @:appName",
"loginButtonText": "Ingresar",
"buttonText": "Ingresar",
"forgotPassword": "¿Olvidó su contraseña?",
"emailHint": "Correo",
"passwordHint": "Contraseña",
"dontHaveAnAccount": "¿No posee credenciales?",
"repeatPasswordEmptyError": "La contraseña no puede estar en blanco",
"unmatchedPasswordError": "Las contraseñas no coinciden"
},
"workspace": {
"create": "Crear espacio de trabajo",
"hint": "Espacio de trabajo",
"notFoundError": "Espacio de trabajo no encontrado"
},
"shareAction": {
"buttonText": "Compartir",
"workInProgress": "Próximamente",
"markdown": "Marcador",
"copyLink": "Copiar enlace"
},
"disclosureAction": {
"rename": "Renombrar",
"delete": "Eliminar",
"duplicate": "Duplicar"
},
"blankPageTitle": "Página en blanco",
"newPageText": "Nueva página",
"trash": {
"text": "Papelera",
"restoreAll": "Recuperar todo",
"deleteAll": "Eliminar todo",
"pageHeader": {
"fileName": "Nombre de archivo",
"lastModified": "Última modificación",
"created": "Creado"
}
},
"deletePagePrompt": {
"text": "Esta página está en la Papelera",
"restore": "Recuperar página",
"deletePermanent": "Eliminar permanentemente"
},
"dialogCreatePageNameHint": "Nombre de página",
"questionBubble": {
"whatsNew": "¿Qué hay de nuevo?",
"help": "Ayuda y Soporte",
"debug": {
"name": "Información de depuración",
"success": "¡Información copiada!",
"fail": "No fue posible copiar la información"
}
},
"menuAppHeader": {
"addPageTooltip": "Inserta una página",
"defaultNewPageName": "Sin Título",
"renameDialog": "Renombrar"
},
"toolbar": {
"undo": "Deshacer",
"redo": "Rehacer",
"bold": "Negrita",
"italic": "Cursiva",
"underline": "Subrayado",
"strike": "Tachado",
"numList": "Lista numerada",
"bulletList": "Lista con viñetas",
"checkList": "Lista de verificación",
"inlineCode": "Código embebido",
"quote": "Cita",
"header": "Título",
"highlight": "Resaltado",
"color": "Color"
},
"tooltip": {
"lightMode": "Cambiar a modo Claro",
"darkMode": "Cambiar a modo Oscuro"
},
"notifications": {
"export": {
"markdown": "Nota exportada a Markdown",
"path": "Documentos/flowy"
}
},
"contactsPage": {
"title": "Contactos",
"whatsHappening": "¿Qué está pasando esta semana?",
"addContact": "Agregar Contacto",
"editContact": "Editar Contacto"
},
"button": {
"OK": "OK",
"Cancel": "Cancelar",
"signIn": "Ingresar",
"signOut": "Salir",
"complete": "Completar",
"save": "Guardar"
},
"label": {
"welcome": "¡Bienvenido!",
"firstName": "Primer nombre",
"middleName": "Segundo nombre",
"lastName": "Apellido",
"stepX": "Paso {X}"
},
"oAuth": {
"err": {
"failedTitle": "Imposible conectarse con sus credenciales.",
"failedMsg": "Por favor asegurese haber completado el proceso de ingreso en su navegador."
},
"google": {
"title": "Ingresar con Google",
"instruction1": "Para importar sus contactos de Google, debe autorizar esta aplicación usando su navegador web.",
"instruction2": "Copie este código al presionar el icono o al seleccionar el texto:",
"instruction3": "Navege al siguiente enlace en su navegador web, e ingrese el código anterior:",
"instruction4": "Presione el botón de abajo cuando haya completado su registro:"
}
},
"settings": {
"title": "Ajustes",
"menu": {
"appearance": "Apariencia",
"language": "Lenguaje",
"open": "Abrir ajustes"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Modo Claro",
"dark": "Modo Oscuro",
"system": "Adapt to System"
}
}
},
"grid": {
"settings": {
"filter": "Filtrar",
"sortBy": "Ordenar por",
"Properties": "Propiedades"
},
"field": {
"hide": "Ocultar",
"insertLeft": "Insertar a la Izquierda",
"insertRight": "Insertar a la Derecha",
"duplicate": "Duplicar",
"delete": "Eliminar",
"textFieldName": "Texto",
"checkboxFieldName": "Casilla de verificación",
"dateFieldName": "Fecha",
"numberFieldName": "Números",
"singleSelectFieldName": "Seleccionar",
"multiSelectFieldName": "Selección múltiple",
"urlFieldName": "URL",
"numberFormat": "Formato numérico",
"dateFormat": "Formato de fecha",
"includeTime": "Incluir tiempo",
"dateFormatFriendly": "Mes Día, Año",
"dateFormatISO": "Año-Mes-Día",
"dateFormatLocal": "Mes/Día/Año",
"dateFormatUS": "Año/Mes/Día",
"timeFormat": "Formato de tiempo",
"invalidTimeFormat": "Formato de tiempo inválido",
"timeFormatTwelveHour": "12 horas",
"timeFormatTwentyFourHour": "24 horas",
"addSelectOption": "Añadir una opción",
"optionTitle": "Opciones",
"addOption": "Añadir opción",
"editProperty": "Editar propiedad"
},
"row": {
"duplicate": "Duplicar",
"delete": "Eliminar",
"textPlaceholder": "Vacío",
"copyProperty": "Propiedad copiada al portapapeles"
},
"selectOption": {
"create": "Crear",
"purpleColor": "Morado",
"pinkColor": "Rosa",
"lightPinkColor": "Rosa Claro",
"orangeColor": "Naranja",
"yellowColor": "Amarillo",
"limeColor": "Lima",
"greenColor": "Verde",
"aquaColor": "Agua",
"blueColor": "Azul",
"deleteTag": "Borrar etiqueta",
"colorPanelTitle": "Colores",
"panelTitle": "Selecciona una opción o crea una",
"searchOption": "Buscar una opción"
},
"menuName": "Cuadrícula"
},
"document": {
"menuName": "Documento",
"date": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
}
},
"sideBar": {
"openSidebar": "Abrir panel lateral",
"closeSidebar": "Cerrar panel lateral"
},
"board": {
"column": {
"create_new_card": "Nuevo"
}
}
}
@@ -1,333 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Ni",
"welcomeText": "Ongietorri @:appName -ra",
"githubStarText": "Izarra GitHub-en",
"subscribeNewsletterText": "Harpidetu buletinera",
"letsGoButtonText": "Hasi",
"title": "Izenburua",
"signUp": {
"buttonText": "Izena eman",
"title": "Izena eman @:appName -ra",
"getStartedText": "Hasi",
"emptyPasswordError": "Pasahitzak ezin du hutsik egon",
"repeatPasswordEmptyError": "Pasahitz errepikapenak ezin du hutsik egon",
"unmatchedPasswordError": "Pasahitz errepikapena ez da berdina",
"alreadyHaveAnAccount": "Kontu bat duzu jada?",
"emailHint": "Emaila",
"passwordHint": "Pasahitza",
"repeatPasswordHint": "Pasahitza errepikatu"
},
"signIn": {
"loginTitle": "Hasi saioa @:appName -n",
"loginButtonText": "Hasi saioa",
"buttonText": "Sartu",
"forgotPassword": "Pasahitza ahaztu duzu?",
"emailHint": "Emaila",
"passwordHint": "Pasahitza",
"dontHaveAnAccount": "Ez daukazu konturik?",
"repeatPasswordEmptyError": "Pasahitz errepikapenak ezin du hutsik egon",
"unmatchedPasswordError": "Pasahitz errepikapena ez da berdina"
},
"workspace": {
"create": "Lan-eremua",
"hint": "lan-eremua",
"notFoundError": "Lan-eremurik ez da aurkitu"
},
"shareAction": {
"buttonText": "Konpartitu",
"workInProgress": "Laister",
"markdown": "Markdown",
"copyLink": "Esteka kopiatu"
},
"moreAction": {
"small": "txikia",
"medium": "ertaina",
"large": "handia",
"fontSize": "Letra tamaina"
},
"disclosureAction": {
"rename": "Izena aldatu",
"delete": "Ezabatu",
"duplicate": "Duplikatu"
},
"blankPageTitle": "Orri zuria",
"newPageText": "Orri berria",
"trash": {
"text": "Zaborrontzia",
"restoreAll": "Guztia berreskuratu",
"deleteAll": "Guztia ezabatu",
"pageHeader": {
"fileName": "Fitxategi izena",
"lastModified": "Azken aldaketa",
"created": "Sortua"
}
},
"deletePagePrompt": {
"text": "Orri hau zaborrontzian dago",
"restore": "Orria berreskuratu",
"deletePermanent": "Betirako ezabatu"
},
"dialogCreatePageNameHint": "Orriaren izena",
"questionBubble": {
"whatsNew": "Ze berri?",
"help": "Laguntza",
"debug": {
"name": "Debug informazioa",
"success": "Debug informazioa kopiatu da!",
"fail": "Ezin izan da debug informazioa kopiatu"
}
},
"menuAppHeader": {
"addPageTooltip": "Gehitu orri bat",
"defaultNewPageName": "Izenbururik ez",
"renameDialog": "Izena aldatu"
},
"toolbar": {
"undo": "Desegin",
"redo": "Berregin",
"bold": "Lodia",
"italic": "Etzana",
"underline": "Azpimarratua",
"strike": "Markatua",
"numList": "Zembakidun zerrenda",
"bulletList": "Buletetako zerrenda",
"checkList": "Egiaztapen zerrenda",
"inlineCode": "Lerroko kodea",
"quote": "Aipamena",
"header": "Goiburua",
"highlight": "Nabarmendu",
"color": "Kolorea"
},
"tooltip": {
"lightMode": "Modu argira aldatu",
"darkMode": "Modu ilunera aldatu",
"openAsPage": "Orri gisa ireki",
"addNewRow": "Ilara berri bat gehitu",
"openMenu": "Egin klik menua irekitzeko"
},
"sideBar": {
"closeSidebar": "Alboko barra itxi",
"openSidebar": "Alboko barra ireki"
},
"notifications": {
"export": {
"markdown": "Oharra markdownera esportatuta",
"path": "Documents/flowy"
}
},
"contactsPage": {
"title": "Kontaktuak",
"whatsHappening": "Ze berri aste honetan?",
"addContact": "Kontaktua gehitu",
"editContact": "Kontaktua editatu"
},
"button": {
"OK": "OK",
"Cancel": "Ezteztatu",
"signIn": "Saioa hasi",
"signOut": "Saioa itxi",
"complete": "Burututa",
"save": "Gorde"
},
"label": {
"welcome": "Ongi etorri!",
"firstName": "Izena",
"middleName": "Bigarren izena",
"lastName": "Abizena",
"stepX": "{X}. pausoa"
},
"oAuth": {
"err": {
"failedTitle": "Ezin izan da kontura sartu.",
"failedMsg": "Mesedez, ziurtatu zure arakatzailean saioa hasteko prozesua amaitu duzula."
},
"google": {
"title": "GOOGLE SAIOA HASI",
"instruction1": "Zure Google Kontaktuak inportatzeko, zure web arakatzailea erabiliz aplikazio hau baimendu beharko duzu.",
"instruction2": "Kopiatu kode hau ikonoan klik eginez edo testua hautatuz:",
"instruction3": "Nabigatu zure web arakatzailean esteka honetara eta idatzi goiko kodea:",
"instruction4": "Sakatu beheko botoia erregistroa amaitzean:"
}
},
"settings": {
"title": "Ezarpenak",
"menu": {
"appearance": "Itxura",
"language": "Hizkuntza",
"user": "Erabiltzailea",
"files": "Fitxategiak",
"open": "Ezarpenak ireki"
},
"appearance": {
"themeMode": {
"label": "Itxura modua",
"light": "Modu argia",
"dark": "Modu iluna",
"system": "Zure sistemara moldatu"
},
"theme": "Itxura"
},
"files": {
"defaultLocation": "Non gordetzen diren zure datuak",
"doubleTapToCopy": "Sakatu birritan bidea kopiatzeko",
"restoreLocation": "Berrezarri AppFlowy-ren biden lehenetsira",
"customizeLocation": "Beste karpeta bat ireki",
"restartApp": "Mesedez, berrabiarazi aplikazioa aldaketak indarrean egon daitezen.",
"exportDatabase": "Datubasea exportatu",
"selectFiles": "Aukeratu exportatu nahi dituzun fitxategiak",
"createNewFolder": "Karpeta berri bat sortu",
"createNewFolderDesc": "Non nahi dituzu datuak gorde ...",
"open": "Oreki",
"openFolder": "Ireki karpeta bat",
"openFolderDesc": "Irakurri eta idatzi zure AppFlowy karpetan...",
"folderHintText": "karpetaren izena",
"location": "Karpeta berria sortzen",
"locationDesc": "Aukeratu izen bat AppFlowy datuen karpetarako",
"browser": "Bilatu",
"create": "Sortu",
"folderPath": "Zure karpeta gordetzeko bidea",
"locationCannotBeEmpty": "Bideak ezin du hutsa egon"
}
},
"grid": {
"settings": {
"filter": "Filtroa",
"sort": "Ordenatu",
"sortBy": "Ordenatu honekiko",
"Properties": "Propietateak",
"group": "Taldea",
"addFilter": "Gehitu iragazkia",
"deleteFilter": "Ezabatu iragazkia",
"filterBy": "Iragazi arabera...",
"typeAValue": "Idatzi balio bat..."
},
"textFilter": {
"contains": "Dauka",
"doesNotContain": "Ez dauka",
"endsWith": "Honez amaitzen da",
"startWith": "Honez hasten da",
"is": "da",
"isNot": "Ez da",
"isEmpty": "Hutsa dago",
"isNotEmpty": "Ez dago hutsik",
"choicechipPrefix": {
"isNot": "Ez da",
"startWith": "Honez hasten da",
"endWith": "Honez amaitzen da",
"isEmpty": "hutsik dago",
"isNotEmpty": "ez dago hutsik"
}
},
"checkboxFilter": {
"isChecked": "Egiaztatuta",
"isUnchecked": "Desmarkatua",
"choicechipPrefix": {
"da": "da"
}
},
"checklistFilter": {
"isComplete": "osatu da",
"isIncomplted": "osatu gabe dago"
},
"singleSelectOptionFilter": {
"is": "da",
"isNot": "Ez da",
"isEmpty": "Hutsa dago",
"isNotEmpty": "Ez dago hutsik"
},
"multiSelectOptionFilter": {
"contains": "Duen",
"doesNotContain": "Ez dauka",
"isEmpty": "Hutsa dago",
"isNotEmpty": "Ez dago hutsik"
},
"field": {
"hide": "Ezkutatu",
"insertLeft": "Txertatu ezkerrera",
"insertRight": "Txertatu eskuinera",
"duplicate": "Bikoiztu",
"delete": "Ezabatu",
"textFieldName": "Testua",
"checkboxFieldName": "Markatu laukia",
"dateFieldName": "Data",
"numberFieldName": "Zenbakiak",
"singleSelectFieldName": "Hautatu",
"multiSelectFieldName": "Multi-hautaketa",
"urlFieldName": "URL",
"checklistFieldName": "Kontrol zerrenda",
"numberFormat": "Zenbaki formatua",
"dateFormat": "Data formatua",
"includeTime": "Sartu ordua",
"dateFormatFriendly": "Hilabete Eguna, Urtea",
"dateFormatISO": "Urtea-Hilabetea-Eguna",
"dateFormatLocal": "Hilabetea/Eguna/Urtea",
"dateFormatUS": "Urtea/Hilabetea/Eguna",
"timeFormat": "Denboraren formatua",
"invalidTimeFormat": "Formatu baliogabea",
"timeFormatTwelveHour": "12 ordu",
"timeFormatTwentyFourHour": "24 ordu",
"addSelectOption": "Gehitu aukera bat",
"optionTitle": "Aukerak",
"addOption": "Gehitu aukera",
"editProperty": "Editatu propietatea",
"newProperty": "Zutabe berria",
"deleteFieldPromptMessage": "Ziur al zaude? Propietate hau ezabatu egingo da"
},
"sort": {
"ascending": "Gorarantz",
"descending": "Jaisten",
"deleteSort": "Ezabatu ordena",
"addSort": "Gehitu ordenatu"
},
"row": {
"duplicate": "Bikoiztu",
"delete": "Ezabatu",
"textPlaceholder": "Hutsik",
"copyProperty": "Propietatea arbelean kopiatu da",
"count": "Kontatu",
"newRow": "Errenkada berria"
},
"selectOption": {
"create": "Sortu",
"purpleColor": "Purple",
"pinkColor": "Rosa",
"lightPinkColor": "Arrosa argia",
"orangeColor": "Laranja",
"yellowColor": "Horia",
"limeColor": "Lima",
"greenColor": "Berdea",
"aquaColor": "Aqua",
"blueColor": "Urdina",
"deleteTag": "Ezabatu etiketa",
"colorPanelTitle": "Koloreak",
"panelTitle": "Hautatu aukera bat edo sortu bat",
"searchOption": "Aukera bat bilatu"
},
"checklist": {
"panelTitle": "Gehitu elementu bat"
},
"menuName": "Sareta"
},
"document": {
"menuName": "Dokumentua",
"data": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
}
},
"board": {
"column": {
"create_new_card": "Berria"
}
},
"calendar": {
"menuName": "Egutegia",
"navigation": {
"today": "Gaur",
"jumpToday": "Gaurko egunera salto egin",
"previousMonth": "Aurreko hilabetea",
"nextMonth": "Hurrengo hilabetea"
}
}
}
@@ -1,153 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Moi",
"welcomeText": "Bienvenue sur @:appName",
"githubStarText": "Favoriser sur GitHub",
"subscribeNewsletterText": "Abonnez-vous à notre courriel",
"letsGoButtonText": "Allons-y",
"title": "Titre",
"signUp": {
"buttonText": "S'inscrire",
"title": "S'inscrire à @:appName",
"getStartedText": "Commencer",
"emptyPasswordError": "Vous n'avez pas saisi votre mot de passe",
"repeatPasswordEmptyError": "Mot de passe ne doit pas être vide",
"unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques",
"alreadyHaveAnAccount": "Avez-vous déjà un compte?",
"emailHint": "courriel",
"passwordHint": "Mot de passe",
"repeatPasswordHint": "Ressaisir votre mot de passe"
},
"signIn": {
"loginTitle": "Connexion à @:appName",
"loginButtonText": "Connexion",
"buttonText": "Se connecter",
"forgotPassword": "Mot de passe oublié?",
"emailHint": "courriel",
"passwordHint": "Mot de passe",
"dontHaveAnAccount": "Vous n'avez pas de compte?",
"repeatPasswordEmptyError": "Mot de passe ne doit pas être vide",
"unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques"
},
"workspace": {
"create": "Créer un espace de travail",
"hint": "Espace de travail",
"notFoundError": "Espace de travail introuvable"
},
"shareAction": {
"buttonText": "Partager",
"workInProgress": "Bientôt disponible",
"markdown": "Markdown",
"copyLink": "Copier le lien"
},
"disclosureAction": {
"rename": "Renommer",
"delete": "Supprimer",
"duplicate": "Dupliquer"
},
"blankPageTitle": "Page vierge",
"newPageText": "Nouvelle page",
"trash": {
"text": "Corbeille",
"restoreAll": "Tout récupérer",
"deleteAll": "Tout supprimer",
"pageHeader": {
"fileName": "Nom de fichier",
"lastModified": "Dernière modification",
"created": "Créé"
}
},
"deletePagePrompt": {
"text": "Cette page est dans la corbeille",
"restore": "Récupérer la page",
"deletePermanent": "Supprimer définitivement"
},
"dialogCreatePageNameHint": "Nom de la page",
"questionBubble": {
"whatsNew": "Nouveautés",
"help": "Aide et Support Technique",
"debug": {
"name": "Infos du système",
"success": "Info copié!",
"fail": "Impossible de copier l'info"
}
},
"menuAppHeader": {
"addPageTooltip": "Ajouter une page",
"defaultNewPageName": "Sans titre",
"renameDialog": "Renommer"
},
"toolbar": {
"undo": "Annuler",
"redo": "Rétablir",
"bold": "Gras",
"italic": "Italique",
"underline": "Souligner",
"strike": "Barré",
"numList": "Liste numérotée",
"bulletList": "Liste à puces",
"checkList": "Liste de contrôle",
"inlineCode": "Code en ligne",
"quote": "Citation",
"header": "En-tête",
"highlight": "Surligner"
},
"tooltip": {
"lightMode": "Passer en mode clair",
"darkMode": "Passer en mode sombre"
},
"contactsPage": {
"title": "Contacts",
"whatsHappening": "Quoi de neuf?",
"addContact": "Ajouter un contact",
"editContact": "Modifier le contact"
},
"button": {
"OK": "OK",
"Cancel": "Annuler",
"signIn": "Se connecter",
"signOut": "Se déconnecter",
"complete": "Achevé",
"save": "Sauvegarder"
},
"label": {
"welcome": "Bienvenue!",
"firstName": "Prénom",
"middleName": "Deuxième nom",
"lastName": "Nom de famille",
"stepX": "Étape {X}"
},
"oAuth": {
"err": {
"failedTitle": "Incapable de se connecter à votre compte.",
"failedMsg": "SVP vous assurrez d'avoir complèté le processus d'enregistrement dans votre fureteur."
},
"google": {
"title": "S'identifier avec Google",
"instruction1": "Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur Web.",
"instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:",
"instruction3": "Accédez au lien suivant dans votre navigateur Web et saisissez le code ci-dessus:",
"instruction4": "Appuyez sur le bouton ci-dessous lorsque vous avez terminé votre inscription:"
}
},
"settings": {
"title": "Paramètres",
"menu": {
"appearance": "Apparence",
"language": "Langue",
"open": "Ouvrir les paramètres"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Mode clair",
"dark": "Mode sombre",
"system": "Adapt to System"
}
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,239 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Moi",
"welcomeText": "Bienvenue sur @:appName",
"githubStarText": "Favoriser sur GitHub",
"subscribeNewsletterText": "S'inscrire à la Newsletter",
"letsGoButtonText": "Allons-y",
"title": "Titre",
"signUp": {
"buttonText": "S'inscrire",
"title": "Inscrivez-vous sur @:appName",
"getStartedText": "Commencer",
"emptyPasswordError": "Vous n'avez pas saisi votre mot de passe",
"repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe",
"unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques",
"alreadyHaveAnAccount": "Avez-vous déjà un compte ?",
"emailHint": "Email",
"passwordHint": "Mot de passe",
"repeatPasswordHint": "Ressaisir votre mot de passe"
},
"signIn": {
"loginTitle": "Connexion à @:appName",
"loginButtonText": "Connexion",
"buttonText": "Se connecter",
"forgotPassword": "Mot de passe oublié ?",
"emailHint": "Email",
"passwordHint": "Mot de passe",
"dontHaveAnAccount": "Vous n'avez pas encore créé votre compte ?",
"repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe",
"unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques"
},
"workspace": {
"create": "Créer un espace de travail",
"hint": "Espace de travail",
"notFoundError": "Espace de travail introuvable"
},
"shareAction": {
"buttonText": "Partager",
"workInProgress": "Bientôt disponible",
"markdown": "Markdown",
"copyLink": "Copier le lien"
},
"disclosureAction": {
"rename": "Renommer",
"delete": "Supprimer",
"duplicate": "Dupliquer"
},
"blankPageTitle": "Page vierge",
"newPageText": "Nouvelle page",
"trash": {
"text": "Corbeille",
"restoreAll": "Restaurer tout",
"deleteAll": "Supprimer tout",
"pageHeader": {
"fileName": "Nom de fichier",
"lastModified": "Dernière modification",
"created": "Créé"
}
},
"deletePagePrompt": {
"text": "Cette page a été supprimée, vous pouvez la retrouver dans la corbeille",
"restore": "Restaurer la page",
"deletePermanent": "Supprimer définitivement"
},
"dialogCreatePageNameHint": "Nom de la page",
"questionBubble": {
"whatsNew": "Nouveautés",
"help": "Aide et Support",
"debug": {
"name": "Informations de Débogage",
"success": "Informations de Débogage copiées dans le presse-papiers !",
"fail": "Impossible de copier les informations de Débogage dans le presse-papiers"
}
},
"menuAppHeader": {
"addPageTooltip": "Ajoutez rapidement une page à l'intérieur",
"defaultNewPageName": "Sans-titre",
"renameDialog": "Renommer"
},
"toolbar": {
"undo": "Annuler",
"redo": "Rétablir",
"bold": "Gras",
"italic": "Italique",
"underline": "Souligner",
"strike": "Barré",
"numList": "Liste numérotée",
"bulletList": "Liste à puces",
"checkList": "To-Do List",
"inlineCode": "Code",
"quote": "Bloc de citation",
"header": "En-tête",
"highlight": "Surligner"
},
"tooltip": {
"lightMode": "Passer en mode clair",
"darkMode": "Passer en mode sombre",
"openAsPage": "Ouvrir en tant que page",
"addNewRow": "Ajouter une ligne",
"openMenu": "Cliquer pour ouvrir le menu"
},
"sideBar": {
"closeSidebar": "Fermer le menu latéral",
"openSidebar": "Ouvrir le menu latéral"
},
"notifications": {
"export": {
"markdown": "Note exportée en Markdown",
"path": "Documents/flowy"
}
},
"contactsPage": {
"title": "Contacts",
"whatsHappening": "Que se passe-t-il cette semaine ?",
"addContact": "Ajouter un contact",
"editContact": "Modifier le contact"
},
"button": {
"OK": "OK",
"Cancel": "Annuler",
"signIn": "Se connecter",
"signOut": "Se déconnecter",
"complete": "Achevé",
"save": "Enregistrer"
},
"label": {
"welcome": "Bienvenue !",
"firstName": "Prénom",
"middleName": "Deuxième prénom",
"lastName": "Nom",
"stepX": "Étape {X}"
},
"oAuth": {
"err": {
"failedTitle": "Impossible de se connecter à votre compte.",
"failedMsg": "Assurez-vous d'avoir terminé le processus de connexion dans votre navigateur."
},
"google": {
"title": "CONNEXION VIA GOOGLE",
"instruction1": "Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur web.",
"instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:",
"instruction3": "Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus:",
"instruction4": "Appuyez sur le bouton ci-dessous lorsque vous avez terminé votre inscription:"
}
},
"settings": {
"title": "Paramètres",
"menu": {
"appearance": "Apparence",
"language": "Langue",
"user": "Utilisateur",
"open": "Ouvrir les paramètres"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Mode clair",
"dark": "Mode sombre",
"system": "Adapt to System"
}
}
},
"grid": {
"settings": {
"filter": "Filtrer",
"sortBy": "Filtrer par",
"Properties": "Propriétés",
"group": "Groupe"
},
"field": {
"hide": "Cacher",
"insertLeft": "Insérer à gauche",
"insertRight": "Insérer à droite",
"duplicate": "Dupliquer",
"delete": "Supprimer",
"textFieldName": "Texte",
"checkboxFieldName": "Case à cocher",
"dateFieldName": "Date",
"numberFieldName": "Nombre",
"singleSelectFieldName": "Sélectionner",
"multiSelectFieldName": "Multisélection",
"urlFieldName": "URL",
"numberFormat": "Format du nombre",
"dateFormat": "Format de la date",
"includeTime": "Inclure l'heure",
"dateFormatFriendly": "Mois Jour, Année",
"dateFormatISO": "Année-Mois-Jour",
"dateFormatLocal": "Mois/Jour/Année",
"dateFormatUS": "Année/Mois/Jour",
"timeFormat": "Format du temps",
"invalidTimeFormat": "Format invalide",
"timeFormatTwelveHour": "12 heures",
"timeFormatTwentyFourHour": "24 heures",
"addSelectOption": "Ajouter une option",
"optionTitle": "Options",
"addOption": "Ajouter une option",
"editProperty": "Modifier la propriété",
"newProperty": "Nouvelle colonne",
"deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?"
},
"row": {
"duplicate": "Dupliquer",
"delete": "Supprimer",
"textPlaceholder": "Vide",
"copyProperty": "Copie de la propriété dans le presse-papiers",
"count": "Nombre",
"newRow": "Nouvelle ligne"
},
"selectOption": {
"create": "Créer",
"purpleColor": "Violet",
"pinkColor": "Rose",
"lightPinkColor": "Rose clair",
"orangeColor": "Orange",
"yellowColor": "Jaune",
"limeColor": "Citron vert",
"greenColor": "Vert",
"aquaColor": "Aqua",
"blueColor": "Bleu",
"deleteTag": "Supprimer l'étiquette",
"colorPanelTitle": "Couleurs",
"panelTitle": "Sélectionnez une option ou créez-en une",
"searchOption": "Rechercher une option"
},
"menuName": "Grille"
},
"document": {
"menuName": "Document",
"date": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
}
},
"board": {
"column": {
"create_new_card": "Nouveau"
}
}
}
@@ -1,153 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Én",
"welcomeText": "Üdvözöl az @:appName",
"githubStarText": "GitHub csillagozás",
"subscribeNewsletterText": "Iratkozz fel a hírlevelünkre",
"letsGoButtonText": "Vágjunk bele",
"title": "Cím",
"signUp": {
"buttonText": "Regisztráció",
"title": "Regisztrálj az @:appName -ra",
"getStartedText": "Kezdés",
"emptyPasswordError": "A jelszó nem lehet üres",
"repeatPasswordEmptyError": "A jelszó megerősítése nem lehet üres",
"unmatchedPasswordError": "A jelszavak nem egyeznek",
"alreadyHaveAnAccount": "Rendelkezel már fiókkal?",
"emailHint": "Email",
"passwordHint": "Jelszó",
"repeatPasswordHint": "Jelszó megerősítése"
},
"signIn": {
"loginTitle": "Bejelentkezés az @:appName -ba",
"loginButtonText": "Belépés",
"buttonText": "Bejelentkezés",
"forgotPassword": "Elfelejtett jelszó?",
"emailHint": "Email",
"passwordHint": "Jelszó",
"dontHaveAnAccount": "Még nincs fiókod?",
"repeatPasswordEmptyError": "A jelszó megerősítése nem lehet üres",
"unmatchedPasswordError": "A jelszavak nem egyeznek"
},
"workspace": {
"create": "Új munkaterület létrehozása",
"hint": "munkaterület",
"notFoundError": "munkaterület nem található"
},
"shareAction": {
"buttonText": "Megosztás",
"workInProgress": "Hamarosan érkezik...",
"markdown": "Markdown",
"copyLink": "Link másolása"
},
"disclosureAction": {
"rename": "Átnevezés",
"delete": "Törlés",
"duplicate": "Duplikálás"
},
"blankPageTitle": "Üres oldal",
"newPageText": "Új oldal",
"trash": {
"text": "Kuka",
"restoreAll": "Összes visszaállítása",
"deleteAll": "Összes törlése",
"pageHeader": {
"fileName": "Fájlnév",
"lastModified": "Utoljára módosítva",
"created": "Létrehozva"
}
},
"deletePagePrompt": {
"text": "Ez az oldal a kukában van",
"restore": "Oldal visszaállítása",
"deletePermanent": "Végleges törlés"
},
"dialogCreatePageNameHint": "Oldalnév",
"questionBubble": {
"whatsNew": "Újdonságok",
"help": "Segítség & Támogatás",
"debug": {
"name": "Debug Információ",
"success": "Debug információ a vágólapra másolva",
"fail": "A Debug információ nem másolható a vágólapra"
}
},
"menuAppHeader": {
"addPageTooltip": "Belső oldal hozzáadása",
"defaultNewPageName": "Névtelen",
"renameDialog": "Átnevezés"
},
"toolbar": {
"undo": "Vissza",
"redo": "Előre",
"bold": "Félkövér",
"italic": "Dőlt",
"underline": "Aláhúzott",
"strike": "Áthúzott",
"numList": "Számozott lista",
"bulletList": "Felsorolás",
"checkList": "Ellenőrző lista",
"inlineCode": "Inline kód",
"quote": "Idézet",
"header": "Címsor",
"highlight": "Kiemelés"
},
"tooltip": {
"lightMode": "Világos mód",
"darkMode": "Éjjeli mód"
},
"contactsPage": {
"title": "Kontaktok",
"whatsHappening": "Heti újdonságok",
"addContact": "Új Kontakt",
"editContact": "Kontakt Szerkesztése"
},
"button": {
"OK": "OK",
"Cancel": "Mégse",
"signIn": "Bejelentkezés",
"signOut": "Kijelentkezés",
"complete": "Kész",
"save": "Mentés"
},
"label": {
"welcome": "Üdvözlünk!",
"firstName": "Keresztnév",
"middleName": "Középső név",
"lastName": "Vezetéknév",
"stepX": "{X}. lépés"
},
"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"
},
"google": {
"title": "Bejelentkezés Google-al",
"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:"
}
},
"settings": {
"title": "Beállítások",
"menu": {
"appearance": "Megjelenés",
"language": "Nyelv",
"open": "Beállítások megnyitása"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Világos mód",
"dark": "Éjjeli mód",
"system": "Adapt to System"
}
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,226 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Saya",
"welcomeText": "Selamat datang di @:appName",
"githubStarText": "Bintangi GitHub",
"subscribeNewsletterText": "Berlangganan buletin",
"letsGoButtonText": "Ayo",
"title": "Judul",
"signUp": {
"buttonText": "Daftar",
"title": "Daftar ke @:appName",
"getStartedText": "Mulai",
"emptyPasswordError": "Sandi tidak boleh kosong",
"repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong",
"unmatchedPasswordError": "Sandi ulang tidak sama dengan sandi",
"alreadyHaveAnAccount": "Sudah punya akun?",
"emailHint": "Email",
"passwordHint": "Sandi",
"repeatPasswordHint": "Sandi ulang"
},
"signIn": {
"loginTitle": "Masuk ke @:appName",
"loginButtonText": "Masuk",
"buttonText": "Masuk",
"forgotPassword": "Lupa Sandi?",
"emailHint": "Email",
"passwordHint": "Sandi",
"dontHaveAnAccount": "Belum punya akun?",
"repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong",
"unmatchedPasswordError": "Sandi ulang tidak sama dengan sandi"
},
"workspace": {
"create": "Buat workspace",
"hint": "workspace",
"notFoundError": "Workspace tidak ditemukan"
},
"shareAction": {
"buttonText": "Bagikan",
"workInProgress": "Segera",
"markdown": "Markdown",
"copyLink": "Salin tautan"
},
"disclosureAction": {
"rename": "Ganti nama",
"delete": "Hapus",
"duplicate": "Duplikat"
},
"blankPageTitle": "Halaman kosong",
"newPageText": "Halaman baru",
"trash": {
"text": "Sampah",
"restoreAll": "Pulihkan Semua",
"deleteAll": "Hapus semua",
"pageHeader": {
"fileName": "Nama file",
"lastModified": "Terakhir diubah",
"created": "Dibuat"
}
},
"deletePagePrompt": {
"text": "Halaman ini di tempat sampah",
"restore": "Pulihkan halaman",
"deletePermanent": "Hapus secara permanen"
},
"dialogCreatePageNameHint": "Nama halaman",
"questionBubble": {
"whatsNew": "Apa yang baru?",
"help": "Bantuan & Dukungan",
"debug": {
"name": "Info debug",
"success": "Info debug disalin ke papan klip!",
"fail": "Tidak dapat menyalin info debug ke papan klip"
}
},
"menuAppHeader": {
"addPageTooltip": "Menambahkan halaman di dalam dengan cepat",
"defaultNewPageName": "Tanpa Judul",
"renameDialog": "Ganti nama"
},
"toolbar": {
"undo": "Undo",
"redo": "Redo",
"bold": "Tebal",
"italic": "Miring",
"underline": "Garis bawah",
"strike": "Dicoret",
"numList": "Daftar bernomor",
"bulletList": "Daftar berpoin",
"checkList": "Daftar periksa",
"inlineCode": "Kode sebaris",
"quote": "Blok kutipan",
"header": "Tajuk",
"highlight": "Sorotan"
},
"tooltip": {
"lightMode": "Ganti mode terang",
"darkMode": "Ganti mode gelap"
},
"notifications": {
"export": {
"markdown": "Mengekspor Catatan ke Markdown",
"path": "Documents/flowy"
}
},
"contactsPage": {
"title": "Kontak",
"whatsHappening": "Apa yang terjadi minggu ini?",
"addContact": "Tambahkan Kontak",
"editContact": "Ubah Kontak"
},
"button": {
"OK": "Ya",
"Cancel": "Batal",
"signIn": "Masuk",
"signOut": "Keluar",
"complete": "Selesai",
"save": "Simpan"
},
"label": {
"welcome": "Selamat datang!",
"firstName": "Nama Depan",
"middleName": "Nama Tengah",
"lastName": "Nama Akhir",
"stepX": "Langkah {X}"
},
"oAuth": {
"err": {
"failedTitle": "Tidak dapat terhubung ke akun anda",
"failedMsg": "Mohon pastikan anda menyelesaikan proses pendaftaran pada browser anda."
},
"google": {
"title": "MASUK GOOGLE",
"instruction1": "Untuk mengimpor kontak Google Contacts anda, anda harus mengizinkan aplikasi ini menggunakan browser web anda.",
"instruction2": "Salin kode ini ke papan klip anda dengan cara mengklik ikon atau memilih teks:",
"instruction3": "Arahkan ke tautan berikut di browser web Anda, dan masukkan kode di atas:",
"instruction4": "Tekan tombol di bawah ini setelah Anda menyelesaikan pendaftaran:"
}
},
"settings": {
"title": "Pengaturan",
"menu": {
"appearance": "Tampilan",
"language": "Bahasa",
"user": "Pengguna",
"open": "Buka Pengaturan"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Mode Terang",
"dark": "Mode Gelap",
"system": "Adapt to System"
}
}
},
"grid": {
"settings": {
"filter": "Filter",
"sortBy": "Sortir dengan",
"Properties": "Properti"
},
"field": {
"hide": "Sembunyikan",
"insertLeft": "Sisipkan Kiri",
"insertRight": "Sisipkan Kanan",
"duplicate": "Duplikasi",
"delete": "Hapus",
"textFieldName": "Teks",
"checkboxFieldName": "Kotak Centang",
"dateFieldName": "Tanggal",
"numberFieldName": "Angka",
"singleSelectFieldName": "seleksi",
"multiSelectFieldName": "Multi seleksi",
"urlFieldName": "URL",
"numberFormat": "Format angka",
"dateFormat": "Format tanggal",
"includeTime": "Sertakan waktu",
"dateFormatFriendly": "Bulan Hari, Tahun",
"dateFormatISO": "Tahun-Bulan-Hari",
"dateFormatLocal": "Bulan/Hari/Tahun",
"dateFormatUS": "Tahun/Bulan/Hari",
"timeFormat": "Format waktu",
"invalidTimeFormat": "Format yang tidak valid",
"timeFormatTwelveHour": "12 jam",
"timeFormatTwentyFourHour": "24 jam",
"addSelectOption": "Tambahkan opsi",
"optionTitle": "Opsi",
"addOption": "Tambahkan opsi",
"editProperty": "Ubah properti"
},
"row": {
"duplicate": "Duplikasi",
"delete": "Hapus",
"textPlaceholder": "Kosong",
"copyProperty": "Salin properti ke papan klip"
},
"selectOption": {
"create": "Buat",
"purpleColor": "Ungu",
"pinkColor": "Merah Jambu",
"lightPinkColor": "Merah Jambu Muda",
"orangeColor": "Oranye",
"yellowColor": "Kuning",
"limeColor": "Limau",
"greenColor": "Hijau",
"aquaColor": "Air",
"blueColor": "Biru",
"deleteTag": "Hapus tag",
"colorPanelTitle": "Warna",
"panelTitle": "Pilih opsi atau buat baru",
"searchOption": "Cari opsi"
},
"menuName": "Grid"
},
"document": {
"menuName": "Dokter",
"date": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,159 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Me",
"welcomeText": "Benvenuto in @:appName",
"githubStarText": "Vota su GitHub",
"subscribeNewsletterText": "Sottoscrivi la Newsletter",
"letsGoButtonText": "Andiamo",
"title": "Titolo",
"signUp": {
"buttonText": "Registrati",
"title": "Registrati per @:appName",
"getStartedText": "Iniziamo",
"emptyPasswordError": "La password non può essere vuota",
"repeatPasswordEmptyError": "La password ripetuta non può essere vuota",
"unmatchedPasswordError": "La password ripetuta non è uguale alla password",
"alreadyHaveAnAccount": "Hai già un account?",
"emailHint": "Email",
"passwordHint": "Password",
"repeatPasswordHint": "Ripeti password"
},
"signIn": {
"loginTitle": "Accedi a @:appName",
"loginButtonText": "Login",
"buttonText": "Accedi",
"forgotPassword": "Password Dimentica?",
"emailHint": "Email",
"passwordHint": "Password",
"dontHaveAnAccount": "Non hai un account?",
"repeatPasswordEmptyError": "La password ripetuta non può essere vuota",
"unmatchedPasswordError": "La password ripetuta non è uguale alla password"
},
"workspace": {
"create": "Crea spazio di lavoro",
"hint": "spazio di lavoro",
"notFoundError": "Spazio di lavoro non trovato"
},
"shareAction": {
"buttonText": "Condividi",
"workInProgress": "Prossimamente",
"markdown": "Markdown",
"copyLink": "Copia Link"
},
"disclosureAction": {
"rename": "Rinomina",
"delete": "Cancella",
"duplicate": "Duplica"
},
"blankPageTitle": "Pagina vuota",
"newPageText": "Nuova pagina",
"trash": {
"text": "Cestino",
"restoreAll": "Ripristina Tutto",
"deleteAll": "Elimina Tutto",
"pageHeader": {
"fileName": "Nome file",
"lastModified": "Ultima Modifica",
"created": "Creato"
}
},
"deletePagePrompt": {
"text": "Questa pagina è nel Cestino",
"restore": "Ripristina pagina",
"deletePermanent": "Elimina definitivamente"
},
"dialogCreatePageNameHint": "Nome pagina",
"questionBubble": {
"whatsNew": "Cosa c'è di nuovo?",
"help": "Aiuto & Supporto",
"debug": {
"name": "Informazioni di debug",
"success": "Informazioni di debug copiate negli appunti!",
"fail": "Impossibile copiare le informazioni di debug negli appunti"
}
},
"menuAppHeader": {
"addPageTooltip": "Aggiungi velocemente una pagina all'interno",
"defaultNewPageName": "Senza titolo",
"renameDialog": "Rinomina"
},
"toolbar": {
"undo": "Undo",
"redo": "Redo",
"bold": "Grassetto",
"italic": "Italico",
"underline": "Sottolineato",
"strike": "Barrato",
"numList": "Lista numerata",
"bulletList": "Lista a punti",
"checkList": "Lista Controllo",
"inlineCode": "Codice in linea",
"quote": "Cita Blocco",
"header": "intestazione",
"highlight": "Evidenziare"
},
"tooltip": {
"lightMode": "Passa alla modalità Chiara",
"darkMode": "Passa alla modalità Scura"
},
"contactsPage": {
"title": "Contatti",
"whatsHappening": "Cosa accadrà la prossima settimana?",
"addContact": "Aggiungi Contatti",
"editContact": "Modifica Contatti"
},
"button": {
"OK": "OK",
"Cancel": "Annulla",
"signIn": "Accedi",
"signOut": "Esci",
"complete": "Completa",
"save": "Salva"
},
"label": {
"welcome": "Benvenuto!",
"firstName": "Name",
"middleName": "Secondo Name",
"lastName": "Cognome",
"stepX": "Passo {X}"
},
"oAuth": {
"err": {
"failedTitle": "Impossibile collegarsi al tuo account.",
"failedMsg": "Si prega di verificare di aver completato il processo di iscrizione nel tuo browser."
},
"google": {
"title": "GOOGLE SIGN-IN",
"instruction1": "Al fine di importare i tuoi Contatti Google è necessario autorizzare questa applicazione ad utilizzare il tuo beowser web.",
"instruction2": "Copia questo codice negli appunti premendo l'icona o selezionando il testo:",
"instruction3": "Naviga sul seguente link con il tuo browser web e inserisci il codice seguente:",
"instruction4": "Premi il bottone qui sotto quando hai completato l'iscrizione:"
}
},
"settings": {
"title": "Impostazioni",
"menu": {
"appearance": "Aspetto",
"language": "Lingua",
"open": "aprire le impostazioni"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Modalità Chiara",
"dark": "Modalità Scura",
"system": "Adapt to System"
}
}
},
"grid": {
"menuName": "Griglia"
},
"document": {
"menuName": "Documento"
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,207 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "ユーザー",
"welcomeText": "Welcome to @:appName",
"githubStarText": "Star on GitHub",
"subscribeNewsletterText": "新着情報を受け取る",
"letsGoButtonText": "Let's Go",
"title": "タイトル",
"signUp": {
"buttonText": "新規登録",
"title": "@:appNameに新規登録",
"getStartedText": "はじめる",
"emptyPasswordError": "パスワードを空にはできません",
"repeatPasswordEmptyError": "パスワード(確認用)を空にはできません",
"unmatchedPasswordError": "パスワード(確認用)が一致しません",
"alreadyHaveAnAccount": "すでにアカウントを登録済ですか?",
"emailHint": "メールアドレス",
"passwordHint": "パスワード",
"repeatPasswordHint": "パスワード(確認用)"
},
"signIn": {
"loginTitle": "@:appName にログイン",
"loginButtonText": "ログイン",
"buttonText": "サインイン",
"forgotPassword": "パスワードをお忘れですか?",
"emailHint": "メールアドレス",
"passwordHint": "パスワード",
"dontHaveAnAccount": "まだアカウントをお持ちではないですか?",
"repeatPasswordEmptyError": "パスワード(確認用)を空にはできません",
"unmatchedPasswordError": "パスワード(確認用)が一致しません"
},
"workspace": {
"create": "ワークスペースを作成する",
"hint": "ワークスペース",
"notFoundError": "ワークスペースがみつかりません"
},
"shareAction": {
"buttonText": "共有する",
"workInProgress": "Coming soon",
"markdown": "Markdown",
"copyLink": "Copy Link"
},
"disclosureAction": {
"rename": "名前を変更",
"delete": "削除",
"duplicate": "コピーを作成"
},
"blankPageTitle": "空のページ",
"newPageText": "新しいページ",
"trash": {
"text": "ごみ箱",
"restoreAll": "全て復元",
"deleteAll": "全て削除",
"pageHeader": {
"fileName": "ファイル名",
"lastModified": "最終更新日時",
"created": "作成日時"
}
},
"deletePagePrompt": {
"text": "このページはごみ箱にあります",
"restore": "ページを元に戻す",
"deletePermanent": "削除する"
},
"dialogCreatePageNameHint": "ページ名",
"questionBubble": {
"whatsNew": "What's new?",
"help": "ヘルプとサポート",
"debug": {
"name": "デバッグ情報",
"success": "デバッグ情報をクリップボードにコピーしました!",
"fail": "デバッグ情報をクリップボードにコピーできませんでした"
}
},
"menuAppHeader": {
"addPageTooltip": "内部ページを追加",
"defaultNewPageName": "Untitled",
"renameDialog": "名前を変更"
},
"toolbar": {
"undo": "元に戻す",
"redo": "やり直し",
"bold": "太字",
"italic": "斜体",
"underline": "下線",
"strike": "取り消し線",
"numList": "番号付きリスト",
"bulletList": "箇条書き",
"checkList": "チェックボックス",
"inlineCode": "インラインコード",
"quote": "引用文",
"header": "見出し",
"highlight": "文字の背景色"
},
"tooltip": {
"lightMode": "ライトモードに切り替える",
"darkMode": "ダークモードに切り替える"
},
"contactsPage": {
"title": "連絡先",
"whatsHappening": "今週はどんなことがありましたか?",
"addContact": "連絡先を追加する",
"editContact": "連絡先を編集する"
},
"button": {
"OK": "OK",
"Cancel": "キャンセル",
"signIn": "サインイン",
"signOut": "サインアウト",
"complete": "完了",
"save": "保存"
},
"label": {
"welcome": "ようこそ!",
"firstName": "名",
"middleName": "ミドルネーム",
"lastName": "姓",
"stepX": "Step {X}"
},
"oAuth": {
"err": {
"failedTitle": "アカウントに接続できません",
"failedMsg": "サインインが完了したことをブラウザーで確認してください"
},
"google": {
"title": "GOOGLEでサインイン",
"instruction1": "GOOGLEでのサインインを有効にするためには、Webブラウザーを使ってこのアプリケーションを認証する必要があります。",
"instruction2": "アイコンをクリックするか、以下のテキストを選択して、このコードをクリップボードにコピーします。",
"instruction3": "以下のリンク先をブラウザーで開いて、次のコードを入力します。",
"instruction4": "登録が完了したら以下のボタンを押してください。"
}
},
"settings": {
"title": "設定",
"menu": {
"appearance": "外観",
"language": "言語",
"open": "設定"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "ライトモード",
"dark": "ダークモード",
"system": "Adapt to System"
}
}
},
"grid": {
"settings": {
"filter": "絞り込み",
"sortBy": "並び替え",
"Properties": "プロパティ"
},
"field": {
"hide": "隠す",
"insertLeft": "左に挿入",
"insertRight": "右に挿入",
"duplicate": "コピーを作成",
"delete": "削除",
"textFieldName": "テキスト",
"checkboxFieldName": "チェックボックス",
"dateFieldName": "日付",
"numberFieldName": "数値",
"singleSelectFieldName": "単一選択",
"multiSelectFieldName": "複数選択",
"numberFormat": "数値書式",
"dateFormat": "日付書式",
"includeTime": "時刻を含める",
"dateFormatFriendly": "月 日, 年",
"dateFormatISO": "年-月-日",
"dateFormatLocal": "月/日/年",
"dateFormatUS": "年/月/日",
"timeFormat": "時刻書式",
"timeFormatTwelveHour": "12 時間表記",
"timeFormatTwentyFourHour": "24 時間表記",
"addSelectOption": "選択候補追加",
"optionTitle": "選択候補",
"addOption": "選択候補追加",
"editProperty": "プロパティの編集"
},
"row": {
"duplicate": "コピーを作成",
"delete": "削除",
"textPlaceholder": "空白"
},
"selectOption": {
"purpleColor": "紫",
"pinkColor": "ピンク",
"lightPinkColor": "ライトピンク",
"orangeColor": "オレンジ",
"yellowColor": "黄色",
"limeColor": "ライム",
"greenColor": "緑",
"aquaColor": "水色",
"blueColor": "青",
"deleteTag": "選択候補を削除",
"colorPanelTitle": "色",
"panelTitle": "選択候補を検索 または 作成する",
"searchOption": "選択候補を検索"
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,235 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Me",
"welcomeText": "@:appName 에 오신것을 환영합니다",
"githubStarText": "Star on GitHub",
"subscribeNewsletterText": "뉴스레터 구독",
"letsGoButtonText": "Let's Go",
"title": "제목",
"signUp": {
"buttonText": "회원가입",
"title": "@:appName 에 회원가입",
"getStartedText": "시작하기",
"emptyPasswordError": "비밀번호는 공백일 수 없습니다",
"repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다",
"unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다",
"alreadyHaveAnAccount": "이미 계정이 있으신가요?",
"emailHint": "이메일",
"passwordHint": "비밀번호",
"repeatPasswordHint": "비밀번호 재입력"
},
"signIn": {
"loginTitle": "@:appName 에 로그인",
"loginButtonText": "로그인",
"buttonText": "로그인",
"forgotPassword": "비밀번호를 잊으셨나요?",
"emailHint": "이메일",
"passwordHint": "비밀번호",
"dontHaveAnAccount": "계정이 없으신가요?",
"repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다",
"unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다"
},
"workspace": {
"create": "워크스페이스 생성",
"hint": "워크스페이스",
"notFoundError": "워크스페이스를 찾을 수 없습니다"
},
"shareAction": {
"buttonText": "공유",
"workInProgress": "Coming soon",
"markdown": "마크다운",
"copyLink": "링크 복사"
},
"disclosureAction": {
"rename": "이름변경",
"delete": "삭제",
"duplicate": "복제"
},
"blankPageTitle": "빈 페이지",
"newPageText": "새로운 페이지",
"trash": {
"text": "휴지통",
"restoreAll": "모두 복구",
"deleteAll": "모두 삭제",
"pageHeader": {
"fileName": "파일 이름",
"lastModified": "수정날짜",
"created": "생성날짜"
}
},
"deletePagePrompt": {
"text": "현재 페이지는 휴지통에 있습니다",
"restore": "페이지 복구",
"deletePermanent": "영구 삭제"
},
"dialogCreatePageNameHint": "페이지 이름",
"questionBubble": {
"whatsNew": "새로운 소식",
"help": "도움 및 지원",
"debug": {
"name": "디버그 정보",
"success": "디버그 정보를 클립보드로 복사했습니다.",
"fail": "디버그 정보를 클립보드로 복사할 수 없습니다."
}
},
"menuAppHeader": {
"addPageTooltip": "하위에 페이지 추가",
"defaultNewPageName": "제목없음",
"renameDialog": "이름변경"
},
"toolbar": {
"undo": "실행취소",
"redo": "재실행",
"bold": "굵게",
"italic": "기울임꼴",
"underline": "밑줄",
"strike": "취소선",
"numList": "번호 매기기 목록",
"bulletList": "글머리 기호 목록",
"checkList": "작업 목록",
"inlineCode": "인라인 코드",
"quote": "인용구 블록",
"header": "헤더",
"highlight": "하이라이트"
},
"tooltip": {
"lightMode": "라이트 모드로 변경",
"darkMode": "다크 모드로 변경",
"openAsPage": "페이지로 열기",
"addNewRow": "열 추가",
"openMenu": "메뉴를 여시려면 클릭하세요"
},
"sideBar": {
"closeSidebar": "사이드바 닫기",
"openSidebar": "사이드바 열기"
},
"notifications": {
"export": {
"markdown": "마크다운으로 노트를 내보냄",
"path": "Documents/flowy"
}
},
"contactsPage": {
"title": "연락처",
"whatsHappening": "이번주에는 무슨 일이 있나요?",
"addContact": "연락처 추가",
"editContact": "연락처 편집"
},
"button": {
"OK": "확인",
"Cancel": "취소",
"signIn": "로그인",
"signOut": "로그아웃",
"complete": "완료",
"save": "저장"
},
"label": {
"welcome": "환영합니다!",
"firstName": "이름",
"middleName": "중간 이름",
"lastName": "성",
"stepX": "{X} 단계"
},
"oAuth": {
"err": {
"failedTitle": "계정에 연결을 할 수 없습니다.",
"failedMsg": "브라우저에서 회원가입이 완료되었는지 확인해주세요."
},
"google": {
"title": "GOOGLE SIGN-IN",
"instruction1": "구글 연락처를 가져오기 위해서 웹브라우저로 앱을 승인 해야 합니다.",
"instruction2": "아이콘을 클릭 또는 텍스트를 선택해서 이 코드를 클립보드로 복사하세요:",
"instruction3": "웹브라우저로 다음 링크로 가셔서 위 코드를 입력해주세요:",
"instruction4": "가입 완료 후 아래 버튼을 눌러주세요:"
}
},
"settings": {
"title": "설정",
"menu": {
"appearance": "화면",
"language": "언어",
"user": "사용자",
"open": "설정 열기"
},
"appearance": {
"lightLabel": "라이트 모드",
"darkLabel": "다크 모드"
}
},
"grid": {
"settings": {
"filter": "필터",
"sortBy": "정렬 기준",
"Properties": "속성",
"group": "그룹"
},
"field": {
"hide": "숨기기",
"insertLeft": "왼쪽 삽입",
"insertRight": "오른쪽 삽입",
"duplicate": "복제",
"delete": "삭제",
"textFieldName": "텍스트",
"checkboxFieldName": "체크박스",
"dateFieldName": "날짜",
"numberFieldName": "숫자",
"singleSelectFieldName": "선택",
"multiSelectFieldName": "다중선택",
"urlFieldName": "링크",
"numberFormat": "숫자 형식",
"dateFormat": "날짜 형식",
"includeTime": "시간 표시",
"dateFormatFriendly": "월 일, 년",
"dateFormatISO": "년-월-일",
"dateFormatLocal": "월/일/년",
"dateFormatUS": "년/월/일",
"timeFormat": "시간 형식",
"invalidTimeFormat": "잘못된 형식",
"timeFormatTwelveHour": "12 시간",
"timeFormatTwentyFourHour": "24 시간",
"addSelectOption": "옵션 추가",
"optionTitle": "옵션",
"addOption": "옵션 추가",
"editProperty": "속성 편집",
"newProperty": "열 추가",
"deleteFieldPromptMessage": "해당 속성을 삭제 하시겠습니까?"
},
"row": {
"duplicate": "복제",
"delete": "삭제",
"textPlaceholder": "비어있음",
"copyProperty": "속성이 클립보드로 복사됨",
"count": "개수",
"newRow": "행 추가"
},
"selectOption": {
"create": "생성",
"purpleColor": "보라색",
"pinkColor": "핑크색",
"lightPinkColor": "연한 핑크색",
"orangeColor": "오렌지색",
"yellowColor": "노랑색",
"limeColor": "라임색",
"greenColor": "초록색",
"aquaColor": "아쿠아색",
"blueColor": "파랑색",
"deleteTag": "태그 삭제",
"colorPanelTitle": "색상",
"panelTitle": "옵션 선택 또는 생성",
"searchOption": "옵션 검색"
},
"menuName": "그리드"
},
"document": {
"menuName": "도큐먼트",
"date": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
}
},
"board": {
"column": {
"create_new_card": "추가"
}
}
}
@@ -1,153 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Ja",
"welcomeText": "Witaj w @:appName",
"githubStarText": "Gwiazdka na GitHub-ie",
"subscribeNewsletterText": "Zapisz się do naszego Newslettera",
"letsGoButtonText": "Start!",
"title": "Tytuł",
"signUp": {
"buttonText": "Zarejestruj",
"title": "Zarejestruj się w @:appName",
"getStartedText": "Zaczynamy",
"emptyPasswordError": "Hasło nie moze być puste",
"repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste",
"unmatchedPasswordError": "Hasła nie są takie same",
"alreadyHaveAnAccount": "Masz juz konto?",
"emailHint": "Email",
"passwordHint": "Hasło",
"repeatPasswordHint": "Powtórz hasło"
},
"signIn": {
"loginTitle": "Zaloguj do @:appName",
"loginButtonText": "Logowanie",
"buttonText": "Zaloguj",
"forgotPassword": "Zapomniałem hasła?",
"emailHint": "Email",
"passwordHint": "Password",
"dontHaveAnAccount": "Nie masz konta?",
"repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste",
"unmatchedPasswordError": "Hasła nie są takie same"
},
"workspace": {
"create": "Utwórz przestrzeń",
"hint": "przestrzeń robocza",
"notFoundError": "Przestrzeni nie znaleziono"
},
"shareAction": {
"buttonText": "Udostępnij",
"workInProgress": "Wkrótce",
"markdown": "Markdown",
"copyLink": "Skopiuj link"
},
"disclosureAction": {
"rename": "Zmień nazwę",
"delete": "Usuń",
"duplicate": "Duplikuj"
},
"blankPageTitle": "Pusta strona",
"newPageText": "Nowa strona",
"trash": {
"text": "Kosz",
"restoreAll": "Przywróć Wszystko",
"deleteAll": "Usuń Wszystko",
"pageHeader": {
"fileName": "Nazwa Pliku",
"lastModified": "Ostatnio Zmodyfikowano",
"created": "Utworzono"
}
},
"deletePagePrompt": {
"text": "Ta strona jest w Koszu",
"restore": "Przywróć strone",
"deletePermanent": "Usuń bezpowrotnie"
},
"dialogCreatePageNameHint": "Nazwa Strony",
"questionBubble": {
"whatsNew": "What's new?",
"help": "Pomoc & Wsparcie",
"debug": {
"name": "Informacje Debugowania",
"success": "Skopiowano informacje debugowania do schowka!",
"fail": "Nie mozna skopiować informacji debugowania do schowka"
}
},
"menuAppHeader": {
"addPageTooltip": "Szybko dodaj stronę do środka",
"defaultNewPageName": "Brak tytułu",
"renameDialog": "Zmień nazwę"
},
"toolbar": {
"undo": "Cofnij",
"redo": "Powtórz",
"bold": "Pogrubiony",
"italic": "Kursywa",
"underline": "Podkreśl",
"strike": "Przekreśl",
"numList": "Lista Numerowana",
"bulletList": "Lista Punktowana",
"checkList": "Lista Kontrolna",
"inlineCode": "Kod Wbudowany",
"quote": "Blok cytat",
"header": "Nagłówek",
"highlight": "Podświetl"
},
"tooltip": {
"lightMode": "Przełącz w Tryb Jasny",
"darkMode": "Przełącz w Tryb Ciemny"
},
"contactsPage": {
"title": "Kontakty",
"whatsHappening": "Co się dzieje w tym tygodniu?",
"addContact": "Dodaj Kontakt",
"editContact": "Edytuj Kontakt"
},
"button": {
"OK": "OK",
"Cancel": "Anuluj",
"signIn": "Zaloguj",
"signOut": "Wyloguj",
"complete": "Zakończono",
"save": "Zapisz"
},
"label": {
"welcome": "Witaj!",
"firstName": "Imię Pierwsze",
"middleName": "Imię Drugie",
"lastName": "Nazwisko",
"stepX": "Krok {X}"
},
"oAuth": {
"err": {
"failedTitle": "Nie można połączyć się z Twoim kontem.",
"failedMsg": "Upewnij się, że zakończyłeś proces logowania w przeglądarce."
},
"google": {
"title": "LOGOWANIE GOOGLE",
"instruction1": "Aby zaimportować Kontakty Google, musisz autoryzować tę aplikację za pomocą przeglądarki internetowej.",
"instruction2": "Skopiuj ten kod do schowka, klikając ikonę lub zaznaczając tekst:",
"instruction3": "Przejdź do następującego linku w przeglądarce internetowej i wprowadź powyższy kod:",
"instruction4": "Naciśnij poniższy przycisk po zakończeniu rejestracji:"
}
},
"settings": {
"title": "Ustawienia",
"menu": {
"appearance": "Wygląd",
"language": "Język",
"open": "Otwórz Ustawienia"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Tryb Jasny",
"dark": "Tryb Ciemny",
"system": "Adapt to System"
}
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,149 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Me",
"welcomeText": "Bem vindo ao @:appName",
"githubStarText": "Star on GitHub",
"subscribeNewsletterText": "Inscreve-te ao Newsletter",
"letsGoButtonText": "Bora",
"title": "Título",
"signUp": {
"buttonText": "Inscreve-te",
"title": "Inscreve-te ao @:appName",
"getStartedText": "Começar",
"emptyPasswordError": "A palavra-passe não pode estar em branco.",
"repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
"unmatchedPasswordError": "As palavras-passes não coincidem.",
"alreadyHaveAnAccount": "Já possuis uma conta?",
"emailHint": "Email",
"passwordHint": "Password",
"repeatPasswordHint": "Confirma a tua password"
},
"signIn": {
"loginTitle": "Entre no @:appName",
"loginButtonText": "Login",
"buttonText": "Entre",
"forgotPassword": "Esqueceste-te da tua palavra-passe?",
"emailHint": "Email",
"passwordHint": "Palavra-passe",
"dontHaveAnAccount": "Não possuis uma conta?",
"repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
"unmatchedPasswordError": "As palavras-passes não conferem."
},
"workspace": {
"create": "Cria um ambiente de trabalho",
"hint": "ambiente de trabalho",
"notFoundError": "Ambiente de trabalho não encontrada"
},
"shareAction": {
"buttonText": "Partilhar",
"workInProgress": "Em breve",
"markdown": "Markdown",
"copyLink": "Copiar o link"
},
"disclosureAction": {
"rename": "Renomear",
"delete": "Apagar",
"duplicate": "Duplicar"
},
"blankPageTitle": "Página em branco",
"newPageText": "Nova página",
"trash": {
"text": "Lixo",
"restoreAll": "Restaurar todos",
"deleteAll": "Apagar todos",
"pageHeader": {
"fileName": "Nome do ficheiro",
"lastModified": "Última modificação",
"created": "Criado"
}
},
"deletePagePrompt": {
"text": "Esta página está no lixo",
"restore": "Restaurar a página",
"deletePermanent": "Apagar permanentemente"
},
"dialogCreatePageNameHint": "Nome da página",
"questionBubble": {
"whatsNew": "O que há de novo?",
"help": "Ajuda & Suporte",
"debug": {
"name": "Informação de depuração",
"success": "Copiar informação de depuração para o clipboard!",
"fail": "Falha em copiar a informação de depuração para o clipboard"
}
},
"menuAppHeader": {
"addPageTooltip": "Adiciona uma nova página.",
"defaultNewPageName": "Sem título",
"renameDialog": "Renomear"
},
"toolbar": {
"undo": "Desfazer",
"redo": "Refazer",
"bold": "Negrito",
"italic": "Itálico",
"underline": "Sublinhado",
"strike": "Riscado",
"numList": "Lista numerada",
"bulletList": "Lista com marcadores",
"checkList": "Lista de verificação",
"inlineCode": "Embutir código",
"quote": "Citação em bloco",
"header": "Cabeçalho",
"highlight": "Realçar"
},
"tooltip": {
"lightMode": "Mudar para o modo Claro.",
"darkMode": "Mudar para o modo Escuro."
},
"contactsPage": {
"title": "Conctatos",
"whatsHappening": "O que está a acontecer nesta semana?",
"addContact": "Adicionar um conctato",
"editContact": "Editar um conctato"
},
"button": {
"OK": "OK",
"Cancel": "Cancelar",
"signIn": "Entrar",
"signOut": "Sair",
"complete": "Completar",
"save": "Guardar"
},
"label": {
"welcome": "Bem vindo!",
"firstName": "Nome",
"middleName": "Nome do Meio",
"lastName": "Apelido",
"stepX": "Passo {X}"
},
"oAuth": {
"err": {
"failedTitle": "Erro ao conectar à sua conta.",
"failedMsg": "Verifica se concluiste o processo de login no teu navegador."
},
"google": {
"title": "GOOGLE SIGN-IN",
"instruction1": "Para importar os teus Conctatos do Google, tens de autorizar esta aplicação usando o teu navegador web.",
"instruction2": "Copia este código para a tua área de transferências clicando no ícone ou selecionando o texto:",
"instruction3": "Navega até o link a seguir no seu navegador e digite o código acima:",
"instruction4": "Clica no botão abaixo ao concluir a inscrição:"
}
},
"settings": {
"title": "Definições",
"menu": {
"appearance": "Aparência",
"language": "Idioma",
"open": "Abrir as Definições"
},
"appearance": {
"lightLabel": "Modo Claro",
"darkLabel": "Modo Escuro"
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,239 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Jag",
"welcomeText": "Välkommen till @:appName",
"githubStarText": "Stärnmärk på GitHub",
"subscribeNewsletterText": "Prenumerera på nyhetsbrev",
"letsGoButtonText": "Kör igång",
"title": "Namn",
"signUp": {
"buttonText": "Registrera dig",
"title": "Registrera dig på @:appName",
"getStartedText": "Sätt igång",
"emptyPasswordError": "Lösenordet kan inte vara tomt",
"repeatPasswordEmptyError": "Upprepat lösenord kan inte vara tomt",
"unmatchedPasswordError": "Upprepat lösenord är inte samma som det första",
"alreadyHaveAnAccount": "Har du redan ett konto?",
"emailHint": "E-post",
"passwordHint": "Lösenord",
"repeatPasswordHint": "Uprepa lösenordet"
},
"signIn": {
"loginTitle": "Logga in till @:appName",
"loginButtonText": "Logga in",
"buttonText": "Registrering",
"forgotPassword": "Glömt lösenordet?",
"emailHint": "E-post",
"passwordHint": "Lösenord",
"dontHaveAnAccount": "Har du inget konto?",
"repeatPasswordEmptyError": "Upprepat lösenord kan inte vara tomt",
"unmatchedPasswordError": "Upprepat lösenord är inte samma som det första"
},
"workspace": {
"create": "Skapa arbetsyta",
"hint": "Arbetsyta",
"notFoundError": "Hittade ingen arbetsyta"
},
"shareAction": {
"buttonText": "Dela",
"workInProgress": "Kommer snart",
"markdown": "Markdown",
"copyLink": "Kopiera länk"
},
"disclosureAction": {
"rename": "Byt namn",
"delete": "Ta bort",
"duplicate": "Klona"
},
"blankPageTitle": "Tom sida",
"newPageText": "Ny sida",
"trash": {
"text": "Skräp",
"restoreAll": "Återställ alla",
"deleteAll": "Ta bort alla",
"pageHeader": {
"fileName": "Filnamn",
"lastModified": "Ändrad",
"created": "Skapad"
}
},
"deletePagePrompt": {
"text": "Denna sida är i skräpmappen",
"restore": "Återställ sida",
"deletePermanent": "Radera permanent"
},
"dialogCreatePageNameHint": "Sidnamn",
"questionBubble": {
"whatsNew": "Vad nytt?",
"help": "Hjälp & Support",
"debug": {
"name": "Felsökningsinfo",
"success": "Kopierade felsökningsinfo till urklipp!",
"fail": "Kunde inte kopiera felsökningsinfo till urklipp"
}
},
"menuAppHeader": {
"addPageTooltip": "Lägg snabbt till en sida inuti",
"defaultNewPageName": "Namnlös",
"renameDialog": "Byt namn"
},
"toolbar": {
"undo": "Ångra",
"redo": "Upprepa",
"bold": "Fet",
"italic": "Kursiv",
"underline": "Understruken",
"strike": "Genomstruken",
"numList": "Numrerad lista",
"bulletList": "Punktlista",
"checkList": "Checklista",
"inlineCode": "Infogad kod",
"quote": "Citatblock",
"header": "Rubrik",
"highlight": "Färgmarkera"
},
"tooltip": {
"lightMode": "Växla till ljust läge",
"darkMode": "Växla till mörkt läge",
"openAsPage": "Öppna som sida",
"addNewRow": "Lägg till ny rad",
"openMenu": "Klicka för att öppna meny"
},
"sideBar": {
"closeSidebar": "Stäng sidofältet",
"openSidebar": "Öppna sidofältet"
},
"notifications": {
"export": {
"markdown": "Exporterade anteckning till Markdown",
"path": "Dokument/flowy"
}
},
"contactsPage": {
"title": "Kontakter",
"whatsHappening": "Vad händer denna vecka?",
"addContact": "Lägg till kontakt",
"editContact": "Redigera kontakt"
},
"button": {
"OK": "OK",
"Cancel": "Avbryt",
"signIn": "Logga in",
"signOut": "Logga ut",
"complete": "Slutfört",
"save": "Spara"
},
"label": {
"welcome": "Välkommen!",
"firstName": "Förnamn",
"middleName": "Mellannamn",
"lastName": "Efternamn",
"stepX": "Steg {X}"
},
"oAuth": {
"err": {
"failedTitle": "Kan inte ansluta till ditt konto.",
"failedMsg": "Tillse att du har slutfört registreringsprocessen i din webbläsare."
},
"google": {
"title": "GOOGLE-inloggning",
"instruction1": "För att kunna importera dina Google-kontakter, måste du auktorisera detta program med hjälp av din webbläsare.",
"instruction2": "Kopiera den här koden till urklipp genom att klicka på ikonen eller genom att markera texten:",
"instruction3": "Gå till följande länk i din webbläsare, och ange ovanstående kod:",
"instruction4": "Tryck på nedanstående knapp när du slutfört registreringen:"
}
},
"settings": {
"title": "Inställningar",
"menu": {
"appearance": "Utseende",
"language": "Språk",
"user": "Användare",
"open": "Öppna inställningarna"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Ljust läge",
"dark": "Mörkt läge",
"system": "Adapt to System"
}
}
},
"grid": {
"settings": {
"filter": "Filter",
"sortBy": "Sortera efter",
"Properties": "Egenskaper",
"group": "Grupp"
},
"field": {
"hide": "Dölj",
"insertLeft": "Infoga till vänster",
"insertRight": "Infoga till höger",
"duplicate": "Klona",
"delete": "Ta bort",
"textFieldName": "Text",
"checkboxFieldName": "Checkruta",
"dateFieldName": "Datum",
"numberFieldName": "Siffror",
"singleSelectFieldName": "Välj",
"multiSelectFieldName": "Välj flera",
"urlFieldName": "URL",
"numberFormat": "Sifferformat",
"dateFormat": "Datumformat",
"includeTime": "Inkludera tid",
"dateFormatFriendly": "Månad Dag, År",
"dateFormatISO": "År-Månad-Dag",
"dateFormatLocal": "Månad/Dag/År",
"dateFormatUS": "År/Månad/Dag",
"timeFormat": "Tidsformat",
"invalidTimeFormat": "Ogiltigt format",
"timeFormatTwelveHour": "12-timmars",
"timeFormatTwentyFourHour": "24-timmars",
"addSelectOption": "Lägg till ett alternativ",
"optionTitle": "Alternativ",
"addOption": "Lägg till alternativ",
"editProperty": "Redigera egenskap",
"newProperty": "Ny kolumn",
"deleteFieldPromptMessage": "Är du säker? Denna egenskap kommer att raderas."
},
"row": {
"duplicate": "Klona",
"delete": "Ta bort",
"textPlaceholder": "Tom",
"copyProperty": "Kopierade egenskap till urklipp",
"count": "Antal",
"newRow": "Ny rad"
},
"selectOption": {
"create": "Skapa",
"purpleColor": "Purpur",
"pinkColor": "Rosa",
"lightPinkColor": "Ljusrosa",
"orangeColor": "Orange",
"yellowColor": "Gul",
"limeColor": "Lime",
"greenColor": "Grön",
"aquaColor": "Vatten",
"blueColor": "Blå",
"deleteTag": "Ta bort tagg",
"colorPanelTitle": "Färger",
"panelTitle": "Välj ett alternativ eller skapa ett",
"searchOption": "Sök efter ett alternativ"
},
"menuName": "Tabell"
},
"document": {
"menuName": "Dokument",
"date": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
}
},
"board": {
"column": {
"create_new_card": "Nytt"
}
}
}
@@ -1,153 +0,0 @@
{
"appName": "AppFlowy",
"defaultUsername": "Ben",
"welcomeText": "@:appName'e Hoş Geldiniz!",
"githubStarText": "GitHub Yıldızı!",
"subscribeNewsletterText": "Bültene Abone Ol",
"letsGoButtonText": "Hadi başlayalım.",
"title": "Başlık",
"signUp": {
"buttonText": "Kayıt Ol",
"title": "@:appName'e kaydolun.",
"getStartedText": "başlayalım",
"emptyPasswordError": "Parola boş olamaz",
"repeatPasswordEmptyError": "Parola (tekrar) boş olamaz",
"unmatchedPasswordError": "Parolalar eşleşmiyor",
"alreadyHaveAnAccount": "Zaten hesabınız var mı?",
"emailHint": "E-Posta",
"passwordHint": "Parola",
"repeatPasswordHint": "Tekrar parola"
},
"signIn": {
"loginTitle": "@:appName oturum aç",
"loginButtonText": "Giriş",
"buttonText": "Oturum Aç",
"forgotPassword": "Parolanızı mı Unuttunuz?",
"emailHint": "E-Posta",
"passwordHint": "Parola",
"dontHaveAnAccount": "Hesabınız yok mu?",
"repeatPasswordEmptyError": "Parola (tekrar) boş olamaz",
"unmatchedPasswordError": "Parolalar eşleşmiyor"
},
"workspace": {
"create": "Çalışma alanı oluştur",
"hint": "Çalışma alanı",
"notFoundError": "Çalışma alanı bulunamadı"
},
"shareAction": {
"buttonText": "Paylaş",
"workInProgress": "Yakında",
"markdown": "Markdown",
"copyLink": "Link'i Kopyala"
},
"disclosureAction": {
"rename": "Yeniden adlandır",
"delete": "Sil",
"duplicate": "Çoğalt"
},
"blankPageTitle": "Boş sayfa",
"newPageText": "Yeni sayfa",
"trash": {
"text": "Çöp",
"restoreAll": "Geri Yükle",
"deleteAll": "Sil",
"pageHeader": {
"fileName": "Dosya adı",
"lastModified": "Son Değiştirme",
"created": "Oluşturuldu"
}
},
"deletePagePrompt": {
"text": "Bu sayfa Çöp Kutusu'nda",
"restore": "Sayfayı geri yükle",
"deletePermanent": "Kalıcı olarak sil"
},
"dialogCreatePageNameHint": "Sayfa adı",
"questionBubble": {
"whatsNew": "Yeni ne var?",
"help": "Yardım & Destek",
"debug": {
"name": "Hata Ayıklama",
"success": "Hata ayıklama bilgileri panoya kopyalandı!",
"fail": "Hata ayıklama bilgileri panoya kopyalanamıyor"
}
},
"menuAppHeader": {
"addPageTooltip": "Yeni bir sayfa ekleyin",
"defaultNewPageName": "Başlıksız",
"renameDialog": "Yeniden adlandır"
},
"toolbar": {
"undo": "Geri",
"redo": "İleri",
"bold": "Kalın",
"italic": "İtalik",
"underline": "Altı Çizili",
"strike": "Üstü Çizili",
"numList": "Numaralı Liste",
"bulletList": "Madde İşaretli Liste",
"checkList": "Yapılacaklar Listesi",
"inlineCode": "Kod",
"quote": "Alıntı",
"header": "Başlık",
"highlight": "Vurgu"
},
"tooltip": {
"lightMode": "Aydınlık Mod'a Geç",
"darkMode": "Karanlık Mod'a Geç"
},
"contactsPage": {
"title": "İletişim",
"whatsHappening": "Bu hafta neler var?",
"addContact": "Kişi Ekle",
"editContact": "Kişiyi Düzenle"
},
"button": {
"OK": "TAMAM",
"Cancel": "İptal",
"signIn": "Oturum Aç",
"signOut": "Oturum Kapat",
"complete": "Tamamlandı",
"save": "Kaydet"
},
"label": {
"welcome": "Merhaba!",
"firstName": "Ad",
"middleName": "İkinci Ad",
"lastName": "Soyad",
"stepX": "Aşama {X}"
},
"oAuth": {
"err": {
"failedTitle": "Hesabınıza bağlanılamıyor.",
"failedMsg": "Lütfen, tarayıcınızda oturum açma işlemini tamamladığınızdan emin olun."
},
"google": {
"title": "GOOGLE OTURUM AÇMA",
"instruction1": "Google Kişilerinizi içe aktarmak için web tarayıcınızı kullanarak bu uygulamaya izin vermeniz gerekir.",
"instruction2": "Simgeyi tıklayarak veya metni seçerek bu kodu panonuza kopyalayın:",
"instruction3": "Web tarayıcınızda aşağıdaki bağlantıyı açın ve yukarıdaki kodu girin:",
"instruction4": "Kayıt işlemini tamamladığınızda aşağıdaki düğmeye basın:"
}
},
"settings": {
"title": "Ayarlar",
"menu": {
"appearance": "Görünüm",
"language": "Dil",
"open": "Ayarları Aç"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "Aydınlık Mod",
"dark": "Karanlık Mod",
"system": "Adapt to System"
}
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
@@ -1,240 +0,0 @@
{
"appName": "Appflowy",
"defaultUsername": "我",
"welcomeText": "欢迎使用 @:appName",
"githubStarText": "Star on GitHub",
"subscribeNewsletterText": "消息订阅",
"letsGoButtonText": "开始",
"title": "标题",
"signUp": {
"buttonText": "注册",
"title": "注册 @:appName 账户",
"getStartedText": "开始",
"emptyPasswordError": "密码不能为空",
"repeatPasswordEmptyError": "确认密码不能为空",
"unmatchedPasswordError": "两次密码输入不一致",
"alreadyHaveAnAccount": "已有账户?",
"emailHint": "邮箱",
"passwordHint": "密码",
"repeatPasswordHint": "确认密码"
},
"signIn": {
"loginTitle": "登录 @:appName",
"loginButtonText": "登录",
"buttonText": "登录",
"forgotPassword": "忘记密码?",
"emailHint": "邮箱",
"passwordHint": "密码",
"dontHaveAnAccount": "没有已注册的账户?",
"repeatPasswordEmptyError": "确认密码不能为空",
"unmatchedPasswordError": "两次密码输入不一致"
},
"workspace": {
"create": "新建空间",
"hint": "空间",
"notFoundError": "未知的空间"
},
"shareAction": {
"buttonText": "分享",
"workInProgress": "敬请期待",
"markdown": "Markdown",
"copyLink": "复制链接"
},
"disclosureAction": {
"rename": "重命名",
"delete": "删除",
"duplicate": "复制"
},
"blankPageTitle": "空白页",
"newPageText": "新页面",
"trash": {
"text": "回收站",
"restoreAll": "全部恢复",
"deleteAll": "全部删除",
"pageHeader": {
"fileName": "文件名",
"lastModified": "最近修改",
"created": "创建"
}
},
"deletePagePrompt": {
"text": "此页面已被移动至回收站",
"restore": "恢复页面",
"deletePermanent": "彻底删除"
},
"dialogCreatePageNameHint": "页面名称",
"questionBubble": {
"whatsNew": "新功能?",
"help": "帮助 & 支持",
"debug": {
"name": "调试信息",
"success": "将调试信息复制到剪贴板!",
"fail": "无法将调试信息复制到剪贴板"
}
},
"menuAppHeader": {
"addPageTooltip": "在其中快速添加页面",
"defaultNewPageName": "未命名页面",
"renameDialog": "重命名"
},
"toolbar": {
"undo": "撤销",
"redo": "恢复",
"bold": "加粗",
"italic": "斜体",
"underline": "下划线",
"strike": "删除线",
"numList": "有序列表",
"bulletList": "无序列表",
"checkList": "任务列表",
"inlineCode": "内联代码",
"quote": "块引用",
"header": "标题",
"highlight": "高亮"
},
"tooltip": {
"lightMode": "切换到亮色模式",
"darkMode": "切换到暗色模式",
"openAsPage": "作为页面打开",
"addNewRow": "增加一行",
"openMenu": "点击打开菜单"
},
"sideBar": {
"openSidebar": "打开侧边栏",
"closeSidebar": "关闭侧边栏"
},
"notifications": {
"export": {
"markdown": "导出笔记为Markdown文档",
"path": "Documents/flowy"
}
},
"contactsPage": {
"title": "联系人",
"whatsHappening": "这周发生了哪些事?",
"addContact": "添加联系人",
"editContact": "编辑联系人"
},
"button": {
"OK": "确认",
"Cancel": "取消",
"signIn": "登录",
"signOut": "登出",
"complete": "完成",
"save": "保存"
},
"label": {
"welcome": "欢迎!",
"firstName": "名",
"middleName": "中间名",
"lastName": "姓",
"stepX": "第{X}步"
},
"oAuth": {
"err": {
"failedTitle": "无法连接到您的账户。",
"failedMsg": "请确认您已在浏览器中完成登录。"
},
"google": {
"title": "Google 账号登录",
"instruction1": "为了导入您的 Google 联系人,您需要在浏览器中给予本程序授权。",
"instruction2": "单击图标或选择文本复制到剪贴板:",
"instruction3": "进入下面的链接,然后输入上面的代码:",
"instruction4": "完成注册后,点击下面的按钮:"
}
},
"settings": {
"title": "设置",
"menu": {
"appearance": "外观",
"language": "语言",
"user": "用户",
"open": "打开设置"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"light": "日间模式",
"dark": "夜间模式",
"system": "Adapt to System"
}
}
},
"grid": {
"settings": {
"filter": "过滤器",
"sortBy": "排序",
"Properties": "属性",
"group": "组"
},
"field": {
"hide": "隐藏",
"insertLeft": "左侧插入",
"insertRight": "右侧插入",
"duplicate": "拷贝",
"delete": "删除",
"textFieldName": "文本",
"checkboxFieldName": "勾选框",
"dateFieldName": "日期",
"numberFieldName": "数字",
"singleSelectFieldName": "单项选择器",
"multiSelectFieldName": "多项选择器",
"urlFieldName": "链接",
"numberFormat": "数字格式",
"dateFormat": "日期格式",
"includeTime": "包含时间",
"dateFormatFriendly": "月 日, 年",
"dateFormatISO": "年-月-日",
"dateFormatLocal": "月/日/年",
"dateFormatUS": "年/月/日",
"timeFormat": "时间格式",
"invalidTimeFormat": "时间格式错误",
"timeFormatTwelveHour": "12小时制",
"timeFormatTwentyFourHour": "24小时制",
"addSelectOption": "添加一个标签",
"optionTitle": "标签",
"addOption": "添加标签",
"editProperty": "编辑列属性",
"newProperty": "增加一列",
"deleteFieldPromptMessage": "确定要删除这个属性吗? "
},
"row": {
"duplicate": "复制",
"delete": "删除",
"textPlaceholder": "空",
"copyProperty": "复制列",
"count": "数量",
"newRow": "添加一行"
},
"selectOption": {
"create": "新建",
"purpleColor": "紫色",
"pinkColor": "粉色",
"lightPinkColor": "浅粉色",
"orangeColor": "橙色",
"yellowColor": "黄色",
"limeColor": "鲜绿色",
"greenColor": "绿色",
"aquaColor": "水蓝色",
"blueColor": "蓝色",
"deleteTag": "删除标签",
"colorPanelTitle": "颜色",
"panelTitle": "选择或新建一个标签",
"searchOption": "搜索标签"
},
"menuName": "网格"
},
"document": {
"menuName": "文档",
"date": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
}
},
"board": {
"column": {
"create_new_card": "新建"
},
"menuName": "看板"
}
}
@@ -0,0 +1,45 @@
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('appearance settings tests', () {
testWidgets('after editing text field, button should be able to be clicked',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.appearance);
final dropDown = find.byKey(ThemeFontFamilySetting.popoverKey);
await tester.tap(dropDown);
await tester.pumpAndSettle();
final textField = find.byKey(ThemeFontFamilySetting.textFieldKey);
await tester.tap(textField);
await tester.pumpAndSettle();
await tester.enterText(textField, 'Abel');
await tester.pumpAndSettle();
final fontFamilyButton = find.byKey(const Key('Abel'));
expect(fontFamilyButton, findsOneWidget);
await tester.tap(fontFamilyButton);
await tester.pumpAndSettle();
// just switch the page and verify that the font family was set after that
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
expect(find.textContaining('Abel'), findsOneWidget);
});
});
}
@@ -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();
@@ -75,7 +60,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('create new calendar event using new button', (tester) async {
testWidgets('creating and editing calendar events', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -98,28 +83,8 @@ void main() {
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(1);
// Create another event on the same day
await tester.hoverOnTodayCalendarCell();
await tester.tapAddCalendarEventButton();
await tester.dismissRowDetailPage();
tester.assertNumberOfEventsInCalendar(2);
tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
});
testWidgets('create new calendar event by double-clicking', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Scroll until today's date cell is visible
await tester.scrollToToday();
// Double click on today's calendar cell to create a new event
await tester.doubleClickCalendarCell(DateTime.now());
@@ -130,110 +95,42 @@ void main() {
await tester.dismissRowDetailPage();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(1);
// Create another event on the same day
await tester.doubleClickCalendarCell(DateTime.now());
await tester.dismissRowDetailPage();
tester.assertNumberOfEventsInCalendar(2);
tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
});
testWidgets('duplicate/delete a calendar event', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Click on the event
await tester.openCalendarEvent(index: 0);
tester.assertRowDetailPageOpened();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Change the title of the event
await tester.editTitleInRowDetailPage('hello world');
await tester.dismissRowDetailPage();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(DateTime.now());
// 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);
tester.assertNumberOfEventsInCalendar(2, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(3, DateTime.now());
// Delete an event
await tester.openCalendarEvent(index: 0);
await tester.openCalendarEvent(index: 1);
await tester.tapRowDetailPageDeleteRowButton();
// Check that there is 1 event
tester.assertNumberOfEventsInCalendar(1);
});
testWidgets('edit the title of a calendar date event', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(DateTime.now());
await tester.dismissRowDetailPage();
// Click on the event
await tester.openCalendarEvent(index: 0);
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Change the title of the event
await tester.editTitleInRowDetailPage('hello world');
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
});
testWidgets(
'edit the date of the date field being used to layout the calendar',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(DateTime.now());
await tester.dismissRowDetailPage();
// Make sure that the event is today
tester.assertNumberofEventsOnSpecificDay(1, DateTime.now());
// 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
final tomorrow = DateTime.now().add(const Duration(days: 1));
await tester.selectDay(content: tomorrow.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, tomorrow);
});
testWidgets('reschedule an event by drag-and-drop', (tester) async {
testWidgets('rescheduling events', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -251,11 +148,42 @@ void main() {
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,
firstOfThisMonth.add(const Duration(days: 7)),
);
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);
});
});
}
@@ -11,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();
@@ -237,7 +222,7 @@ void main() {
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM d, y').format(today),
content: DateFormat('MMM dd, y').format(today),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
@@ -252,7 +237,7 @@ void main() {
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM d, y HH:mm').format(now),
content: DateFormat('MMM dd, y HH:mm').format(now),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
@@ -1,5 +1,8 @@
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -10,21 +13,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();
@@ -197,5 +185,31 @@ void main() {
await tester.pumpAndSettle();
}
});
testWidgets('add option', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditPropertyButton();
// tap 'add option' button
await tester.tapAddSelectOptionButton();
const text = 'Hello AppFlowy';
final inputField = find.descendant(
of: find.byType(CreateOptionTextField),
matching: find.byType(TextField),
);
await tester.enterText(inputField, text);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle(const Duration(seconds: 1));
// check the result
tester.expectToSeeText(text);
});
});
}
@@ -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();
@@ -2,32 +2,18 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/ime.dart';
import 'util/emoji.dart';
import 'util/util.dart';
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('row details page opens', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -186,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),
@@ -207,6 +194,48 @@ void main() {
);
});
testWidgets(
'check if the title wraps properly when a long text is inserted',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Wait for the document to be loaded
await tester.wait(500);
// Focus on the editor
final textField = find
.descendant(
of: find.byType(SimpleDialog),
matching: find.byType(TextField),
)
.first;
// Input a long text
await tester.enterText(textField, 'Long text' * 25);
await tester.pumpAndSettle();
// Tap outside to dismiss the field
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
// Check if there is any overflow in the widget tree
expect(tester.takeException(), isNull);
// Re-open the document
await tester.openFirstRowDetailPage();
// Check again if there is any overflow in the widget tree
expect(tester.takeException(), isNull);
});
testWidgets('delete row in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -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();
});
});
}
@@ -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,26 @@
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 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
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();
document_with_outline_block.main();
document_with_toggle_list_test.main();
}
@@ -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();
});
});
}
@@ -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,
@@ -0,0 +1,120 @@
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/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 SVGIconItemWidget &&
widget.tooltip ==
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
expect(
tester.widget<SVGIconItemWidget>(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);
});
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -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;
}
@@ -0,0 +1,85 @@
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('test editing link in document', () {
late MockUrlLauncher mock;
setUp(() {
mock = MockUrlLauncher();
UrlLauncherPlatform.instance = mock;
});
testWidgets('insert/edit/open link', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
const link = 'AppFlowy';
await tester.ime.insertText(link);
await tester.editor.updateSelection(
Selection.single(path: [0], startOffset: 0, endOffset: link.length),
);
// tap the link button
final linkButton = find.byTooltip(
'Link',
);
await tester.tapButton(linkButton);
expect(find.text('Add your link', findRichText: true), findsOneWidget);
// input the link
const url = 'https://appflowy.io';
final textField = find.byWidgetPredicate(
(widget) => widget is TextField && widget.decoration!.hintText == 'URL',
);
await tester.enterText(textField, url);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// single-click the link menu to show the menu
await tester.tapButton(find.text(link, findRichText: true));
expect(find.text('Open link', findRichText: true), findsOneWidget);
expect(find.text('Copy link', findRichText: true), findsOneWidget);
expect(find.text('Remove link', findRichText: true), findsOneWidget);
// double-click the link menu to open the link
mock
..setLaunchExpectations(
url: url,
useSafariVC: false,
useWebView: false,
universalLinksOnly: false,
enableJavaScript: true,
enableDomStorage: true,
headers: <String, String>{},
webOnlyWindowName: null,
launchMode: PreferredLaunchMode.platformDefault,
)
..setResponse(true);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.doubleTapAt(
tester.getTopLeft(find.text(link, findRichText: true)).translate(5, 5),
);
expect(mock.canLaunchCalled, isTrue);
expect(mock.launchCalled, isTrue);
});
});
}
@@ -0,0 +1,102 @@
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/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();
}
@@ -0,0 +1,227 @@
import 'dart:io';
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:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
group('toggle list in document', () {
void expectToggleListOpened() {
expect(find.byIcon(Icons.arrow_drop_down), findsOneWidget);
expect(find.byIcon(Icons.arrow_right), findsNothing);
}
void expectToggleListClosed() {
expect(find.byIcon(Icons.arrow_drop_down), findsNothing);
expect(find.byIcon(Icons.arrow_right), findsOneWidget);
}
testWidgets('convert > to toggle list, and click the icon to close it',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a toggle list
const text = 'This is a toggle list sample';
await tester.ime.insertText('> $text');
final editorState = tester.editor.getCurrentEditorState();
final toggleList = editorState.document.nodeAtPath([0])!;
expect(
toggleList.type,
ToggleListBlockKeys.type,
);
expect(
toggleList.attributes[ToggleListBlockKeys.collapsed],
false,
);
expect(
toggleList.delta!.toPlainText(),
text,
);
// Simulate pressing enter key to move the cursor to the next line
await tester.ime.insertCharacter('\n');
const text2 = 'This is a child node';
await tester.ime.insertText(text2);
expect(find.text(text2, findRichText: true), findsOneWidget);
// Click the toggle list icon to close it
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
await tester.tapButton(toggleListIcon);
// expect the toggle list to be closed
expect(find.text(text2, findRichText: true), findsNothing);
});
testWidgets('press enter key when the toggle list is closed',
(tester) async {
// if the toggle list is closed, press enter key will insert a new toggle list after it
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a toggle list
const text = 'Hello AppFlowy';
await tester.ime.insertText('> $text');
// Click the toggle list icon to close it
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
await tester.tapButton(toggleListIcon);
// Press the enter key
await tester.editor.updateSelection(
Selection.collapse(
[0],
'Hello '.length,
),
);
await tester.ime.insertCharacter('\n');
final editorState = tester.editor.getCurrentEditorState();
final node0 = editorState.getNodeAtPath([0])!;
final node1 = editorState.getNodeAtPath([1])!;
expect(node0.type, ToggleListBlockKeys.type);
expect(node0.attributes[ToggleListBlockKeys.collapsed], true);
expect(node0.delta!.toPlainText(), 'Hello ');
expect(node1.type, ToggleListBlockKeys.type);
expect(node1.delta!.toPlainText(), 'AppFlowy');
});
testWidgets('press enter key when the toggle list is open', (tester) async {
// if the toggle list is open, press enter key will insert a new paragraph inside it
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a toggle list
const text = 'Hello AppFlowy';
await tester.ime.insertText('> $text');
// Press the enter key
await tester.editor.updateSelection(
Selection.collapse(
[0],
'Hello '.length,
),
);
await tester.ime.insertCharacter('\n');
final editorState = tester.editor.getCurrentEditorState();
final node0 = editorState.getNodeAtPath([0])!;
final node00 = editorState.getNodeAtPath([0, 0])!;
final node1 = editorState.getNodeAtPath([1]);
expect(node0.type, ToggleListBlockKeys.type);
expect(node0.attributes[ToggleListBlockKeys.collapsed], false);
expect(node0.delta!.toPlainText(), 'Hello ');
expect(node00.type, ParagraphBlockKeys.type);
expect(node00.delta!.toPlainText(), 'AppFlowy');
expect(node1, isNull);
});
testWidgets('clear the format if toggle list if empty', (tester) async {
// if the toggle list is open, press enter key will insert a new paragraph inside it
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a toggle list
await tester.ime.insertText('> ');
// Press the enter key
// Click the toggle list icon to close it
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
await tester.tapButton(toggleListIcon);
await tester.editor.updateSelection(
Selection.collapse(
[0],
0,
),
);
await tester.ime.insertCharacter('\n');
final editorState = tester.editor.getCurrentEditorState();
final node0 = editorState.getNodeAtPath([0])!;
expect(node0.type, ParagraphBlockKeys.type);
});
testWidgets('use cmd/ctrl + enter to open/close the toggle list',
(tester) async {
// if the toggle list is open, press enter key will insert a new paragraph inside it
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a toggle list
await tester.ime.insertText('> Hello');
expectToggleListOpened();
await tester.editor.updateSelection(
Selection.collapse(
[0],
0,
),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.enter,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isLinux || Platform.isWindows,
);
expectToggleListClosed();
await tester.simulateKeyEvent(
LogicalKeyboardKey.enter,
isMetaPressed: Platform.isMacOS,
isControlPressed: Platform.isLinux || Platform.isWindows,
);
expectToggleListOpened();
});
});
}
@@ -6,24 +6,12 @@ import 'package:flutter/services.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('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();
@@ -110,7 +98,6 @@ void main() {
});
}
// TODO(Lucas.Xu): there're no shorctcuts for underline, format code yet.
const _sample = r'''
# Heading 1
## Heading 2
@@ -118,7 +105,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(
@@ -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(),
);
});
});
@@ -0,0 +1,69 @@
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/base.dart';
import 'util/common_operations.dart';
const _readmeName = 'Read me';
const _documentName = 'Document';
const _calendarName = 'Calendar';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Tabs', () {
testWidgets('Open AppFlowy and open/navigate multiple tabs',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsNothing,
);
await tester.createNewPageWithName(ViewLayoutPB.Calendar, _calendarName);
await tester.createNewPageWithName(ViewLayoutPB.Document, _documentName);
// Navigate current view to "Read me" document again
await tester.tapButtonWithName(_readmeName);
/// Open second menu item in a new tab
await tester.openAppInNewTab(_calendarName);
/// Open third menu item in a new tab
await tester.openAppInNewTab(_documentName);
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byType(TabBar),
matching: find.byType(FlowyTab),
),
findsNWidgets(3),
);
/// Navigate to the first tab
await tester.tap(
find.descendant(
of: find.byType(FlowyTab),
matching: find.text(_readmeName),
),
);
});
});
}
@@ -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,18 @@ 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,
@@ -134,20 +146,22 @@ extension AppFlowyTestBase on WidgetTester {
bool warnIfMissed = true,
int milliseconds = 500,
}) async {
await tapButton(
await tap(
finder,
pointer: pointer,
buttons: buttons,
warnIfMissed: warnIfMissed,
milliseconds: kDoubleTapMinTime.inMilliseconds,
);
await tapButton(
await pump(kDoubleTapMinTime);
await tap(
finder,
pointer: pointer,
buttons: buttons,
pointer: pointer,
warnIfMissed: warnIfMissed,
milliseconds: milliseconds,
);
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> wait(int milliseconds) async {
@@ -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);
@@ -229,21 +224,26 @@ extension CommonOperations on WidgetTester {
await tapButton(markdownButton);
}
Future<void> createNewPageWithName(ViewLayoutPB layout, String name) async {
Future<void> createNewPageWithName(
ViewLayoutPB layout, [
String? name,
]) async {
// create a new page
await tapAddButton();
await tapButtonWithName(layout.menuName);
await pumpAndSettle();
// hover on it and change it's name
await hoverOnPageName(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
onHover: () async {
await renamePage(name);
await pumpAndSettle();
},
);
await pumpAndSettle();
if (name != null) {
await hoverOnPageName(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
onHover: () async {
await renamePage(name);
await pumpAndSettle();
},
);
await pumpAndSettle();
}
}
Future<void> simulateKeyEvent(
@@ -281,6 +281,14 @@ extension CommonOperations on WidgetTester {
}
await pumpAndSettle();
}
Future<void> openAppInNewTab(String name) async {
await hoverOnPageName(name);
await tap(find.byType(ViewDisclosureButton));
await pumpAndSettle();
await tap(find.text(LocaleKeys.disclosureAction_openNewTab.tr()));
await pumpAndSettle();
}
}
extension ViewLayoutPBTest on ViewLayoutPB {
@@ -2,6 +2,7 @@ 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';
@@ -40,6 +41,7 @@ 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';
@@ -71,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
@@ -81,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');
}
@@ -294,7 +303,17 @@ extension AppFlowyDatabaseTest on WidgetTester {
matching: findDay,
);
await tapButton(finder);
// 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 {
@@ -434,8 +453,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> dismissRowDetailPage() async {
await sendKeyEvent(LogicalKeyboardKey.escape);
// 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 {
@@ -461,17 +487,6 @@ 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> tapEmoji(String emoji) async {
final emojiWidget = find.text(emoji);
await tapButton(emojiWidget);
}
Future<void> tapDateCellInRowDetailPage() async {
final findDateCell = find.byType(GridDateCell);
await tapButton(findDateCell);
@@ -978,9 +993,20 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> scrollToToday() async {
final todayCell = find.byWidgetPredicate(
(widget) => widget is CalendarDayCard && widget.isToday,
skipOffstage: false,
);
await ensureVisible(todayCell);
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));
}
@@ -1012,7 +1038,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(findEvents, findsNWidgets(number));
}
void assertNumberofEventsOnSpecificDay(
void assertNumberOfEventsOnSpecificDay(
int number,
DateTime date, {
String? title,
@@ -1039,8 +1065,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
final todayCell = find.byWidgetPredicate(
(widget) => widget is CalendarDayCard && isSameDay(date, widget.date),
);
await doubleTapButton(todayCell);
final location = getTopLeft(todayCell).translate(10, 10);
await doubleTapAt(location);
}
Future<void> openCalendarEvent({required index, DateTime? date}) async {
@@ -1171,6 +1197,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> tapDatabaseRawDataButton() async {
await tapButtonWithName(LocaleKeys.importPanel_database.tr());
}
Future<void> tapAddSelectOptionButton() async {
await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr());
}
}
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
@@ -1,7 +1,17 @@
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';
import 'util.dart';
extension EditorWidgetTester on WidgetTester {
@@ -21,19 +31,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 +141,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 +153,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(
@@ -0,0 +1,110 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:url_launcher_platform_interface/link.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
class MockUrlLauncher extends Fake
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {
String? url;
PreferredLaunchMode? launchMode;
bool? useSafariVC;
bool? useWebView;
bool? enableJavaScript;
bool? enableDomStorage;
bool? universalLinksOnly;
Map<String, String>? headers;
String? webOnlyWindowName;
bool? response;
bool closeWebViewCalled = false;
bool canLaunchCalled = false;
bool launchCalled = false;
// ignore: use_setters_to_change_properties
void setCanLaunchExpectations(String url) {
this.url = url;
}
void setLaunchExpectations({
required String url,
PreferredLaunchMode? launchMode,
bool? useSafariVC,
bool? useWebView,
required bool enableJavaScript,
required bool enableDomStorage,
required bool universalLinksOnly,
required Map<String, String> headers,
required String? webOnlyWindowName,
}) {
this.url = url;
this.launchMode = launchMode;
this.useSafariVC = useSafariVC;
this.useWebView = useWebView;
this.enableJavaScript = enableJavaScript;
this.enableDomStorage = enableDomStorage;
this.universalLinksOnly = universalLinksOnly;
this.headers = headers;
this.webOnlyWindowName = webOnlyWindowName;
}
// ignore: use_setters_to_change_properties
void setResponse(bool response) {
this.response = response;
}
@override
LinkDelegate? get linkDelegate => null;
@override
Future<bool> canLaunch(String url) async {
expect(url, this.url);
canLaunchCalled = true;
return response!;
}
@override
Future<bool> launch(
String url, {
required bool useSafariVC,
required bool useWebView,
required bool enableJavaScript,
required bool enableDomStorage,
required bool universalLinksOnly,
required Map<String, String> headers,
String? webOnlyWindowName,
}) async {
expect(url, this.url);
expect(useSafariVC, this.useSafariVC);
expect(useWebView, this.useWebView);
expect(enableJavaScript, this.enableJavaScript);
expect(enableDomStorage, this.enableDomStorage);
expect(universalLinksOnly, this.universalLinksOnly);
expect(headers, this.headers);
expect(webOnlyWindowName, this.webOnlyWindowName);
launchCalled = true;
return response!;
}
@override
Future<bool> launchUrl(String url, LaunchOptions options) async {
expect(url, this.url);
expect(options.mode, launchMode);
expect(options.webViewConfiguration.enableJavaScript, enableJavaScript);
expect(options.webViewConfiguration.enableDomStorage, enableDomStorage);
expect(options.webViewConfiguration.headers, headers);
expect(options.webOnlyWindowName, webOnlyWindowName);
launchCalled = true;
return response!;
}
@override
Future<void> closeWebView() async {
closeWebViewCalled = true;
}
}
@@ -4,3 +4,5 @@ export 'settings.dart';
export 'data.dart';
export 'expectation.dart';
export 'editor_test_operations.dart';
export 'mock/mock_url_launcher.dart';
export 'ime.dart';
@@ -1,38 +0,0 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart';
class Config {
static Future<void> setSupabaseConfig({
required String url,
required String anonKey,
required String key,
required String secret,
}) async {
await ConfigEventSetSupabaseConfig(
SupabaseConfigPB.create()
..supabaseUrl = url
..key = key
..anonKey = anonKey
..jwtSecret = secret,
).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,15 @@ class KVKeys {
/// The value is a json string with the following format:
/// {'height': 600.0, 'width': 800.0}
static const String windowSize = 'windowSize';
/// The key for saving the window position
///
/// The value is a json string with the following format:
/// {'dx': 10.0, 'dy': 10.0}
static const String windowPosition = 'windowPosition';
static const String kDocumentAppearanceFontSize =
'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily =
'kDocumentAppearanceFontFamily';
}
@@ -1,11 +1,12 @@
import 'package:flutter/foundation.dart' show TargetPlatform;
import 'package:flutter/foundation.dart' show TargetPlatform, kIsWeb;
extension TargetPlatformHelper on TargetPlatform {
/// Convenience function to check if the app is running on a desktop computer.
///
/// Easily check if on desktop by checking `defaultTargetPlatform.isDesktop`.
bool get isDesktop =>
this == TargetPlatform.linux ||
this == TargetPlatform.macOS ||
this == TargetPlatform.windows;
!kIsWeb &&
(this == TargetPlatform.linux ||
this == TargetPlatform.macOS ||
this == TargetPlatform.windows);
}
@@ -0,0 +1,11 @@
import 'package:flutter/services.dart';
extension RawKeyboardExtension on RawKeyboard {
bool get isAltPressed => keysPressed.any(
(key) => [
LogicalKeyboardKey.alt,
LogicalKeyboardKey.altLeft,
LogicalKeyboardKey.altRight,
].contains(key),
);
}
+50 -7
View File
@@ -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,46 @@ 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;
@EnviedField(
obfuscate: true,
varName: 'ENABLE_SUPABASE_SYNC',
defaultValue: true,
)
static final bool enableSupabaseSync = _Env.enableSupabaseSync;
}
bool get isSupabaseEnable =>
Env.supabaseUrl.isNotEmpty &&
Env.supabaseAnonKey.isNotEmpty &&
Env.supabaseKey.isNotEmpty &&
Env.supabaseJwtSecret.isNotEmpty;
bool get isSupabaseEnable => false;
// Env.supabaseUrl.isNotEmpty &&
// Env.supabaseAnonKey.isNotEmpty &&
// Env.supabaseKey.isNotEmpty &&
// Env.supabaseJwtSecret.isNotEmpty &&
// Env.supabaseDb.isNotEmpty &&
// Env.supabaseDbUser.isNotEmpty &&
// Env.supabaseDbPassword.isNotEmpty &&
// Env.supabaseDbPort.isNotEmpty;
@@ -42,6 +42,9 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
@override
Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
@override
Widget tabBarItem(String pluginId) => leftBarItem;
@override
Widget buildWidget({PluginContext? context}) => const BlankPage();
@@ -24,38 +24,38 @@ class CellCacheKey {
/// We use GridCellCacheKey to index the cell in the cache.
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid
/// for more information
class CellCache {
class CellMemCache {
final String viewId;
/// fieldId: {cacheKey: GridCell}
final Map<String, Map<RowId, dynamic>> _cellDataByFieldId = {};
CellCache({
final Map<String, Map<RowId, dynamic>> _cellByFieldId = {};
CellMemCache({
required this.viewId,
});
void removeCellWithFieldId(String fieldId) {
_cellDataByFieldId.remove(fieldId);
_cellByFieldId.remove(fieldId);
}
void remove(CellCacheKey key) {
final map = _cellDataByFieldId[key.fieldId];
final map = _cellByFieldId[key.fieldId];
if (map != null) {
map.remove(key.rowId);
}
}
void insert<T extends DatabaseCell>(CellCacheKey key, T value) {
var map = _cellDataByFieldId[key.fieldId];
var map = _cellByFieldId[key.fieldId];
if (map == null) {
_cellDataByFieldId[key.fieldId] = {};
map = _cellDataByFieldId[key.fieldId];
_cellByFieldId[key.fieldId] = {};
map = _cellByFieldId[key.fieldId];
}
map![key.rowId] = value.object;
}
T? get<T>(CellCacheKey key) {
final map = _cellDataByFieldId[key.fieldId];
final map = _cellByFieldId[key.fieldId];
if (map == null) {
return null;
} else {
@@ -72,6 +72,6 @@ class CellCache {
}
Future<void> dispose() async {
_cellDataByFieldId.clear();
_cellByFieldId.clear();
}
}
@@ -24,7 +24,7 @@ import 'cell_service.dart';
// ignore: must_be_immutable
class CellController<T, D> extends Equatable {
DatabaseCellContext _cellContext;
final CellCache _cellCache;
final CellMemCache _cellCache;
final CellCacheKey _cacheKey;
final FieldBackendService _fieldBackendSvc;
final CellDataLoader<T> _cellDataLoader;
@@ -54,7 +54,7 @@ class CellController<T, D> extends Equatable {
CellController({
required DatabaseCellContext cellContext,
required CellCache cellCache,
required CellMemCache cellCache,
required CellDataLoader<T> cellDataLoader,
required CellDataPersistence<D> cellDataPersistence,
}) : _cellContext = cellContext,
@@ -103,12 +103,15 @@ class CellController<T, D> extends Equatable {
},
);
_rowMetaListener?.start(
callback: (newRowMeta) {
_cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
_onRowMetaChanged?.call();
},
);
// Only the primary can listen on the row meta changes.
if (_cellContext.fieldInfo.isPrimary) {
_rowMetaListener?.start(
callback: (newRowMeta) {
_cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
_onRowMetaChanged?.call();
},
);
}
}
/// Listen on the cell content or field changes
@@ -18,11 +18,11 @@ typedef URLCellController = CellController<URLCellDataPB, String>;
class CellControllerBuilder {
final DatabaseCellContext _cellContext;
final CellCache _cellCache;
final CellMemCache _cellCache;
CellControllerBuilder({
required DatabaseCellContext cellContext,
required CellCache cellCache,
required CellMemCache cellCache,
}) : _cellCache = cellCache,
_cellContext = cellContext;
@@ -17,13 +17,13 @@ typedef OnDatabaseChanged = void Function(DatabasePB);
typedef OnRowsCreated = void Function(List<RowId> ids);
typedef OnRowsUpdated = void Function(
List<RowId> ids,
RowsChangedReason reason,
ChangedReason reason,
);
typedef OnRowsDeleted = void Function(List<RowId> ids);
typedef OnNumOfRowsChanged = void Function(
UnmodifiableListView<RowInfo> rows,
UnmodifiableMapView<RowId, RowInfo> rowByRowId,
RowsChangedReason reason,
ChangedReason reason,
);
typedef OnError = void Function(FlowyError);
@@ -687,27 +687,29 @@ class FieldController {
}
}
class RowDelegatesImpl extends RowFieldsDelegate with RowCacheDelegate {
final FieldController _cache;
class RowCacheDependenciesImpl extends RowFieldsDelegate with RowLifeCycle {
final FieldController _fieldController;
OnReceiveFields? _onFieldFn;
RowDelegatesImpl(FieldController cache) : _cache = cache;
RowCacheDependenciesImpl(FieldController cache) : _fieldController = cache;
@override
UnmodifiableListView<FieldInfo> get fields =>
UnmodifiableListView(_cache.fieldInfos);
UnmodifiableListView(_fieldController.fieldInfos);
@override
void onFieldsChanged(void Function(List<FieldInfo>) callback) {
_onFieldFn = (fieldInfos) {
callback(fieldInfos);
};
_cache.addListener(onReceiveFields: _onFieldFn);
if (_onFieldFn != null) {
_fieldController.removeListener(onFieldsListener: _onFieldFn!);
}
_onFieldFn = (fieldInfos) => callback(fieldInfos);
_fieldController.addListener(onReceiveFields: _onFieldFn);
}
@override
void onRowDispose() {
void onRowDisposed() {
if (_onFieldFn != null) {
_cache.removeListener(onFieldsListener: _onFieldFn!);
_fieldController.removeListener(onFieldsListener: _onFieldFn!);
_onFieldFn = null;
}
}
@@ -13,30 +13,25 @@ part 'row_cache.freezed.dart';
typedef RowUpdateCallback = void Function();
/// A delegate that provides the fields of the row.
abstract class RowFieldsDelegate {
UnmodifiableListView<FieldInfo> get fields;
void onFieldsChanged(void Function(List<FieldInfo>) callback);
}
abstract mixin class RowCacheDelegate {
UnmodifiableListView<FieldInfo> get fields;
void onRowDispose();
abstract mixin class RowLifeCycle {
void onRowDisposed();
}
/// Cache the rows in memory
/// Insert / delete / update row
///
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information.
class RowCache {
final String viewId;
/// _rows contains the current block's rows
/// Use List to reverse the order of the GridRow.
final RowList _rowList = RowList();
final CellCache _cellCache;
final RowCacheDelegate _delegate;
final RowChangesetNotifier _rowChangeReasonNotifier;
final CellMemCache _cellMemCache;
final RowLifeCycle _rowLifeCycle;
final RowFieldsDelegate _fieldDelegate;
final RowChangesetNotifier _changedNotifier;
/// Returns a unmodifiable list of RowInfo
UnmodifiableListView<RowInfo> get rowInfos {
@@ -44,29 +39,29 @@ class RowCache {
return UnmodifiableListView(visibleRows);
}
/// Returns a unmodifiable map of rowId to RowInfo
/// Returns a unmodifiable map of RowInfo
UnmodifiableMapView<RowId, RowInfo> get rowByRowId {
return UnmodifiableMapView(_rowList.rowInfoByRowId);
}
CellCache get cellCache => _cellCache;
RowsChangedReason get changeReason => _rowChangeReasonNotifier.reason;
CellMemCache get cellCache => _cellMemCache;
ChangedReason get changeReason => _changedNotifier.reason;
RowCache({
required this.viewId,
required RowFieldsDelegate fieldsDelegate,
required RowCacheDelegate cacheDelegate,
}) : _cellCache = CellCache(viewId: viewId),
_rowChangeReasonNotifier = RowChangesetNotifier(),
_delegate = cacheDelegate {
//
required RowLifeCycle rowLifeCycle,
}) : _cellMemCache = CellMemCache(viewId: viewId),
_changedNotifier = RowChangesetNotifier(),
_rowLifeCycle = rowLifeCycle,
_fieldDelegate = fieldsDelegate {
// Listen on the changed of the fields. If the fields changed, we need to
// clear the cell cache with the given field id.
fieldsDelegate.onFieldsChanged((fieldInfos) {
for (final fieldInfo in fieldInfos) {
_cellCache.removeCellWithFieldId(fieldInfo.id);
_cellMemCache.removeCellWithFieldId(fieldInfo.id);
}
_rowChangeReasonNotifier
.receive(const RowsChangedReason.fieldDidChange());
_changedNotifier.receive(const ChangedReason.fieldDidChange());
});
}
@@ -82,9 +77,9 @@ class RowCache {
}
Future<void> dispose() async {
_delegate.onRowDispose();
_rowChangeReasonNotifier.dispose();
await _cellCache.dispose();
_rowLifeCycle.onRowDisposed();
_changedNotifier.dispose();
await _cellMemCache.dispose();
}
void applyRowsChanged(RowsChangePB changeset) {
@@ -100,7 +95,7 @@ class RowCache {
void reorderAllRows(List<String> rowIds) {
_rowList.reorderWithRowIds(rowIds);
_rowChangeReasonNotifier.receive(const RowsChangedReason.reorderRows());
_changedNotifier.receive(const ChangedReason.reorderRows());
}
void reorderSingleRow(ReorderSingleRowPB reorderRow) {
@@ -111,8 +106,8 @@ class RowCache {
reorderRow.oldIndex,
reorderRow.newIndex,
);
_rowChangeReasonNotifier.receive(
RowsChangedReason.reorderSingleRow(
_changedNotifier.receive(
ChangedReason.reorderSingleRow(
reorderRow,
rowInfo,
),
@@ -124,7 +119,7 @@ class RowCache {
for (final rowId in deletedRowIds) {
final deletedRow = _rowList.remove(rowId);
if (deletedRow != null) {
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow));
_changedNotifier.receive(ChangedReason.delete(deletedRow));
}
}
}
@@ -134,8 +129,7 @@ class RowCache {
final insertedIndex =
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
if (insertedIndex != null) {
_rowChangeReasonNotifier
.receive(RowsChangedReason.insert(insertedIndex));
_changedNotifier.receive(ChangedReason.insert(insertedIndex));
}
}
}
@@ -149,7 +143,7 @@ class RowCache {
fieldId: fieldId,
rowId: updatedRow.rowId,
);
_cellCache.remove(key);
_cellMemCache.remove(key);
}
if (updatedRow.hasRowMeta()) {
updatedList.add(updatedRow.rowMeta);
@@ -160,7 +154,7 @@ class RowCache {
_rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId));
if (updatedIndexs.isNotEmpty) {
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
_changedNotifier.receive(ChangedReason.update(updatedIndexs));
}
}
@@ -168,7 +162,7 @@ class RowCache {
for (final rowId in invisibleRows) {
final deletedRow = _rowList.remove(rowId);
if (deletedRow != null) {
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow));
_changedNotifier.receive(ChangedReason.delete(deletedRow));
}
}
}
@@ -178,62 +172,45 @@ class RowCache {
final insertedIndex =
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
if (insertedIndex != null) {
_rowChangeReasonNotifier
.receive(RowsChangedReason.insert(insertedIndex));
_changedNotifier.receive(ChangedReason.insert(insertedIndex));
}
}
}
void onRowsChanged(void Function(RowsChangedReason) onRowChanged) {
_rowChangeReasonNotifier.addListener(() {
onRowChanged(_rowChangeReasonNotifier.reason);
void onRowsChanged(void Function(ChangedReason) onRowChanged) {
_changedNotifier.addListener(() {
onRowChanged(_changedNotifier.reason);
});
}
RowUpdateCallback addListener({
required RowId rowId,
void Function(CellContextByFieldId, RowsChangedReason)? onCellUpdated,
bool Function()? listenWhen,
void Function(CellContextByFieldId, ChangedReason)? onRowChanged,
}) {
listenerHandler() async {
if (listenWhen != null && listenWhen() == false) {
return;
}
notifyUpdate() {
if (onCellUpdated != null) {
final rowInfo = _rowList.get(rowId);
if (rowInfo != null) {
final CellContextByFieldId cellDataMap = _makeGridCells(
rowInfo.rowMeta,
);
onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
}
if (onRowChanged != null) {
final rowInfo = _rowList.get(rowId);
if (rowInfo != null) {
final cellDataMap = _makeCells(rowInfo.rowMeta);
onRowChanged(cellDataMap, _changedNotifier.reason);
}
}
_rowChangeReasonNotifier.reason.whenOrNull(
update: (indexs) {
if (indexs[rowId] != null) notifyUpdate();
},
fieldDidChange: () => notifyUpdate(),
);
}
_rowChangeReasonNotifier.addListener(listenerHandler);
_changedNotifier.addListener(listenerHandler);
return listenerHandler;
}
void removeRowListener(VoidCallback callback) {
_rowChangeReasonNotifier.removeListener(callback);
_changedNotifier.removeListener(callback);
}
CellContextByFieldId loadGridCells(RowMetaPB rowMeta) {
CellContextByFieldId loadCells(RowMetaPB rowMeta) {
final rowInfo = _rowList.get(rowMeta.id);
if (rowInfo == null) {
_loadRow(rowMeta.id);
}
return _makeGridCells(rowMeta);
return _makeCells(rowMeta);
}
Future<void> _loadRow(RowId rowId) async {
@@ -257,18 +234,17 @@ class RowCache {
rowId: rowMetaPB.id,
);
_rowChangeReasonNotifier
.receive(RowsChangedReason.update(updatedIndexs));
_changedNotifier.receive(ChangedReason.update(updatedIndexs));
}
},
(err) => Log.error(err),
);
}
CellContextByFieldId _makeGridCells(RowMetaPB rowMeta) {
CellContextByFieldId _makeCells(RowMetaPB rowMeta) {
// ignore: prefer_collection_literals
final cellContextMap = CellContextByFieldId();
for (final field in _delegate.fields) {
for (final field in _fieldDelegate.fields) {
if (field.visibility) {
cellContextMap[field.id] = DatabaseCellContext(
rowMeta: rowMeta,
@@ -283,7 +259,7 @@ class RowCache {
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
return RowInfo(
viewId: viewId,
fields: _delegate.fields,
fields: _fieldDelegate.fields,
rowId: rowMetaPB.id,
rowMeta: rowMetaPB,
);
@@ -291,11 +267,11 @@ class RowCache {
}
class RowChangesetNotifier extends ChangeNotifier {
RowsChangedReason reason = const InitialListState();
ChangedReason reason = const InitialListState();
RowChangesetNotifier();
void receive(RowsChangedReason newReason) {
void receive(ChangedReason newReason) {
reason = newReason;
reason.map(
insert: (_) => notifyListeners(),
@@ -326,14 +302,14 @@ typedef DeletedIndexs = List<DeletedIndex>;
typedef UpdatedIndexMap = LinkedHashMap<RowId, UpdatedIndex>;
@freezed
class RowsChangedReason with _$RowsChangedReason {
const factory RowsChangedReason.insert(InsertedIndex item) = _Insert;
const factory RowsChangedReason.delete(DeletedIndex item) = _Delete;
const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update;
const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
const factory RowsChangedReason.initial() = InitialListState;
const factory RowsChangedReason.reorderRows() = _ReorderRows;
const factory RowsChangedReason.reorderSingleRow(
class ChangedReason with _$ChangedReason {
const factory ChangedReason.insert(InsertedIndex item) = _Insert;
const factory ChangedReason.delete(DeletedIndex item) = _Delete;
const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update;
const factory ChangedReason.fieldDidChange() = _FieldDidChange;
const factory ChangedReason.initial() = InitialListState;
const factory ChangedReason.reorderRows() = _ReorderRows;
const factory ChangedReason.reorderSingleRow(
ReorderSingleRowPB reorderRow,
RowInfo rowInfo,
) = _ReorderSingleRow;
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import '../cell/cell_service.dart';
import 'row_cache.dart';
typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason);
typedef OnRowChanged = void Function(CellContextByFieldId, ChangedReason);
class RowController {
final RowMetaPB rowMeta;
@@ -24,13 +24,13 @@ class RowController {
}) : _rowCache = rowCache;
CellContextByFieldId loadData() {
return _rowCache.loadGridCells(rowMeta);
return _rowCache.loadCells(rowMeta);
}
void addListener({OnRowChanged? onRowChanged}) {
final fn = _rowCache.addListener(
rowId: rowMeta.id,
onCellUpdated: onRowChanged,
onRowChanged: onRowChanged,
);
// Add the listener to the list so that we can remove it later.
@@ -0,0 +1,68 @@
import 'dart:typed_data';
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:dartz/dartz.dart';
typedef DidFetchRowCallback = void Function(DidFetchRowPB);
typedef RowMetaCallback = void Function(RowMetaPB);
class RowListener {
final String rowId;
DidFetchRowCallback? _onRowFetchedCallback;
RowMetaCallback? _onMetaChangedCallback;
DatabaseNotificationListener? _listener;
RowListener(this.rowId);
/// OnMetaChanged will be called when the row meta is changed.
/// OnRowFetched will be called when the row is fetched from remote storage
void start({
RowMetaCallback? onMetaChanged,
DidFetchRowCallback? onRowFetched,
}) {
_onMetaChangedCallback = onMetaChanged;
_onRowFetchedCallback = onRowFetched;
_listener = DatabaseNotificationListener(
objectId: rowId,
handler: _handler,
);
}
void _handler(
DatabaseNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case DatabaseNotification.DidUpdateRowMeta:
result.fold(
(payload) {
if (_onMetaChangedCallback != null) {
_onMetaChangedCallback!(RowMetaPB.fromBuffer(payload));
}
},
(error) => Log.error(error),
);
break;
case DatabaseNotification.DidFetchRow:
result.fold(
(payload) {
if (_onRowFetchedCallback != null) {
_onRowFetchedCallback!(DidFetchRowPB.fromBuffer(payload));
}
},
(error) => Log.error(error),
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_onMetaChangedCallback = null;
_onRowFetchedCallback = null;
}
}
@@ -27,20 +27,32 @@ class DatabasePropertyBloc
_startListening();
},
setFieldVisibility: (_SetFieldVisibility value) async {
final fieldBackendSvc =
FieldBackendService(viewId: viewId, fieldId: value.fieldId);
final result =
await fieldBackendSvc.updateField(visibility: value.visibility);
result.fold(
(l) => null,
(err) => Log.error(err),
final fieldBackendSvc = FieldBackendService(
viewId: viewId,
fieldId: value.fieldId,
);
final result = await fieldBackendSvc.updateField(
visibility: value.visibility,
);
result.fold((l) => null, (err) => Log.error(err));
},
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
emit(state.copyWith(fieldContexts: value.fields));
},
moveField: (_MoveField value) {
//
moveField: (_MoveField value) async {
final fieldBackendService = FieldBackendService(
viewId: viewId,
fieldId: value.fieldId,
);
final result = await fieldBackendService.moveField(
value.fromIndex,
value.toIndex,
);
result.fold((l) => null, (r) => Log.error(r));
},
);
},
@@ -76,8 +88,11 @@ class DatabasePropertyEvent with _$DatabasePropertyEvent {
const factory DatabasePropertyEvent.didReceiveFieldUpdate(
List<FieldInfo> fields,
) = _DidReceiveFieldUpdate;
const factory DatabasePropertyEvent.moveField(int fromIndex, int toIndex) =
_MoveField;
const factory DatabasePropertyEvent.moveField({
required String fieldId,
required int fromIndex,
required int toIndex,
}) = _MoveField;
}
@freezed
@@ -46,11 +46,11 @@ class DatabaseViewCache {
required this.viewId,
required FieldController fieldController,
}) : _databaseViewListener = DatabaseViewListener(viewId: viewId) {
final delegate = RowDelegatesImpl(fieldController);
final depsImpl = RowCacheDependenciesImpl(fieldController);
_rowCache = RowCache(
viewId: viewId,
fieldsDelegate: delegate,
cacheDelegate: delegate,
fieldsDelegate: depsImpl,
rowLifeCycle: depsImpl,
);
_databaseViewListener.start(
@@ -6,7 +6,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
@@ -96,7 +96,10 @@ class BoardPage extends StatelessWidget {
(_) => BoardContent(
onEditStateChanged: onEditStateChanged,
),
(err) => FlowyErrorPage.message(err.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),),
(err) => FlowyErrorPage.message(
err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
);
},
);
@@ -24,7 +24,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
// Getters
String get viewId => databaseController.viewId;
FieldController get fieldController => databaseController.fieldController;
CellCache get cellCache => databaseController.rowCache.cellCache;
CellMemCache get cellCache => databaseController.rowCache.cellCache;
RowCache get rowCache => databaseController.rowCache;
CalendarBloc({required ViewPB view, required this.databaseController})
@@ -21,7 +21,7 @@ class UnscheduleEventsBloc
// Getters
String get viewId => databaseController.viewId;
FieldController get fieldController => databaseController.fieldController;
CellCache get cellCache => databaseController.rowCache.cellCache;
CellMemCache get cellCache => databaseController.rowCache.cellCache;
RowCache get rowCache => databaseController.rowCache;
UnscheduleEventsBloc({
@@ -14,7 +14,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_data_controller.dart';
import '../../application/row/row_controller.dart';
import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
@@ -153,7 +153,7 @@ class GridEvent with _$GridEvent {
const factory GridEvent.moveRow(int from, int to) = _MoveRow;
const factory GridEvent.didLoadRows(
List<RowInfo> rows,
RowsChangedReason reason,
ChangedReason reason,
) = _DidReceiveRowUpdate;
const factory GridEvent.didReceiveFieldUpdate(
List<FieldInfo> fields,
@@ -179,7 +179,7 @@ class GridState with _$GridState {
required int rowCount,
required GridLoadingState loadingState,
required bool reorderable,
required RowsChangedReason reason,
required ChangedReason reason,
required List<SortInfo> sorts,
required List<FilterInfo> filters,
}) = _GridState;
@@ -1,4 +1,6 @@
import 'dart:collection';
import 'package:appflowy/plugins/database_view/application/row/row_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -7,7 +9,7 @@ import 'dart:async';
import '../../../application/cell/cell_service.dart';
import '../../../application/field/field_controller.dart';
import '../../../application/row/row_cache.dart';
import '../../../application/row/row_data_controller.dart';
import '../../../application/row/row_controller.dart';
import '../../../application/row/row_service.dart';
part 'row_bloc.freezed.dart';
@@ -15,6 +17,7 @@ part 'row_bloc.freezed.dart';
class RowBloc extends Bloc<RowEvent, RowState> {
final RowBackendService _rowBackendSvc;
final RowController _dataController;
final RowListener _rowListener;
final String viewId;
final String rowId;
@@ -24,6 +27,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
required RowController dataController,
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
_dataController = dataController,
_rowListener = RowListener(rowId),
super(RowState.initial(dataController.loadData())) {
on<RowEvent>(
(event, emit) async {
@@ -46,6 +50,9 @@ class RowBloc extends Bloc<RowEvent, RowState> {
),
);
},
reloadRow: (DidFetchRowPB row) {
emit(state.copyWith(rowSource: RowSourece.remote(row)));
},
);
},
);
@@ -54,6 +61,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
@override
Future<void> close() async {
_dataController.dispose();
await _rowListener.stop();
return super.close();
}
@@ -65,6 +73,14 @@ class RowBloc extends Bloc<RowEvent, RowState> {
}
},
);
_rowListener.start(
onRowFetched: (fetchRow) {
if (!isClosed) {
add(RowEvent.reloadRow(fetchRow));
}
},
);
}
}
@@ -72,9 +88,10 @@ class RowBloc extends Bloc<RowEvent, RowState> {
class RowEvent with _$RowEvent {
const factory RowEvent.initial() = _InitialRow;
const factory RowEvent.createRow() = _CreateRow;
const factory RowEvent.reloadRow(DidFetchRowPB row) = _ReloadRow;
const factory RowEvent.didReceiveCells(
CellContextByFieldId cellsByFieldId,
RowsChangedReason reason,
ChangedReason reason,
) = _DidReceiveCells;
}
@@ -83,7 +100,8 @@ class RowState with _$RowState {
const factory RowState({
required CellContextByFieldId cellByFieldId,
required UnmodifiableListView<GridCellEquatable> cells,
RowsChangedReason? changeReason,
required RowSourece rowSource,
ChangedReason? changeReason,
}) = _RowState;
factory RowState.initial(
@@ -96,6 +114,7 @@ class RowState with _$RowState {
.map((e) => GridCellEquatable(e.fieldInfo))
.toList(),
),
rowSource: const RowSourece.disk(),
);
}
@@ -112,3 +131,11 @@ class GridCellEquatable extends Equatable {
_fieldContext.width,
];
}
@freezed
class RowSourece with _$RowSourece {
const factory RowSourece.disk() = _Disk;
const factory RowSourece.remote(
DidFetchRowPB row,
) = _Remote;
}
@@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_service.dart';
import '../../../application/field/field_service.dart';
import '../../../application/row/row_data_controller.dart';
import '../../../application/row/row_controller.dart';
part 'row_detail_bloc.freezed.dart';
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
@@ -18,7 +18,7 @@ import 'package:flutter/material.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import '../../application/field/field_controller.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_data_controller.dart';
import '../../application/row/row_controller.dart';
import '../application/grid_bloc.dart';
import '../../application/database_controller.dart';
import 'grid_scroll.dart';
@@ -117,18 +117,25 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
height: 20,
child: Row(
children: [
FlowyText(state.filterInfo.fieldInfo.name),
const HSpace(4),
TextFilterConditionPBList(
filterInfo: state.filterInfo,
popoverMutex: popoverMutex,
onCondition: (condition) {
context
.read<TextFilterEditorBloc>()
.add(TextFilterEditorEvent.updateCondition(condition));
},
Expanded(
child: FlowyText(
state.filterInfo.fieldInfo.name,
overflow: TextOverflow.ellipsis,
),
),
const Spacer(),
const HSpace(4),
Expanded(
child: TextFilterConditionPBList(
filterInfo: state.filterInfo,
popoverMutex: popoverMutex,
onCondition: (condition) {
context
.read<TextFilterEditorBloc>()
.add(TextFilterEditorEvent.updateCondition(condition));
},
),
),
const HSpace(4),
DisclosureButton(
popoverMutex: popoverMutex,
onAction: (action) {
@@ -32,6 +32,7 @@ class ConditionButton extends StatelessWidget {
conditionName,
fontSize: 10,
color: AFThemeExtension.of(context).textColor,
overflow: TextOverflow.ellipsis,
),
margin: const EdgeInsets.symmetric(horizontal: 4),
radius: const BorderRadius.all(Radius.circular(2)),
@@ -233,10 +233,7 @@ class _DeleteFieldButton extends StatelessWidget {
LocaleKeys.grid_field_delete.tr(),
color: enable ? null : Theme.of(context).disabledColor,
),
leftIcon: svgWidget(
'grid/delete',
color: enable ? null : Theme.of(context).disabledColor,
),
leftIcon: const FlowySvg(name: 'grid/delete'),
onTap: () {
if (enable) onDeleted?.call();
},
@@ -267,7 +264,7 @@ class _HideFieldButton extends StatelessWidget {
text: FlowyText.medium(
LocaleKeys.grid_field_hide.tr(),
),
leftIcon: svgWidget('grid/hide'),
leftIcon: const FlowySvg(name: 'grid/hide'),
onTap: () => onHidden?.call(),
onHover: (_) => popoverMutex.close(),
);
@@ -42,7 +42,7 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
const TypeOptionSeparator(),
const OptionTitle(),
if (state.isEditingOption)
_CreateOptionTextField(popoverMutex: popoverMutex),
CreateOptionTextField(popoverMutex: popoverMutex),
if (state.options.isNotEmpty && state.isEditingOption)
const VSpace(10),
if (state.options.isEmpty && !state.isEditingOption)
@@ -266,18 +266,18 @@ class _AddOptionButton extends StatelessWidget {
}
}
class _CreateOptionTextField extends StatefulWidget {
class CreateOptionTextField extends StatefulWidget {
final PopoverMutex? popoverMutex;
const _CreateOptionTextField({
const CreateOptionTextField({
Key? key,
this.popoverMutex,
}) : super(key: key);
@override
State<_CreateOptionTextField> createState() => _CreateOptionTextFieldState();
State<CreateOptionTextField> createState() => _CreateOptionTextFieldState();
}
class _CreateOptionTextFieldState extends State<_CreateOptionTextField> {
class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
late final FocusNode _focusNode;
@override
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
@@ -65,7 +65,9 @@ class _GridRowState extends State<GridRow> {
child: _RowEnterRegion(
child: BlocBuilder<RowBloc, RowState>(
// The row need to rebuild when the cell count changes.
buildWhen: (p, c) => p.cellByFieldId.length != c.cellByFieldId.length,
buildWhen: (p, c) =>
p.cellByFieldId.length != c.cellByFieldId.length ||
p.rowSource != c.rowSource,
builder: (context, state) {
final content = Expanded(
child: RowContent(
@@ -78,6 +80,7 @@ class _GridRowState extends State<GridRow> {
);
return Row(
key: ValueKey(state.rowSource),
children: [
_RowLeading(
index: widget.index,
@@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -210,6 +211,9 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
@override
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
@override
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
@@ -29,7 +29,7 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
_rowCache = rowCache,
super(
RowCardState.initial(
_makeCells(groupFieldId, rowCache.loadGridCells(rowMeta)),
_makeCells(groupFieldId, rowCache.loadCells(rowMeta)),
isEditing,
),
) {
@@ -78,7 +78,7 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
Future<void> _startListening() async {
_rowCallback = _rowCache.addListener(
rowId: rowMeta.id,
onCellUpdated: (cellMap, reason) {
onRowChanged: (cellMap, reason) {
if (!isClosed) {
final cells = _makeCells(groupFieldId, cellMap);
add(RowCardEvent.didReceiveCells(cells, reason));
@@ -112,7 +112,7 @@ class RowCardEvent with _$RowCardEvent {
const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
const factory RowCardEvent.didReceiveCells(
List<DatabaseCellContext> cells,
RowsChangedReason reason,
ChangedReason reason,
) = _DidReceiveCells;
}
@@ -121,7 +121,7 @@ class RowCardState with _$RowCardState {
const factory RowCardState({
required List<DatabaseCellContext> cells,
required bool isEditing,
RowsChangedReason? changeReason,
ChangedReason? changeReason,
}) = _RowCardState;
factory RowCardState.initial(
@@ -14,7 +14,7 @@ import 'cells/url_card_cell.dart';
// T represents as the Generic card data
class CardCellBuilder<CustomCardData> {
final CellCache cellCache;
final CellMemCache cellCache;
final Map<FieldType, CardCellStyle>? styles;
CardCellBuilder(this.cellCache, {this.styles});
@@ -9,6 +9,7 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:reorderables/reorderables.dart';
import 'package:styled_widget/styled_widget.dart';
import '../../grid/presentation/layout/sizes.dart';
@@ -17,24 +18,19 @@ import '../../grid/presentation/widgets/header/field_editor.dart';
class DatabasePropertyList extends StatefulWidget {
final String viewId;
final FieldController fieldController;
const DatabasePropertyList({
super.key,
required this.viewId,
required this.fieldController,
Key? key,
}) : super(key: key);
});
@override
State<StatefulWidget> createState() => _DatabasePropertyListState();
}
class _DatabasePropertyListState extends State<DatabasePropertyList> {
late PopoverMutex _popoverMutex;
@override
void initState() {
_popoverMutex = PopoverMutex();
super.initState();
}
final PopoverMutex _popoverMutex = PopoverMutex();
@override
Widget build(BuildContext context) {
@@ -47,21 +43,29 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
builder: (context, state) {
final cells = state.fieldContexts.map((field) {
return _GridPropertyCell(
popoverMutex: _popoverMutex,
key: ValueKey(field.id),
viewId: widget.viewId,
fieldInfo: field,
key: ValueKey(field.id),
popoverMutex: _popoverMutex,
);
}).toList();
return ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) => cells[index],
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
return ReorderableColumn(
needsLongPressDraggable: false,
buildDraggableFeedback: (context, constraints, child) =>
ConstrainedBox(
constraints: constraints,
child: Material(color: Colors.transparent, child: child),
),
onReorder: (from, to) => context.read<DatabasePropertyBloc>().add(
DatabasePropertyEvent.moveField(
fieldId: cells[from].fieldInfo.id,
fromIndex: from,
toIndex: to,
),
),
padding: const EdgeInsets.symmetric(vertical: 6.0),
children: cells,
);
},
),
@@ -75,24 +79,18 @@ class _GridPropertyCell extends StatefulWidget {
final PopoverMutex popoverMutex;
const _GridPropertyCell({
required this.viewId,
super.key,
required this.fieldInfo,
required this.viewId,
required this.popoverMutex,
Key? key,
}) : super(key: key);
});
@override
State<_GridPropertyCell> createState() => _GridPropertyCellState();
}
class _GridPropertyCellState extends State<_GridPropertyCell> {
late PopoverController _popoverController;
@override
void initState() {
_popoverController = PopoverController();
super.initState();
}
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -22,23 +24,35 @@ class GridCellShortcuts extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
const GridCellCopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV):
const GridCellPasteIntent(),
},
shortcuts: shortcuts,
child: Actions(
actions: {
GridCellEnterIdent: GridCellEnterAction(child: child),
GridCellCopyIntent: GridCellCopyAction(child: child),
GridCellPasteIntent: GridCellPasteAction(child: child),
},
actions: actions,
child: child,
),
);
}
Map<ShortcutActivator, Intent> get shortcuts => {
if (shouldAddKeyboardKey(CellKeyboardKey.onEnter))
LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
if (shouldAddKeyboardKey(CellKeyboardKey.onCopy))
LogicalKeySet(
Platform.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control,
LogicalKeyboardKey.keyC,
): const GridCellCopyIntent(),
};
Map<Type, Action<Intent>> get actions => {
if (shouldAddKeyboardKey(CellKeyboardKey.onEnter))
GridCellEnterIdent: GridCellEnterAction(child: child),
if (shouldAddKeyboardKey(CellKeyboardKey.onCopy))
GridCellCopyIntent: GridCellCopyAction(child: child),
};
bool shouldAddKeyboardKey(CellKeyboardKey key) =>
child.shortcutHandlers.containsKey(key);
}
class GridCellEnterIdent extends Intent {
Loaded 100 of 758 files, more files were not shown because too many files have changed in this diff. Show more