Compare commits

...
18 Commits
Author SHA1 Message Date
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
432 changed files with 11291 additions and 2918 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
+17 -3
View File
@@ -1,10 +1,24 @@
# Release Notes
## Version 0.2.4 - 06/23/2023
## Version 0.2.6 - 07/11/2023
### Bug fixes:
### 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
- Unable to copy and paste a word
- Some UI issues
## Version 0.2.4 - 06/23/2023
+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.5"
CURRENT_APP_VERSION = "0.2.6"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
+1
View File
@@ -70,6 +70,7 @@ windows/flutter/dart_ffi/
**/.vscode/
*.env
*.env.*
coverage/
@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" style="width: 16px; height: 16px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="30" height="30" ><path d="M6.04883 27.3232C7.24219 27.3232 7.86426 26.4854 8.20703 25.3936L13.9834 6.79492H27.5039C28.2275 6.79492 28.7227 6.35059 28.7227 5.65234C28.7227 4.97949 28.2275 4.53516 27.5039 4.53516H13.958C12.6631 4.53516 12.0791 5.01758 11.7363 6.13477L6.18848 24.3525H5.97266L3.58594 15.9355C3.38281 15.2373 3.00195 14.9072 2.40527 14.9072C1.73242 14.9072 1.2373 15.3896 1.2373 16.0117C1.2373 16.2656 1.30078 16.4941 1.35156 16.6846L4.04297 25.5332C4.36035 26.5615 4.93164 27.3232 6.04883 27.3232ZM16.3955 24.7334C16.8652 24.7334 17.1064 24.5684 17.4619 24.0732L20.4707 19.8203H20.5215L23.5049 24.0732C23.873 24.5684 24.1143 24.7334 24.5713 24.7334C25.2061 24.7334 25.6758 24.3018 25.6758 23.7051C25.6758 23.4258 25.6123 23.1973 25.4219 22.9561L21.9307 18.208L25.4473 13.4346C25.6377 13.168 25.7139 12.9395 25.7139 12.6855C25.7139 12.127 25.2568 11.6953 24.6475 11.6953C24.2031 11.6953 23.9365 11.8477 23.6064 12.3174L20.7246 16.5957H20.6611L17.6904 12.3047C17.373 11.8477 17.0938 11.6953 16.624 11.6953C16.0146 11.6953 15.5068 12.165 15.5068 12.7363C15.5068 13.0537 15.583 13.2568 15.8115 13.5488L19.1504 18.1445L15.6084 23.0322C15.418 23.2861 15.3672 23.4639 15.3672 23.7559C15.3672 24.3018 15.8115 24.7334 16.3955 24.7334Z" fill="#2F3030"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -419,4 +419,4 @@
"layoutDateField": "تقويم التخطيط بواسطة"
}
}
}
}
@@ -149,5 +149,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "petit"
}
@@ -186,13 +186,29 @@
"open": "Open Settings"
},
"appearance": {
"fontFamily": {
"label": "Font Family",
"search": "Search"
},
"themeMode": {
"label": "Theme Mode",
"light": "Light Mode",
"dark": "Dark Mode",
"system": "Adapt to System"
},
"theme": "Theme"
"themeUpload": {
"button": "Upload",
"description": "Upload your own AppFlowy theme using the button below.",
"failure": "The theme that was uploaded had an invalid format.",
"loading": "Please wait while we validate and upload your theme...",
"uploadSuccess": "Your theme was uploaded successfully",
"deletionFailure": "Failed to delete the theme. Try to delete it manually.",
"filePickerDialogTitle": "Choose a .flowy_plugin file",
"urlUploadFailure": "Failed to open url: {}"
},
"theme": "Theme",
"builtInsLabel": "Built-in Themes",
"pluginsLabel": "Plugins"
},
"files": {
"copy": "Copy",
@@ -410,6 +426,7 @@
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings",
"discardResponse": "Do you want to discard the AI responses?",
"createInlineMathEquation": "Create equation",
"cover": {
"changeCover": "Change Cover",
"colors": "Colors",
@@ -227,5 +227,6 @@
"column": {
"create_new_card": "Nuevo"
}
}
},
"moreAction.small": "pequeño"
}
@@ -149,5 +149,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "petit"
}
@@ -235,5 +235,6 @@
"column": {
"create_new_card": "Nouveau"
}
}
},
"moreAction.small": "petit"
}
@@ -147,7 +147,9 @@
}
},
"sideBar": {
"openSidebar": "Oldalsáv megnyitása",
"closeSidebar": "Oldalsáv bezárása"
}
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
},
"moreAction.small": "kicsi"
}
@@ -222,5 +222,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "kecil"
}
@@ -155,5 +155,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "piccolo"
}
@@ -203,5 +203,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "小さい"
}
@@ -231,5 +231,6 @@
"column": {
"create_new_card": "추가"
}
}
},
"moreAction.small": "작은"
}
@@ -149,5 +149,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "mały"
}
@@ -145,5 +145,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "pequeno"
}
@@ -235,5 +235,6 @@
"column": {
"create_new_card": "Nytt"
}
}
},
"moreAction.small": "små"
}
@@ -149,5 +149,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "küçük"
}
@@ -458,6 +458,7 @@
"menuName": "看板",
"referencedBoardPrefix": "视图"
},
"moreAction.small": "小的",
"calendar": {
"menuName": "日历",
"defaultNewCalendarTitle": "未命名",
@@ -485,3 +486,4 @@
"github": "在 GitHub 查看"
}
}
@@ -9,22 +9,7 @@ import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
group('calendar database view', () {
testWidgets('update calendar layout', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -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,74 +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 {
testWidgets('rescheduling events', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -205,14 +138,31 @@ void main() {
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
// Create a new event on the first of this month
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month, 1);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissRowDetailPage();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
// Make sure that the event has been rescheduled to the new date
final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberOfEventsOnSpecificDay(1, sameDayNextWeek);
// Delete the event
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
await tester.tapRowDetailPageDeleteRowButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(today);
await tester.dismissRowDetailPage();
// Make sure that the event is today
tester.assertNumberofEventsOnSpecificDay(1, today);
tester.assertNumberOfEventsOnSpecificDay(1, today);
// Click on the event
await tester.openCalendarEvent(index: 0);
@@ -233,32 +183,7 @@ void main() {
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberofEventsOnSpecificDay(1, newDate);
});
testWidgets('reschedule an event by drag-and-drop', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event on the first of this month
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month, 1);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissRowDetailPage();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
// Make sure that the event has been rescheduled to the new date
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberofEventsOnSpecificDay(
1,
firstOfThisMonth.add(const Duration(days: 7)),
);
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();
@@ -10,21 +10,6 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid page', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('rename existing field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -5,27 +5,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
const location = 'import_files';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('add text filter', (tester) async {
await tester.openV020database();
@@ -14,21 +14,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('row details page opens', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -187,9 +172,10 @@ void main() {
// Focus on the editor
final textBlock = find.byType(TextBlockComponentWidget);
await tester.tapAt(tester.getCenter(textBlock));
await tester.pumpAndSettle();
// Input some text
const inputText = 'Hello world';
const inputText = 'Hello World';
await tester.ime.insertText(inputText);
expect(
find.textContaining(inputText, findRichText: true),
@@ -8,21 +8,6 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('create row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -9,21 +9,6 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('update layout', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -1,66 +1,17 @@
import 'dart:io';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
import 'package:path/path.dart' as p;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database', () {
const location = 'import_files';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('import v0.2.0 database data', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
await tester.tapAddButton();
await tester.tapImportButton();
final testFileNames = ['v020.afdb'];
final fileLocation = await tester.currentFileLocation();
for (final fileName in testFileNames) {
final str = await rootBundle.loadString(
p.join(
'assets/test/workspaces/database',
fileName,
),
);
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
}
// mock get files
await mockPickFilePaths(testFileNames, name: location);
await tester.tapDatabaseRawDataButton();
await tester.openPage('v020');
// check the import content
// await tester.assertCellContent(
// rowIndex: 7,
// fieldType: FieldType.RichText,
// // fieldName: 'Name',
// content: '',
// );
await tester.openV020database();
// wait the database data is loaded
await tester.pumpAndSettle(const Duration(microseconds: 500));
// check the text cell
final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];
@@ -3,27 +3,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
const location = 'import_files';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('add text sort', (tester) async {
await tester.openV020database();
// create a filter
@@ -11,21 +11,6 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('create linked view', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -9,22 +9,7 @@ import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
group('create and delete the document', () {
testWidgets('create a new document when launching app in first time',
(tester) async {
await tester.initializeAppFlowy();
@@ -0,0 +1,22 @@
import 'package:integration_test/integration_test.dart';
import 'document_create_and_delete_test.dart'
as document_create_and_delete_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
import 'document_with_database_test.dart' as document_with_database_test;
import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'edit_document_test.dart' as document_edit_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_create_and_delete_test.main();
document_edit_test.main();
document_with_database_test.main();
document_with_inline_page_test.main();
document_with_inline_math_equation_test.main();
document_with_cover_image_test.main();
}
@@ -9,21 +9,6 @@ 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('document cover tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -14,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();
@@ -0,0 +1,121 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/ime.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
group('inline math equation in document', () {
testWidgets('insert an inline math equation', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
const formula = 'E = MC ^ 2';
await tester.ime.insertText(formula);
await tester.editor.updateSelection(
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
);
// tap the inline math equation button
final inlineMathEquationButton = find.byTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block
final inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsOneWidget);
// tap it and update the content
await tester.tapButton(inlineMathEquation);
final textFormField = find.descendant(
of: find.byType(MathInputTextField),
matching: find.byType(TextFormField),
);
const newFormula = 'E = MC ^ 3';
await tester.enterText(textFormField, newFormula);
await tester.tapButton(
find.descendant(
of: find.byType(MathInputTextField),
matching: find.byType(FlowyButton),
),
);
await tester.pumpAndSettle();
});
testWidgets('remove the inline math equation format', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
const formula = 'E = MC ^ 2';
await tester.ime.insertText(formula);
await tester.editor.updateSelection(
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
);
// tap the inline math equation button
var inlineMathEquationButton = find.byTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block
var inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsOneWidget);
// highlight the math equation block
await tester.editor.updateSelection(
Selection.single(path: [0], startOffset: 0, endOffset: 1),
);
// expect to the see the inline math equation button is highlighted
inlineMathEquationButton = find.byWidgetPredicate(
(widget) =>
widget is IconItemWidget &&
widget.tooltip ==
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
expect(
tester.widget<IconItemWidget>(inlineMathEquationButton).isHighlight,
isTrue,
);
// cancel the format
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block is removed
inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsNothing);
tester.expectToSeeText(formula);
});
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -11,17 +11,6 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('inline page view in document', () {
const location = 'inline_page';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('insert a inline page - grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -13,17 +13,6 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('edit document', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('redo & undo', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -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(
@@ -12,17 +12,6 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('outline block test', () {
const location = 'outline_test';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('insert an outline block', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -1,23 +1,19 @@
import 'package:integration_test/integration_test.dart';
import 'switch_folder_test.dart' as switch_folder_test;
import 'document/document_test.dart' as document_test;
import 'document/cover_image_test.dart' as cover_image_test;
import 'share_markdown_test.dart' as share_markdown_test;
import 'import_files_test.dart' as import_files_test;
import 'document/document_with_database_test.dart'
as document_with_database_test;
import 'document/edit_document_test.dart' as edit_document_test;
import 'database_calendar_test.dart' as database_calendar_test;
import 'database_cell_test.dart' as database_cell_test;
import 'database_field_test.dart' as database_field_test;
import 'database_share_test.dart' as database_share_test;
import 'database_filter_test.dart' as database_filter_test;
import 'database_row_page_test.dart' as database_row_page_test;
import 'database_row_test.dart' as database_row_test;
import 'database_setting_test.dart' as database_setting_test;
import 'database_filter_test.dart' as database_filter_test;
import 'database_view_test.dart' as database_view_test;
import 'database_calendar_test.dart' as database_calendar_test;
import 'database_share_test.dart' as database_share_test;
import 'database_sort_test.dart' as database_sort_test;
import 'database_view_test.dart' as database_view_test;
import 'document/document_test_runner.dart' as document_test_runner;
import 'import_files_test.dart' as import_files_test;
import 'share_markdown_test.dart' as share_markdown_test;
import 'switch_folder_test.dart' as switch_folder_test;
/// The main task runner for all integration tests in AppFlowy.
///
@@ -33,10 +29,7 @@ void main() {
import_files_test.main();
// Document integration tests
cover_image_test.main();
document_test.main();
document_with_database_test.main();
edit_document_test.main();
document_test_runner.startTesting();
// Database integration tests
database_cell_test.main();
@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
@@ -11,30 +11,17 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('share markdown in document page', () {
const location = 'markdown';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('click the share button in document page', (tester) async {
await tester.initializeAppFlowy();
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
// mock the file picker
final path = await mockSaveFilePath(location, 'test.md');
final path = await mockSaveFilePath(
p.join(context.applicationDataDirectory, 'test.md'),
);
// click the share button and select markdown
await tester.tapShareButton();
await tester.tapMarkdownButton();
@@ -52,7 +39,7 @@ void main() {
testWidgets(
'share the markdown after renaming the document name',
(tester) async {
await tester.initializeAppFlowy();
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
@@ -65,8 +52,12 @@ void main() {
final shareButton = find.byType(ShareActionList);
final shareButtonState =
tester.state(shareButton) as ShareActionListState;
final path =
await mockSaveFilePath(location, '${shareButtonState.name}.md');
final path = await mockSaveFilePath(
p.join(
context.applicationDataDirectory,
'${shareButtonState.name}.md',
),
);
// click the share button and select markdown
await tester.tapShareButton();
@@ -1,48 +1,36 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
import 'package:path/path.dart' as p;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('customize the folder path', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('switch to B from A, then switch to A again', (tester) async {
final userA = uuid();
final userB = uuid();
const userA = 'UserA';
const userB = 'UserB';
await TestFolder.cleanTestLocation(userA);
await TestFolder.cleanTestLocation(userB);
await TestFolder.setTestLocation(p.join(userA, appFlowyDataFolder));
await tester.initializeAppFlowy();
final initialPath = p.join(userA, appFlowyDataFolder);
final context = await tester.initializeAppFlowy(
pathExtension: initialPath,
);
// remove the last extension
final rootPath = context.applicationDataDirectory.replaceFirst(
initialPath,
'',
);
await tester.tapGoButton();
tester.expectToSeeHomePage();
// switch to user B
{
// set user name to userA
// set user name for userA
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userA);
@@ -51,12 +39,14 @@ void main() {
await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(userB);
await mockGetDirectoryPath(
p.join(rootPath, userB),
);
await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
tester.expectToSeeHomePage();
// set user name to userB
// set user name for userB
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userB);
@@ -68,7 +58,9 @@ void main() {
await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(userA);
await mockGetDirectoryPath(
p.join(rootPath, userA),
);
await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
@@ -83,16 +75,15 @@ void main() {
await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(userB);
await mockGetDirectoryPath(
p.join(rootPath, userB),
);
await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
tester.expectToSeeHomePage();
tester.expectToSeeUserName(userB);
}
await TestFolder.cleanTestLocation(userA);
await TestFolder.cleanTestLocation(userB);
});
testWidgets('reset to default location', (tester) async {
@@ -109,8 +100,8 @@ void main() {
await tester.restoreLocation();
expect(
await TestFolder.defaultDevelopmentLocation(),
await TestFolder.currentLocation(),
await appFlowyApplicationDataDirectory().then((value) => value.path),
await getIt<ApplicationDataStorage>().getPath(),
);
});
});
@@ -1,88 +1,88 @@
import 'dart:io';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as p;
class TestFolder {
/// Location / Path
class FlowyTestContext {
FlowyTestContext({
required this.applicationDataDirectory,
});
/// Set a given AppFlowy data storage location under test environment.
///
/// To pass null means clear the location.
///
/// The file_picker is a system component and can't be tapped, so using logic instead of tapping.
///
static Future<void> setTestLocation(String? name) async {
final location = await testLocation(name);
SharedPreferences.setMockInitialValues({
KVKeys.pathLocation: location.path,
});
return;
}
/// Clean the location.
static Future<void> cleanTestLocation(String? name) async {
final dir = await testLocation(name);
await dir.delete(recursive: true);
return;
}
/// Get current using location.
static Future<String> currentLocation() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(KVKeys.pathLocation)!;
}
/// Get default location under development environment.
static Future<String> defaultDevelopmentLocation() async {
final dir = await appFlowyApplicationDataDirectory();
return dir.path;
}
/// Get default location under test environment.
static Future<Directory> testLocation(String? name) async {
final dir = await getApplicationDocumentsDirectory();
var path = '${dir.path}/flowy_test';
if (name != null) {
path += '/$name';
}
return Directory(path).create(recursive: true);
}
final String applicationDataDirectory;
}
extension AppFlowyTestBase on WidgetTester {
Future<void> initializeAppFlowy() async {
Future<FlowyTestContext> initializeAppFlowy({
// use to append after the application data directory
String? pathExtension,
Size windowsSize = const Size(1600, 1200),
}) async {
binding.setSurfaceSize(windowsSize);
mockHotKeyManagerHandlers();
final directory = await mockApplicationDataStorage(
pathExtension: pathExtension,
);
WidgetsFlutterBinding.ensureInitialized();
await FlowyRunner.run(
FlowyApp(),
IntegrationMode.integrationTest,
);
await wait(3000);
await pumpAndSettle(const Duration(seconds: 2));
return FlowyTestContext(
applicationDataDirectory: directory,
);
}
void mockHotKeyManagerHandlers() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
(MethodCall methodCall) async {
if (methodCall.method == 'unregisterAll') {
// do nothing
}
return;
});
}
WidgetsFlutterBinding.ensureInitialized();
await FlowyRunner.run(FlowyApp(), IntegrationMode.integrationTest);
Future<String> mockApplicationDataStorage({
// use to append after the application data directory
String? pathExtension,
}) async {
final dir = await getTemporaryDirectory();
await wait(3000);
await pumpAndSettle(const Duration(seconds: 2));
// Use a random uuid to avoid conflict.
String path = p.join(dir.path, 'appflowy_integration_test', uuid());
if (pathExtension != null && pathExtension.isNotEmpty) {
path = '$path/$pathExtension';
}
final directory = Directory(path);
if (!directory.existsSync()) {
await directory.create(recursive: true);
}
MockApplicationDataStorage.initialPath = directory.path;
return directory.path;
}
Future<void> tapButton(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
bool warnIfMissed = true,
bool warnIfMissed = false,
int milliseconds = 500,
}) async {
await tap(
@@ -127,6 +127,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);
@@ -73,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
@@ -83,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');
}
@@ -446,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 {
@@ -1024,7 +1038,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(findEvents, findsNWidgets(number));
}
void assertNumberofEventsOnSpecificDay(
void assertNumberOfEventsOnSpecificDay(
int number,
DateTime date, {
String? title,
@@ -1051,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 {
@@ -32,7 +32,7 @@ class EditorOperations {
/// Tap the line of editor at [index]
Future<void> tapLineOfEditorAt(int index) async {
final textBlocks = find.byType(FlowyRichText);
final textBlocks = find.byType(AppFlowyRichText);
index = index.clamp(0, textBlocks.evaluate().length - 1);
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
await tester.pumpAndSettle();
@@ -165,4 +165,14 @@ class EditorOperations {
);
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));
}
}
@@ -1,10 +1,5 @@
import 'dart:io';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:file_picker/file_picker.dart' as fp;
import 'package:path/path.dart' as p;
import '../util.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
class MockFilePicker implements FilePickerService {
MockFilePicker({
@@ -25,7 +20,7 @@ class MockFilePicker implements FilePickerService {
String? dialogTitle,
String? fileName,
String? initialDirectory,
fp.FileType type = fp.FileType.any,
FileType type = FileType.any,
List<String>? allowedExtensions,
bool lockParentWindow = false,
}) {
@@ -36,18 +31,17 @@ class MockFilePicker implements FilePickerService {
Future<FilePickerResult?> pickFiles({
String? dialogTitle,
String? initialDirectory,
fp.FileType type = fp.FileType.any,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(fp.FilePickerStatus p1)? onFileLoading,
Function(FilePickerStatus p1)? onFileLoading,
bool allowCompression = true,
bool allowMultiple = false,
bool withData = false,
bool withReadStream = false,
bool lockParentWindow = false,
}) {
final platformFiles = mockPaths
.map((e) => fp.PlatformFile(path: e, name: '', size: 0))
.toList();
final platformFiles =
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
return Future.value(
FilePickerResult(
platformFiles,
@@ -56,20 +50,21 @@ class MockFilePicker implements FilePickerService {
}
}
Future<void> mockGetDirectoryPath(String? name) async {
final dir = await TestFolder.testLocation(name);
Future<void> mockGetDirectoryPath(
String path,
) async {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPath: dir.path,
mockPath: path,
),
);
return;
}
Future<String> mockSaveFilePath(String? name, String fileName) async {
final dir = await TestFolder.testLocation(name);
final path = p.join(dir.path, fileName);
Future<String> mockSaveFilePath(
String path,
) async {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
@@ -79,18 +74,16 @@ Future<String> mockSaveFilePath(String? name, String fileName) async {
return path;
}
Future<List<String>> mockPickFilePaths(
List<String> fileNames, {
String? name,
String? customPath,
Future<List<String>> mockPickFilePaths({
required List<String> paths,
}) async {
late final Directory dir;
if (customPath != null) {
dir = Directory(customPath);
} else {
dir = await TestFolder.testLocation(name);
}
final paths = fileNames.map((e) => p.join(dir.path, e)).toList();
// late final Directory dir;
// if (customPath != null) {
// dir = Directory(customPath);
// } else {
// dir = await TestFolder.testLocation(applicationDataPath, name);
// }
// final paths = fileNames.map((e) => p.join(dir.path, e)).toList();
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
@@ -7,32 +7,24 @@ class Config {
required String anonKey,
required String key,
required String secret,
required String pgUrl,
required String pgUser,
required String pgPassword,
required String pgPort,
}) async {
final postgresConfig = PostgresConfigurationPB.create()
..url = pgUrl
..userName = pgUser
..password = pgPassword
..port = int.parse(pgPort);
await ConfigEventSetSupabaseConfig(
SupabaseConfigPB.create()
..supabaseUrl = url
..key = key
..anonKey = anonKey
..jwtSecret = secret,
..jwtSecret = secret
..postgresConfig = postgresConfig,
).send();
}
static Future<void> setSupabaseCollabPluginConfig({
required String url,
required String key,
required String jwtSecret,
required String collabTable,
}) async {
final payload = CollabPluginConfigPB.create();
final collabTableConfig = CollabTableConfigPB.create()
..tableName = collabTable;
payload.supabaseConfig = SupabaseDBConfigPB.create()
..supabaseUrl = url
..key = key
..jwtSecret = jwtSecret
..collabTableConfig = collabTableConfig;
await ConfigEventSetCollabPluginConfig(payload).send();
}
}
@@ -18,4 +18,9 @@ class KVKeys {
/// The value is a json string with the following format:
/// {'height': 600.0, 'width': 800.0}
static const String windowSize = 'windowSize';
static const String kDocumentAppearanceFontSize =
'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily =
'kDocumentAppearanceFontFamily';
}
+39 -3
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,39 @@ abstract class Env {
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_COLLAB_TABLE',
varName: 'SUPABASE_DB',
defaultValue: '',
)
static final String supabaseCollabTable = _Env.supabaseCollabTable;
static final String supabaseDb = _Env.supabaseDb;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_DB_USER',
defaultValue: '',
)
static final String supabaseDbUser = _Env.supabaseDbUser;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_DB_PASSWORD',
defaultValue: '',
)
static final String supabaseDbPassword = _Env.supabaseDbPassword;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_DB_PORT',
defaultValue: '5432',
)
static final String supabaseDbPort = _Env.supabaseDbPort;
}
bool get isSupabaseEnable =>
Env.supabaseUrl.isNotEmpty &&
Env.supabaseAnonKey.isNotEmpty &&
Env.supabaseKey.isNotEmpty &&
Env.supabaseJwtSecret.isNotEmpty;
Env.supabaseJwtSecret.isNotEmpty &&
Env.supabaseDb.isNotEmpty &&
Env.supabaseDbUser.isNotEmpty &&
Env.supabaseDbPassword.isNotEmpty &&
Env.supabaseDbPort.isNotEmpty;
@@ -3,7 +3,6 @@ import 'package:appflowy/plugins/database_view/grid/application/row/row_document
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
@@ -86,7 +85,6 @@ class _RowEditorState extends State<RowEditor> {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => DocumentAppearanceCubit()),
BlocProvider.value(value: documentBloc),
],
child: BlocBuilder<DocumentBloc, DocumentState>(
@@ -39,8 +39,6 @@ class DocumentPluginBuilder extends PluginBuilder {
class DocumentPlugin extends Plugin<int> {
late PluginType _pluginType;
final DocumentAppearanceCubit _documentAppearanceCubit =
DocumentAppearanceCubit();
@override
final ViewPluginNotifier notifier;
@@ -52,20 +50,12 @@ class DocumentPlugin extends Plugin<int> {
Key? key,
}) : notifier = ViewPluginNotifier(view: view) {
_pluginType = pluginType;
_documentAppearanceCubit.fetch();
}
@override
void dispose() {
_documentAppearanceCubit.close();
super.dispose();
}
@override
PluginWidgetBuilder get widgetBuilder {
return DocumentPluginWidgetBuilder(
notifier: notifier,
documentAppearanceCubit: _documentAppearanceCubit,
);
}
@@ -81,11 +71,9 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
final ViewPluginNotifier notifier;
ViewPB get view => notifier.view;
int? deletedViewIndex;
DocumentAppearanceCubit documentAppearanceCubit;
DocumentPluginWidgetBuilder({
required this.notifier,
required this.documentAppearanceCubit,
Key? key,
});
@@ -102,17 +90,14 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
});
});
return BlocProvider.value(
value: documentAppearanceCubit,
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
builder: (_, state) {
return DocumentPage(
view: view,
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
key: ValueKey(view.id),
);
},
),
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
builder: (_, state) {
return DocumentPage(
view: view,
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
key: ValueKey(view.id),
);
},
);
}
@@ -128,10 +113,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
view: view,
),
const SizedBox(width: 10),
BlocProvider.value(
value: documentAppearanceCubit,
child: const DocumentMoreButton(),
),
const DocumentMoreButton(),
],
);
}
@@ -10,12 +10,12 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/base64_string.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
hide DocumentEvent;
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
@@ -49,9 +49,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
quoteItem,
bulletedListItem,
numberedListItem,
inlineMathEquationItem,
linkItem,
textColorItem,
highlightColorItem,
buildTextColorItem(),
buildHighlightColorItem(),
];
late final List<SelectionMenuItem> slashMenuItems;
@@ -125,7 +126,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
final (bool autoFocus, Selection? selection) =
_computeAutoFocusParameters();
final editor = AppFlowyEditor.custom(
final editor = AppFlowyEditor(
editorState: widget.editorState,
editable: true,
shrinkWrap: widget.shrinkWrap,
@@ -1,7 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
@@ -46,7 +46,7 @@ void showLinkToPageMenu(
linkToPageMenuEntry.remove();
} on FlowyError catch (e) {
Dialogs.show(
FlowyErrorPage.message(
child: FlowyErrorPage.message(
e.msg,
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
@@ -105,7 +105,7 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget {
class _CalloutBlockComponentWidgetState
extends State<CalloutBlockComponentWidget>
with SelectableMixin, DefaultSelectable, BlockComponentConfigurable {
with SelectableMixin, DefaultSelectableMixin, BlockComponentConfigurable {
// the key used to forward focus to the richtext child
@override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@@ -192,7 +192,7 @@ class _CalloutBlockComponentWidgetState
Widget buildCalloutBlockComponent(BuildContext context) {
return Padding(
padding: padding,
child: FlowyRichText(
child: AppFlowyRichText(
key: forwardKey,
node: widget.node,
editorState: editorState,
@@ -96,7 +96,7 @@ class CodeBlockComponentWidget extends BlockComponentStatefulWidget {
}
class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
with SelectableMixin, DefaultSelectable, BlockComponentConfigurable {
with SelectableMixin, DefaultSelectableMixin, BlockComponentConfigurable {
// the key used to forward focus to the richtext child
@override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@@ -216,7 +216,7 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
final codeTextSpans = _convert(codeNodes);
return Padding(
padding: widget.padding,
child: FlowyRichText(
child: AppFlowyRichText(
key: forwardKey,
node: widget.node,
editorState: editorState,
@@ -1,12 +1,11 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart' as fp;
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
@@ -121,7 +120,7 @@ class CoverImagePickerBloc
final result = await getIt<FilePickerService>().pickFiles(
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
allowMultiple: false,
type: fp.FileType.image,
type: FileType.image,
allowedExtensions: allowedExtensions,
);
if (result != null && result.files.isNotEmpty) {
@@ -2,7 +2,7 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
@@ -43,20 +43,6 @@ enum CoverType {
}
}
class DocumentHeaderNodeWidgetBuilder implements NodeWidgetBuilder {
@override
Widget build(NodeWidgetContext<Node> context) {
return DocumentHeaderNodeWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (_) => true;
}
class DocumentHeaderNodeWidget extends StatefulWidget {
const DocumentHeaderNodeWidget({
required this.node,
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:flowy_infra/uuid.dart';
@@ -0,0 +1,173 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text_input.dart';
import 'package:flutter/material.dart';
import 'package:flutter_math_fork/flutter_math.dart';
import 'package:provider/provider.dart';
class InlineMathEquationKeys {
const InlineMathEquationKeys._();
static const formula = 'formula';
}
class InlineMathEquation extends StatefulWidget {
const InlineMathEquation({
super.key,
required this.formula,
required this.node,
required this.index,
this.textStyle,
});
final Node node;
final int index;
final String formula;
final TextStyle? textStyle;
@override
State<InlineMathEquation> createState() => _InlineMathEquationState();
}
class _InlineMathEquationState extends State<InlineMathEquation> {
final popoverController = PopoverController();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return _IgnoreParentPointer(
child: AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (_) {
return MathInputTextField(
initialText: widget.formula,
onSubmit: (value) async {
popoverController.close();
if (value == widget.formula) {
return;
}
final editorState = context.read<EditorState>();
final transaction = editorState.transaction
..formatText(widget.node, widget.index, 1, {
InlineMathEquationKeys.formula: value,
});
await editorState.apply(transaction);
},
);
},
offset: const Offset(0, 10),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(2),
Math.tex(
widget.formula,
options: MathOptions(
style: MathStyle.text,
mathFontOptions: const FontOptions(
fontShape: FontStyle.italic,
),
fontSize: 14.0,
color: widget.textStyle?.color ??
theme.colorScheme.onBackground,
),
),
const HSpace(2),
],
),
),
),
),
);
}
}
class MathInputTextField extends StatefulWidget {
const MathInputTextField({
super.key,
required this.initialText,
required this.onSubmit,
});
final String initialText;
final void Function(String value) onSubmit;
@override
State<MathInputTextField> createState() => _MathInputTextFieldState();
}
class _MathInputTextFieldState extends State<MathInputTextField> {
late final TextEditingController textEditingController;
@override
void initState() {
super.initState();
textEditingController = TextEditingController(
text: widget.initialText,
);
textEditingController.selection = TextSelection(
baseOffset: 0,
extentOffset: widget.initialText.length,
);
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: 240,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: FlowyFormTextInput(
autoFocus: true,
textAlign: TextAlign.left,
controller: textEditingController,
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 4.0,
),
onEditingComplete: () =>
widget.onSubmit(textEditingController.text),
),
),
const HSpace(4.0),
FlowyButton(
text: FlowyText(LocaleKeys.button_Done.tr()),
useIntrinsicWidth: true,
onTap: () => widget.onSubmit(textEditingController.text),
),
],
),
);
}
}
class _IgnoreParentPointer extends StatelessWidget {
const _IgnoreParentPointer({
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {},
onTapDown: (_) {},
onDoubleTap: () {},
onLongPress: () {},
child: child,
);
}
}
@@ -0,0 +1,74 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flutter/material.dart';
final ToolbarItem inlineMathEquationItem = ToolbarItem(
id: 'editor.inline_math_equation',
group: 2,
isActive: onlyShowInSingleSelectionAndTextType,
builder: (context, editorState) {
final selection = editorState.selection!;
final nodes = editorState.getNodesInSelection(selection);
final isHighlight = nodes.allSatisfyInSelection(selection, (delta) {
return delta.everyAttributes(
(attributes) => attributes[InlineMathEquationKeys.formula] != null,
);
});
return IconItemWidget(
iconBuilder: (_) => svgWidget(
'editor/math',
size: const Size.square(16),
color: isHighlight ? Colors.lightBlue : Colors.white,
),
isHighlight: isHighlight,
tooltip: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
onPressed: () async {
final selection = editorState.selection;
if (selection == null || selection.isCollapsed) {
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
final delta = node?.delta;
if (node == null || delta == null) {
return;
}
final transaction = editorState.transaction;
if (isHighlight) {
final formula = delta
.slice(selection.startIndex, selection.endIndex)
.whereType<TextInsert>()
.firstOrNull
?.attributes?[InlineMathEquationKeys.formula];
assert(formula != null);
if (formula == null) {
return;
}
// clear the format
transaction.replaceText(
node,
selection.startIndex,
selection.length,
formula,
attributes: {},
);
} else {
final text = editorState.getTextInSelection(selection).join();
transaction.replaceText(
node,
selection.startIndex,
selection.length,
'\$',
attributes: {
InlineMathEquationKeys.formula: text,
},
);
}
await editorState.apply(transaction);
},
);
},
);
@@ -24,8 +24,6 @@ class MentionBlockKeys {
static const mention = 'mention';
static const type = 'type'; // MentionType, String
static const pageId = 'page_id';
static const pageType = 'page_type';
static const pageName = 'page_name';
}
class InlinePageReferenceService {
@@ -32,7 +32,7 @@ class MentionPageBlock extends StatefulWidget {
class _MentionPageBlockState extends State<MentionPageBlock> {
late final EditorState editorState;
late final Future<ViewPB?> viewPBFuture;
late Future<ViewPB?> viewPBFuture;
ViewListener? viewListener;
@override
@@ -45,6 +45,7 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
..start(
onViewUpdated: (p0) {
pageMemorizer[p0.id] = p0;
viewPBFuture = fetchView(widget.pageId);
editorState.reload();
},
);
@@ -111,11 +112,10 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
}
Future<ViewPB?> fetchView(String pageId) async {
final views = await ViewBackendService().fetchViews((_, __) => true);
final flattenViews = views.expand((e) => [e.$1, ...e.$2]).toList();
final view = flattenViews.firstWhereOrNull(
(element) => element.id == pageId,
final view = await ViewBackendService.getView(pageId).then(
(value) => value.swap().toOption().toNullable(),
);
if (view == null) {
// try to fetch from trash
final trashViews = await TrashService().readTrash();
@@ -129,6 +129,7 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
..name = trash.name;
}
}
return view;
}
@@ -141,13 +141,13 @@ class EditorMigration {
}
const backgroundColor = 'backgroundColor';
if (attributes.containsKey(backgroundColor)) {
attributes[FlowyRichTextKeys.highlightColor] =
attributes[AppFlowyRichTextKeys.highlightColor] =
attributes[backgroundColor];
attributes.remove(backgroundColor);
}
const color = 'color';
if (attributes.containsKey(color)) {
attributes[FlowyRichTextKeys.textColor] = attributes[color];
attributes[AppFlowyRichTextKeys.textColor] = attributes[color];
attributes.remove(color);
}
return attributes;
@@ -157,7 +157,7 @@ class _SmartEditBlockComponentWidgetState
var width = double.infinity;
final editorSize = editorState.renderBox?.size;
final padding = editorState.editorStyle.padding;
if (editorSize != null && padding != null) {
if (editorSize != null) {
width = editorSize.width - padding.left - padding.right;
}
return width;
@@ -1,22 +1,24 @@
export 'actions/block_action_list.dart';
export 'actions/option_action.dart';
export 'callout/callout_block_component.dart';
export 'code_block/code_block_component.dart';
export 'code_block/code_block_shortcut_event.dart';
export 'header/cover_editor_bloc.dart';
export 'header/document_header_node_widget.dart';
export 'header/custom_cover_picker.dart';
export 'emoji_picker/emoji_menu_item.dart';
export 'extensions/flowy_tint_extension.dart';
export 'database/database_view_block_component.dart';
export 'database/inline_database_menu_item.dart';
export 'database/referenced_database_menu_item.dart';
export 'database/database_view_block_component.dart';
export 'emoji_picker/emoji_menu_item.dart';
export 'extensions/flowy_tint_extension.dart';
export 'header/cover_editor_bloc.dart';
export 'header/custom_cover_picker.dart';
export 'header/document_header_node_widget.dart';
export 'image/image_menu.dart';
export 'image/image_selection_menu.dart';
export 'inline_math_equation/inline_math_equation.dart';
export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
export 'math_equation/math_equation_block_component.dart';
export 'openai/widgets/auto_completion_node_widget.dart';
export 'openai/widgets/smart_edit_node_widget.dart';
export 'openai/widgets/smart_edit_toolbar_item.dart';
export 'outline/outline_block_component.dart';
export 'toggle/toggle_block_component.dart';
export 'toggle/toggle_block_shortcut_event.dart';
export 'outline/outline_block_component.dart';
export 'image/image_menu.dart';
export 'image/image_selection_menu.dart';
export 'actions/option_action.dart';
export 'actions/block_action_list.dart';
@@ -84,7 +84,7 @@ class _ToggleListBlockComponentWidgetState
extends State<ToggleListBlockComponentWidget>
with
SelectableMixin,
DefaultSelectable,
DefaultSelectableMixin,
BlockComponentConfigurable,
BackgroundColorMixin {
// the key used to forward focus to the richtext child
@@ -141,7 +141,7 @@ class _ToggleListBlockComponentWidgetState
width: 4.0,
),
Expanded(
child: FlowyRichText(
child: AppFlowyRichText(
key: forwardKey,
node: widget.node,
editorState: editorState,
@@ -1,7 +1,8 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg, Log;
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -28,30 +29,35 @@ class EditorStyleCustomizer {
EditorStyle desktop() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
return EditorStyle.desktop(
padding: padding,
backgroundColor: theme.colorScheme.surface,
cursorColor: theme.colorScheme.primary,
textStyleConfiguration: TextStyleConfiguration(
text: TextStyle(
fontFamily: 'Poppins',
text: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
color: theme.colorScheme.onBackground,
height: 1.5,
),
bold: const TextStyle(
fontFamily: 'Poppins-Bold',
bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith(
fontWeight: FontWeight.w600,
),
italic: const TextStyle(fontStyle: FontStyle.italic),
underline: const TextStyle(decoration: TextDecoration.underline),
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
href: TextStyle(
italic: baseTextStyle(fontFamily).copyWith(
fontStyle: FontStyle.italic,
),
underline: baseTextStyle(fontFamily).copyWith(
decoration: TextDecoration.underline,
),
strikethrough: baseTextStyle(fontFamily).copyWith(
decoration: TextDecoration.lineThrough,
),
href: baseTextStyle(fontFamily).copyWith(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
code: GoogleFonts.robotoMono(
textStyle: TextStyle(
textStyle: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
fontWeight: FontWeight.normal,
color: Colors.red,
@@ -66,30 +72,32 @@ class EditorStyleCustomizer {
EditorStyle mobile() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
return EditorStyle.desktop(
padding: padding,
backgroundColor: theme.colorScheme.surface,
cursorColor: theme.colorScheme.primary,
textStyleConfiguration: TextStyleConfiguration(
text: TextStyle(
fontFamily: 'poppins',
text: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
color: theme.colorScheme.onBackground,
height: 1.5,
),
bold: const TextStyle(
fontFamily: 'poppins-Bold',
bold: baseTextStyle(fontFamily).copyWith(
fontWeight: FontWeight.w600,
),
italic: const TextStyle(fontStyle: FontStyle.italic),
underline: const TextStyle(decoration: TextDecoration.underline),
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
href: TextStyle(
italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic),
underline: baseTextStyle(fontFamily)
.copyWith(decoration: TextDecoration.underline),
strikethrough: baseTextStyle(fontFamily)
.copyWith(decoration: TextDecoration.lineThrough),
href: baseTextStyle(fontFamily).copyWith(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
code: GoogleFonts.robotoMono(
textStyle: TextStyle(
textStyle: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
fontWeight: FontWeight.normal,
color: Colors.red,
@@ -119,8 +127,8 @@ class EditorStyleCustomizer {
TextStyle codeBlockStyleBuilder() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return TextStyle(
fontFamily: 'poppins',
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
return baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
height: 1.5,
color: theme.colorScheme.onBackground,
@@ -157,14 +165,33 @@ class EditorStyleCustomizer {
);
}
TextStyle baseTextStyle(
String fontFamily, {
FontWeight? fontWeight,
}) {
try {
return GoogleFonts.getFont(
fontFamily,
fontWeight: fontWeight,
);
} on Exception {
return GoogleFonts.getFont('Poppins');
}
}
InlineSpan customizeAttributeDecorator(
TextInsert textInsert,
BuildContext context,
Node node,
int index,
TextInsert text,
TextSpan textSpan,
) {
final attributes = textInsert.attributes;
final attributes = text.attributes;
if (attributes == null) {
return textSpan;
}
// customize the inline mention block, like inline page
final mention = attributes[MentionBlockKeys.mention] as Map?;
if (mention != null) {
final type = mention[MentionBlockKeys.type];
@@ -178,6 +205,21 @@ class EditorStyleCustomizer {
);
}
}
// customize the inline math equation block
final formula = attributes[InlineMathEquationKeys.formula] as String?;
if (formula != null) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: InlineMathEquation(
node: node,
index: index,
formula: formula,
textStyle: style().textStyleConfiguration.text,
),
);
}
return textSpan;
}
}
@@ -1,30 +1,53 @@
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:bloc/bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
const String _kDocumentAppearanceFontSize = 'kDocumentAppearanceFontSize';
class DocumentAppearance {
const DocumentAppearance({
required this.fontSize,
required this.fontFamily,
});
final double fontSize;
// Will be supported...
// final String fontName;
final String fontFamily;
DocumentAppearance copyWith({double? fontSize}) {
DocumentAppearance copyWith({
double? fontSize,
String? fontFamily,
}) {
return DocumentAppearance(
fontSize: fontSize ?? this.fontSize,
fontFamily: fontFamily ?? this.fontFamily,
);
}
}
class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 16.0));
DocumentAppearanceCubit()
: super(const DocumentAppearance(fontSize: 16.0, fontFamily: 'Poppins'));
void fetch() async {
Future<void> fetch() async {
final prefs = await SharedPreferences.getInstance();
final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 16.0;
final fontSize =
prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0;
final fontFamily =
prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ?? 'Poppins';
if (isClosed) {
return;
}
emit(
state.copyWith(
fontSize: fontSize,
fontFamily: fontFamily,
),
);
}
Future<void> syncFontSize(double fontSize) async {
final prefs = await SharedPreferences.getInstance();
prefs.setDouble(KVKeys.kDocumentAppearanceFontSize, fontSize);
if (isClosed) {
return;
@@ -37,9 +60,9 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
);
}
void syncFontSize(double fontSize) async {
Future<void> syncFontFamily(String fontFamily) async {
final prefs = await SharedPreferences.getInstance();
prefs.setDouble(_kDocumentAppearanceFontSize, fontSize);
prefs.setString(KVKeys.kDocumentAppearanceFontFamily, fontFamily);
if (isClosed) {
return;
@@ -47,7 +70,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
emit(
state.copyWith(
fontSize: fontSize,
fontFamily: fontFamily,
),
);
}
@@ -1,13 +1,13 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/plugins/document/application/share_bloc.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -6,14 +6,14 @@ import 'package:appflowy/plugins/database_view/application/field/field_service.d
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/util/file_picker/file_picker_impl.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy/workspace/application/workspace/prelude.dart';
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
@@ -32,26 +32,35 @@ import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
class DependencyResolver {
static Future<void> resolve(GetIt getIt) async {
static Future<void> resolve(
GetIt getIt,
IntegrationMode mode,
) async {
_resolveUserDeps(getIt);
_resolveHomeDeps(getIt);
_resolveFolderDeps(getIt);
_resolveDocDeps(getIt);
_resolveGridDeps(getIt);
_resolveCommonService(getIt);
_resolveCommonService(getIt, mode);
}
}
void _resolveCommonService(GetIt getIt) async {
void _resolveCommonService(
GetIt getIt,
IntegrationMode mode,
) async {
// getIt.registerFactory<KeyValueStorage>(() => RustKeyValue());
getIt.registerFactory<KeyValueStorage>(() => DartKeyValue());
getIt.registerFactory<FilePickerService>(() => FilePicker());
getIt.registerFactory<ApplicationDataStorage>(() => ApplicationDataStorage());
if (mode.isTest) {
getIt.registerFactory<ApplicationDataStorage>(
() => MockApplicationDataStorage(),
);
} else {
getIt.registerFactory<ApplicationDataStorage>(
() => ApplicationDataStorage(),
);
}
getIt.registerFactoryAsync<OpenAIRepository>(
() async {
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -18,8 +17,14 @@ abstract class EntryPoint {
Widget create(LaunchConfiguration config);
}
class FlowyRunnerContext {
final Directory applicationDataDirectory;
FlowyRunnerContext({required this.applicationDataDirectory});
}
class FlowyRunner {
static Future<void> run(
static Future<FlowyRunnerContext> run(
EntryPoint f,
IntegrationMode mode, {
LaunchConfiguration config = const LaunchConfiguration(
@@ -32,44 +37,42 @@ class FlowyRunner {
// Specify the env
initGetIt(getIt, mode, f, config);
final directory = await getIt<ApplicationDataStorage>()
.getPath()
.then((value) => Directory(value));
// final directory = await appFlowyDocumentDirectory();
final applicationDataDirectory =
await getIt<ApplicationDataStorage>().getPath().then(
(value) => Directory(value),
);
// add task
final launcher = getIt<AppLauncher>();
launcher.addTasks(
[
// handle platform errors.
const PlatformErrorCatcherTask(),
// this task should be first task, for handling platform errors.
// don't catch errors in test mode
if (!mode.isUnitTest) const PlatformErrorCatcherTask(),
// localization
const InitLocalizationTask(),
// init the app window
const InitAppWindowTask(),
// Init Rust SDK
InitRustSDKTask(directory: directory),
InitRustSDKTask(directory: applicationDataDirectory),
// Load Plugins, like document, grid ...
const PluginLoadTask(),
// init the app widget
// ignore in test mode
if (!mode.isTest()) ...[
if (!mode.isUnitTest) ...[
const HotKeyTask(),
InitSupabaseTask(
url: Env.supabaseUrl,
anonKey: Env.supabaseAnonKey,
key: Env.supabaseKey,
jwtSecret: Env.supabaseJwtSecret,
collabTable: Env.supabaseCollabTable,
),
InitSupabaseTask(),
const InitAppWidgetTask(),
const InitPlatformServiceTask()
],
],
);
await launcher.launch(); // execute the tasks
return FlowyRunnerContext(
applicationDataDirectory: applicationDataDirectory,
);
}
}
@@ -94,7 +97,7 @@ Future<void> initGetIt(
);
getIt.registerSingleton<PluginSandbox>(PluginSandbox());
await DependencyResolver.resolve(getIt);
await DependencyResolver.resolve(getIt, env);
}
class LaunchContext {
@@ -145,23 +148,24 @@ class AppLauncher {
enum IntegrationMode {
develop,
release,
test,
integrationTest,
}
unitTest,
integrationTest;
extension IntegrationEnvExt on IntegrationMode {
bool isTest() {
return this == IntegrationMode.test;
}
// test mode
bool get isTest => isUnitTest || isIntegrationTest;
bool get isUnitTest => this == IntegrationMode.unitTest;
bool get isIntegrationTest => this == IntegrationMode.integrationTest;
bool isIntegrationTest() {
return this == IntegrationMode.integrationTest;
}
// release mode
bool get isRelease => this == IntegrationMode.release;
// develop mode
bool get isDevelop => this == IntegrationMode.develop;
}
IntegrationMode integrationEnv() {
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return IntegrationMode.test;
return IntegrationMode.unitTest;
}
if (kReleaseMode) {
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -79,8 +80,13 @@ class ApplicationWidget extends StatelessWidget {
final cubit = AppearanceSettingsCubit(appearanceSetting)
..readLocaleWhenAppLaunch(context);
return BlocProvider(
create: (context) => cubit,
return MultiBlocProvider(
providers: [
BlocProvider.value(value: cubit),
BlocProvider<DocumentAppearanceCubit>(
create: (_) => DocumentAppearanceCubit()..fetch(),
),
],
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) => MaterialApp(
builder: overlayManagerBuilder(),
@@ -8,5 +8,6 @@ class InitLocalizationTask extends LaunchTask {
@override
Future<void> initialize(LaunchContext context) async {
await EasyLocalization.ensureInitialized();
EasyLocalization.logger.enableBuildModes = [];
}
}
@@ -23,31 +23,29 @@ class InitRustSDKTask extends LaunchTask {
Future<void> initialize(LaunchContext context) async {
final dir = directory ?? await appFlowyApplicationDataDirectory();
context.getIt<FlowySDK>().setEnv(getAppFlowyEnv());
final env = getAppFlowyEnv();
context.getIt<FlowySDK>().setEnv(env);
await context.getIt<FlowySDK>().init(dir);
}
}
AppFlowyEnv getAppFlowyEnv() {
final postgresConfig = PostgresConfiguration(
url: Env.supabaseDb,
password: Env.supabaseDbPassword,
port: int.parse(Env.supabaseDbPort),
user_name: Env.supabaseDbUser,
);
final supabaseConfig = SupabaseConfiguration(
url: Env.supabaseUrl,
key: Env.supabaseKey,
jwt_secret: Env.supabaseJwtSecret,
);
final collabTableConfig =
CollabTableConfig(enable: true, table_name: Env.supabaseCollabTable);
final supbaseDBConfig = SupabaseDBConfig(
url: Env.supabaseUrl,
key: Env.supabaseKey,
jwt_secret: Env.supabaseJwtSecret,
collab_table_config: collabTableConfig,
postgres_config: postgresConfig,
);
return AppFlowyEnv(
supabase_config: supabaseConfig,
supabase_db_config: supbaseDBConfig,
);
}
@@ -62,7 +60,7 @@ Future<Directory> appFlowyApplicationDataDirectory() async {
case IntegrationMode.release:
final Directory documentsDir = await getApplicationSupportDirectory();
return Directory(path.join(documentsDir.path, 'data')).create();
case IntegrationMode.test:
case IntegrationMode.unitTest:
case IntegrationMode.integrationTest:
return Directory(path.join(Directory.current.path, '.sandbox'));
}
@@ -1,49 +1,37 @@
import 'package:appflowy/core/config/config.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/env/env.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../startup.dart';
bool isSupabaseEnable = false;
bool isSupabaseInitialized = false;
class InitSupabaseTask extends LaunchTask {
const InitSupabaseTask({
required this.url,
required this.anonKey,
required this.key,
required this.jwtSecret,
this.collabTable = "",
});
final String url;
final String anonKey;
final String key;
final String jwtSecret;
final String collabTable;
@override
Future<void> initialize(LaunchContext context) async {
if (url.isEmpty || anonKey.isEmpty || jwtSecret.isEmpty || key.isEmpty) {
isSupabaseEnable = false;
Log.info('Supabase config is empty, skip init supabase.');
if (!isSupabaseEnable) {
return;
}
if (isSupabaseInitialized) {
return;
}
await Supabase.initialize(
url: url,
anonKey: anonKey,
url: Env.supabaseUrl,
anonKey: Env.supabaseAnonKey,
debug: false,
);
await Config.setSupabaseConfig(
url: url,
key: key,
secret: jwtSecret,
anonKey: anonKey,
url: Env.supabaseUrl,
key: Env.supabaseKey,
secret: Env.supabaseJwtSecret,
anonKey: Env.supabaseAnonKey,
pgPassword: Env.supabaseDbPassword,
pgPort: Env.supabaseDbPort,
pgUrl: Env.supabaseDb,
pgUser: Env.supabaseDbUser,
);
isSupabaseEnable = true;
isSupabaseInitialized = true;
}
}
@@ -19,17 +19,14 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
@override
Future<void> initialize(LaunchContext context) async {
// Don't initialize on mobile or web.
if (!defaultTargetPlatform.isDesktop) {
if (!defaultTargetPlatform.isDesktop || context.env.isIntegrationTest) {
return;
}
await windowManager.ensureInitialized();
windowManager.addListener(this);
Size windowSize = await WindowSizeManager().getSize();
if (context.env.isIntegrationTest()) {
windowSize = const Size(1600, 1200);
}
final windowSize = await WindowSizeManager().getSize();
final windowOptions = WindowOptions(
size: windowSize,
@@ -14,6 +14,6 @@ class AuthError {
..code = -10003;
static final supabaseGetUserError = FlowyError()
..msg = 'supabase sign in with oauth error'
..code = -10003;
..msg = 'unable to get user from supabase'
..code = -10004;
}
@@ -1,11 +1,9 @@
import 'dart:async';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/user/application/auth/appflowy_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -112,13 +110,12 @@ class SupabaseAuthService implements AuthService {
final completer = Completer<Either<FlowyError, UserProfilePB>>();
late final StreamSubscription<AuthState> subscription;
subscription = _auth.onAuthStateChange.listen((event) async {
if (event.event != AuthChangeEvent.signedIn) {
final user = event.session?.user;
if (event.event != AuthChangeEvent.signedIn || user == null) {
completer.complete(left(AuthError.supabaseSignInWithOauthError));
} else {
final user = await getSupabaseUser();
final Either<FlowyError, UserProfilePB> response = await user.fold(
(l) => left(l),
(r) async => await setupAuth(map: {AuthServiceMapKeys.uuid: r.id}),
final Either<FlowyError, UserProfilePB> response = await setupAuth(
map: {AuthServiceMapKeys.uuid: user.id},
);
completer.complete(response);
}
@@ -164,16 +161,21 @@ class SupabaseAuthService implements AuthService {
return _appFlowyAuthService.signUpAsGuest();
}
// @override
// Future<Either<FlowyError, UserProfilePB>> getUser() async {
// final loginType = await getIt<KeyValueStorage>()
// .get(KVKeys.loginType)
// .then((value) => value.toOption().toNullable());
// if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) {
// return _appFlowyAuthService.getUser();
// }
// final user = await getSupabaseUser();
// return user.map((r) => r.toUserProfile());
// }
@override
Future<Either<FlowyError, UserProfilePB>> getUser() async {
final loginType = await getIt<KeyValueStorage>()
.get(KVKeys.loginType)
.then((value) => value.toOption().toNullable());
if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) {
return _appFlowyAuthService.getUser();
}
final user = await getSupabaseUser();
return user.map((r) => r.toUserProfile());
return UserBackendService.getCurrentUserProfile();
}
Future<Either<FlowyError, User>> getSupabaseUser() async {
@@ -197,14 +199,6 @@ class SupabaseAuthService implements AuthService {
}
}
extension on User {
UserProfilePB toUserProfile() {
return UserProfilePB()
..email = email ?? ''
..token = this.id;
}
}
extension on String {
Provider toProvider() {
switch (this) {
@@ -1,7 +1,8 @@
import 'dart:io';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -12,7 +13,6 @@ import 'package:google_fonts/google_fonts.dart';
import '../../../generated/locale_keys.g.dart';
import '../../../startup/startup.dart';
import '../../../workspace/application/settings/settings_location_cubit.dart';
import '../../../workspace/presentation/home/toast.dart';
enum _FolderPage {
@@ -1,4 +1,4 @@
import 'package:appflowy/startup/tasks/supabase_task.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
@@ -96,7 +96,7 @@ class SplashScreen extends StatelessWidget {
}
Future<void> _registerIfNeeded() async {
final result = await UserEventCheckUser().send();
final result = await UserEventGetUserProfile().send();
if (!result.isLeft()) {
await getIt<AuthService>().signUpAsGuest();
}
@@ -10,6 +10,7 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:google_fonts/google_fonts.dart';
part 'appearance.freezed.dart';
@@ -36,10 +37,10 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
/// Update selected theme in the user's settings and emit an updated state
/// with the AppTheme named [themeName].
void setTheme(String themeName) {
Future<void> setTheme(String themeName) async {
_setting.theme = themeName;
_saveAppearanceSettings();
emit(state.copyWith(appTheme: AppTheme.fromName(themeName)));
emit(state.copyWith(appTheme: await AppTheme.fromName(themeName)));
}
/// Update the theme mode in the user's settings and emit an updated state.
@@ -49,11 +50,27 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
emit(state.copyWith(themeMode: themeMode));
}
/// Toggle the theme mode
void toggleThemeMode() {
final currentThemeMode = state.themeMode;
setThemeMode(
currentThemeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light,
);
}
/// Update selected font in the user's settings and emit an updated state
/// with the font name.
void setFontFamily(String fontFamilyName) {
_setting.font = fontFamilyName;
_saveAppearanceSettings();
emit(state.copyWith(font: fontFamilyName));
}
/// Updates the current locale and notify the listeners the locale was
/// changed. Fallback to [en] locale if [newLocale] is not supported.
void setLocale(BuildContext context, Locale newLocale) {
if (!context.supportedLocales.contains(newLocale)) {
Log.warn("Unsupported locale: $newLocale, Fallback to locale: en");
// Log.warn("Unsupported locale: $newLocale, Fallback to locale: en");
newLocale = const Locale('en');
}
@@ -182,7 +199,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
double menuOffset,
) {
return AppearanceSettingsState(
appTheme: AppTheme.fromName(themeName),
appTheme: AppTheme.fallback,
font: font,
monospaceFont: monospaceFont,
themeMode: _themeModeFromPB(themeModePB),
@@ -341,14 +358,24 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
}
TextStyle _getFontStyle({
String? fontFamily,
required String fontFamily,
double? fontSize,
FontWeight? fontWeight,
Color? fontColor,
double? letterSpacing,
double? lineHeight,
}) =>
TextStyle(
}) {
try {
return GoogleFonts.getFont(
fontFamily,
fontSize: fontSize ?? FontSizes.s12,
color: fontColor,
fontWeight: fontWeight ?? FontWeight.w500,
letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
height: lineHeight,
);
} catch (e) {
return TextStyle(
fontFamily: fontFamily,
fontSize: fontSize ?? FontSizes.s12,
color: fontColor,
@@ -357,6 +384,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
height: lineHeight,
);
}
}
TextTheme _getTextTheme({
required String fontFamily,
@@ -0,0 +1,106 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import '../../../startup/tasks/prelude.dart';
const appFlowyDataFolder = "AppFlowyDataDoNotRename";
class ApplicationDataStorage {
ApplicationDataStorage();
String? _cachePath;
/// Set the custom path to store the data.
/// If the path is not exists, the path will be created.
/// If the path is invalid, the path will be set to the default path.
Future<void> setCustomPath(String path) async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
Log.info('LocalFileStorage is not supported on this platform.');
return;
}
if (Platform.isMacOS) {
// remove the prefix `/Volumes/*`
path = path.replaceFirst(RegExp(r'^/Volumes/[^/]+'), '');
} else if (Platform.isWindows) {
path = path.replaceAll('/', '\\');
}
// If the path is not ends with `AppFlowyData`, we will append the
// `AppFlowyData` to the path. If the path is ends with `AppFlowyData`,
// which means the path is the custom path.
if (p.basename(path) != appFlowyDataFolder) {
path = p.join(path, appFlowyDataFolder);
}
// create the directory if not exists.
final directory = Directory(path);
if (!directory.existsSync()) {
await directory.create(recursive: true);
}
setPath(path);
}
Future<void> setPath(String path) async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
Log.info('LocalFileStorage is not supported on this platform.');
return;
}
await getIt<KeyValueStorage>().set(KVKeys.pathLocation, path);
// clear the cache path, and not set the cache path to the new path because the set path may be invalid
_cachePath = null;
}
Future<String> getPath() async {
if (_cachePath != null) {
return _cachePath!;
}
final response = await getIt<KeyValueStorage>().get(KVKeys.pathLocation);
String path = await response.fold(
(error) async {
// return the default path if the path is not set
final directory = await appFlowyApplicationDataDirectory();
return directory.path;
},
(path) => path,
);
_cachePath = path;
// if the path is not exists means the path is invalid, so we should clear the kv store
if (!Directory(path).existsSync()) {
await getIt<KeyValueStorage>().clear();
final directory = await appFlowyApplicationDataDirectory();
path = directory.path;
}
return path;
}
}
class MockApplicationDataStorage extends ApplicationDataStorage {
MockApplicationDataStorage();
// this value will be clear after setup
// only for the initial step
@visibleForTesting
static String? initialPath;
@override
Future<String> getPath() async {
final path = initialPath;
if (path != null) {
initialPath = null;
await super.setPath(path);
return Future.value(path);
}
return super.getPath();
}
}
@@ -1 +1,2 @@
export 'settings_dialog_bloc.dart';
export 'application_data_storage.dart';
@@ -1,13 +1,8 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as p;
import '../../../startup/tasks/prelude.dart';
@@ -27,7 +22,7 @@ class SettingsLocationCubit extends Cubit<SettingsLocationState> {
Future<void> resetDataStoragePathToApplicationDefault() async {
final directory = await appFlowyApplicationDataDirectory();
await getIt<ApplicationDataStorage>()._setPath(directory.path);
await getIt<ApplicationDataStorage>().setPath(directory.path);
emit(SettingsLocationState.didReceivedPath(directory.path));
}
@@ -41,79 +36,3 @@ class SettingsLocationCubit extends Cubit<SettingsLocationState> {
emit(SettingsLocationState.didReceivedPath(path));
}
}
const appFlowyDataFolder = "AppFlowyDataDoNotRename";
class ApplicationDataStorage {
ApplicationDataStorage();
String? _cachePath;
/// Set the custom path to store the data.
/// If the path is not exists, the path will be created.
/// If the path is invalid, the path will be set to the default path.
Future<void> setCustomPath(String path) async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
Log.info('LocalFileStorage is not supported on this platform.');
return;
}
if (Platform.isMacOS) {
// remove the prefix `/Volumes/*`
path = path.replaceFirst(RegExp(r'^/Volumes/[^/]+'), '');
} else if (Platform.isWindows) {
path = path.replaceAll('/', '\\');
}
// If the path is not ends with `AppFlowyData`, we will append the
// `AppFlowyData` to the path. If the path is ends with `AppFlowyData`,
// which means the path is the custom path.
if (p.basename(path) != appFlowyDataFolder) {
path = p.join(path, appFlowyDataFolder);
}
// create the directory if not exists.
final directory = Directory(path);
if (!directory.existsSync()) {
await directory.create(recursive: true);
}
_setPath(path);
}
Future<void> _setPath(String path) async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
Log.info('LocalFileStorage is not supported on this platform.');
return;
}
await getIt<KeyValueStorage>().set(KVKeys.pathLocation, path);
// clear the cache path, and not set the cache path to the new path because the set path may be invalid
_cachePath = null;
}
Future<String> getPath() async {
if (_cachePath != null) {
return _cachePath!;
}
final response = await getIt<KeyValueStorage>().get(KVKeys.pathLocation);
String path = await response.fold(
(error) async {
// return the default path if the path is not set
final directory = await appFlowyApplicationDataDirectory();
return directory.path;
},
(path) => path,
);
_cachePath = path;
// if the path is not exists means the path is invalid, so we should clear the kv store
if (!Directory(path).existsSync()) {
await getIt<KeyValueStorage>().clear();
final directory = await appFlowyApplicationDataDirectory();
path = directory.path;
}
return path;
}
}
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
@@ -25,6 +26,21 @@ class HomeHotKeys extends StatelessWidget {
.add(const HomeSettingEvent.collapseMenu());
},
);
final HotKey hotKeyForToggleThemeMode = HotKey(
KeyCode.keyL,
modifiers: [
Platform.isMacOS ? KeyModifier.meta : KeyModifier.control,
KeyModifier.shift,
],
scope: HotKeyScope.inapp,
);
hotKeyManager.register(
hotKeyForToggleThemeMode,
keyDownHandler: (_) {
context.read<AppearanceSettingsCubit>().toggleThemeMode();
},
);
return child;
}
}
@@ -5,12 +5,11 @@ import 'dart:typed_data';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/share/import_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_type.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/container.dart';
import 'package:flutter/material.dart';
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
@@ -103,7 +104,10 @@ class MenuUser extends StatelessWidget {
showDialog(
context: context,
builder: (context) {
return SettingsDialog(userProfile);
return BlocProvider<DocumentAppearanceCubit>.value(
value: BlocProvider.of<DocumentAppearanceCubit>(context),
child: SettingsDialog(userProfile),
);
},
);
},
Loaded 100 of 432 files, more files were not shown because too many files have changed in this diff. Show more