Compare commits

..
39 Commits
Author SHA1 Message Date
Lucas.Xu 764b4db166 chore: bump version 0.2.9 (#3144) 2023-08-10 21:18:31 +08:00
Lucas.Xu 7701533ed2 chore: remove the unused value in supabase_task (#3157) 2023-08-10 21:06:02 +08:00
ferreira-fp 29c89013f0 chore: update PT-BR translations (#3156) 2023-08-10 20:59:53 +08:00
Yijing Huang 1fad5aa804 feat: callout improvement (#3090) 2023-08-10 20:59:24 +08:00
Johan Sutrisno 675e23e971 fix: scroll to heading is too slow (#2952) (#3045) 2023-08-10 18:36:53 +08:00
Lucas.Xu c9ee82ec75 chore: update editor version to 1.2.3 (#3153) 2023-08-10 18:35:27 +08:00
Nathan.fooo 0c9935ccf2 fix: using freeze to refactor the FieldInfo class (#3152) 2023-08-10 15:46:16 +08:00
Lucas.Xu d28846bbaa chore: update German translations 2023-08-10 09:18:18 +07:00
Richard Shiue 58ecf62240 feat: selectOption sort + fix logic in edge cases (#3109) 2023-08-09 12:43:03 +08:00
Nathan.fooo a5eaf15548 refactor: login session (#3149) 2023-08-09 10:13:49 +08:00
fbiere 4589d86d81 inlang: update translations 2023-08-08 18:18:46 +02:00
Richard Shiue 9330df4ce1 fix: overlapped scrollbar in calendar (#3075)
* fix: height for calendar view in document

* fix: hide overlapped scrollbars

* chore: merge remote branch 'upstream/main' into fix-#2618
2023-08-08 16:58:04 +08:00
Richard Shiue 7cdd47757b chore: database tab bar ui improvements (#3093)
* chore: tab bar ui polish

* chore: toolbar buttons ui

* chore: minor padding radius and spacing adjustment

* fix: padding issues

* chore: add view button size and padding

* chore: the carelessness is real

* fix: dark mode colors

* fix: selected icon color
2023-08-08 16:57:25 +08:00
Alex Wallen 9d18e9de40 fix: use dart build script on Windows (#3141) 2023-08-08 16:08:34 +08:00
Lucas.Xu e2dd179001 Merge pull request #3140 from LucasXu0/url_protocol_in_windows_installer 2023-08-08 12:19:56 +07:00
Mathias Mogensen 35a47bfe5d feat: tabs shortcuts (#3112) 2023-08-08 13:09:17 +08:00
Lucas.Xu 9dc209bda7 fix: loading status widget on the signup page flickers. 2023-08-08 11:24:06 +08:00
Lucas.Xu 104851d330 chore: optimize the logout button 2023-08-08 10:58:14 +08:00
Lucas.Xu a1db5bff5c fix: user page inside setting overflow 2023-08-08 10:46:52 +08:00
Lucas.Xu 453346e69c fix: the minHeight of the personal header is too small 2023-08-08 10:32:08 +08:00
Kilu.He 923285bfcf feat: support mention a page (#3117) 2023-08-08 10:07:59 +08:00
Lucas.Xu 0de8b92db6 fix: deeplink doesn't work in windows exe installer 2023-08-08 10:06:29 +08:00
Nathan.fooo 125bcbc9db fix: rebuild application when setting the theme or font (#3139) 2023-08-08 09:08:24 +08:00
Nathan.fooo ef9596f4ab chore: enable single instance desktop application on windows (#3136) 2023-08-07 23:30:14 +08:00
Nathan.fooo 3c04b72932 fix: push to sign in screen when logout (#3127)
* fix: push to sign in screen when logout

* chore: show historical login users

* chore: open historical user

* chore: show historical user

* chore: reload app widget with unique key

* chore: add tooltip for user history
2023-08-07 22:24:04 +08:00
StarAtt a3bea472bf chore: update zh-TW translations (#3131) 2023-08-07 20:59:36 +08:00
Dan Zheng 5ac7b8e165 fix: automatically wrap the date, number or URL cells. 2023-08-07 17:58:24 +08:00
Lucas.Xu 95a938657b feat: support windows supabase deeplink callback (#3133) 2023-08-07 17:54:50 +08:00
Richard Shiue fbb1753350 fix: euro currency number format (#3115) 2023-08-07 17:49:03 +08:00
Lucas.Xu c6401da0b5 feat: support customizing the supabase auth path (#3130) 2023-08-07 14:31:20 +08:00
Lucas.Xu f724185f1c chore: Support CI build universal macOS package (#3124)
* fix: macos release build

* feat: support build arm64 macOS package on CI

* feat: support build arm64 macOS package on CI
2023-08-06 16:06:43 +08:00
Nathan.fooo 63a12a1d2b test: add subscription test (#3126) 2023-08-06 15:54:18 +08:00
Mihir 890f7573f3 fix: favorite selection and context menu doesn't work (#3114) 2023-08-06 12:31:47 +08:00
Nathan.fooo 6f159e741b refactor: remove shared instance KV (#3123)
* refactor: remove shared instance KV

* test: enable document test
2023-08-06 11:51:03 +08:00
Nathan.fooo 9a72f31d60 feat: show loading indicator when loading data from remote (#3120)
* chore: show circle indicator if fetch the data from remote

* chore: fix the lb warnings

* chore: create sdk-build for macOS
2023-08-05 15:02:05 +08:00
Mayur MahajanandMathias Mogensen ab7acbd5de chore: german translation for new property and shortcuts (#3116)
* chore: translation for new property nd shortcuts

* Update frontend/resources/translations/de-DE.json

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2023-08-04 20:46:09 +02:00
Lucas.Xu b7dfc85208 fix: macos release build (#3118) 2023-08-04 21:22:28 +08:00
Kilu.HeandLucas.Xu 16a01e11ed feat: support updating the view name and icon through document (#3099)
* feat: support updating the view name and icon through document

* fix: store expand pages

* fix: refactor text link

* fix: update cargo.toml

* fix: update test

* fix: update event map

* fix: move deal with icon codes to a single file

* fix: delete useless code from flutter

* fix: document banner

* fix: build error

* fix: update rust library

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-08-04 19:27:14 +08:00
Lucas.Xu 70914e6228 chore: support building universal macos package (#3110) 2023-08-03 20:02:44 +08:00
334 changed files with 5841 additions and 3777 deletions

No files matched your search

+99 -11
View File
@@ -20,14 +20,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
# - name: Create .env file
# working-directory: frontend/appflowy_flutter
# run: |
# touch .env
# echo SUPABASE_URL=${{ secrets.HOST_URL }} >> .env
# echo SUPABASE_ANON_KEY=${{ secrets.HOST_ANON_KEY }} >> .env
# echo SUPABASE_JWT_SECRET=${{ secrets.HOST_JWT_SECRET }} >> .env
- name: Build release notes
run: |
touch ${{ env.RELEASE_NOTES_PATH }}
@@ -92,7 +84,7 @@ jobs:
working-directory: frontend
run: |
flutter config --enable-windows-desktop
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-windows-x86 appflowy
dart ./scripts/flutter_release_build/build_flowy.dart . ${{ github.ref_name }}
- name: Archive Asset
uses: vimtor/action-zip@v1
@@ -132,7 +124,7 @@ jobs:
asset_name: ${{ env.WINDOWS_INSTALLER_NAME }}.exe
asset_content_type: application/octet-stream
build-for-macOS:
build-for-macOS-x86_64:
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
runs-on: ${{ matrix.job.os }}
needs: create-release
@@ -220,6 +212,96 @@ jobs:
asset_name: ${{ env.MACOS_DMG_NAME }}.dmg
asset_content_type: application/octet-stream
build-for-macOS-universal:
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
runs-on: ${{ matrix.job.os }}
needs: create-release
env:
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
MACOS_AARCH64_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-universal.zip
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-universal
strategy:
fail-fast: false
matrix:
job:
- {
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-11,
extra-build-args: "",
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: ${{ matrix.job.targets }}
components: rustfmt
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites
working-directory: frontend
run: |
cargo install --force cargo-make
cargo install --force duckscript_cli
- name: Build AppFlowy
working-directory: frontend
run: |
flutter config --enable-macos-desktop
sh scripts/flutter_release_build/build_universal_package_for_macos.sh
- name: Create macOS dmg
run: |
brew install create-dmg
create-dmg \
--volname ${{ env.MACOS_DMG_NAME }} \
--hide-extension "AppFlowy.app" \
--background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \
--window-size 600 450 \
--icon-size 94 \
--icon "AppFlowy.app" 141 249 \
--app-drop-link 458 249 \
"${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \
"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app"
- name: Archive Asset
working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}
run: zip --symlinks -qr ${{ env.MACOS_AARCH64_ZIP_NAME }} AppFlowy.app
- name: Upload Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_AARCH64_ZIP_NAME }}
asset_name: ${{ env.MACOS_AARCH64_ZIP_NAME }}
asset_content_type: application/octet-stream
- name: Upload DMG Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg
asset_name: ${{ env.MACOS_DMG_NAME }}.dmg
asset_content_type: application/octet-stream
build-for-linux:
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
runs-on: ${{ matrix.job.os }}
@@ -368,7 +450,13 @@ jobs:
notify-discord:
runs-on: ubuntu-latest
needs: [build-for-linux, build-for-windows, build-for-macOS]
needs:
[
build-for-linux,
build-for-windows,
build-for-macOS-x86_64,
build-for-macOS-universal,
]
steps:
- name: Notify Discord
run: |
+13
View File
@@ -1,5 +1,18 @@
# Release Notes
## Version 0.2.9 - 08/08/2023
### New Features
- Improve tab and shortcut, click with alt/option to open a page in new tab.
- Improve database tab bar UI.
### Bug fixes
- Add button and more action button of the favorite section doesn't work.
- Fix euro currency number format.
- Some UI issues
## Version 0.2.8 - 08/03/2023
### New Features
+13 -1
View File
@@ -24,7 +24,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.8"
CURRENT_APP_VERSION = "0.2.9"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@@ -60,6 +60,7 @@ BUILD_FLAG = "debug"
FLUTTER_OUTPUT_DIR = "Debug"
PRODUCT_EXT = "app"
BUILD_ARCHS = "arm64"
BUILD_ACTIVE_ARCHS_ONLY = true
CRATE_TYPE = "staticlib"
[env.development-mac-x86_64]
@@ -70,6 +71,7 @@ BUILD_FLAG = "debug"
FLUTTER_OUTPUT_DIR = "Debug"
PRODUCT_EXT = "app"
BUILD_ARCHS = "x86_64"
BUILD_ACTIVE_ARCHS_ONLY = true
CRATE_TYPE = "staticlib"
[env.production-mac-arm64]
@@ -80,6 +82,7 @@ FLUTTER_OUTPUT_DIR = "Release"
PRODUCT_EXT = "app"
APP_ENVIRONMENT = "production"
BUILD_ARCHS = "arm64"
BUILD_ACTIVE_ARCHS_ONLY = false
CRATE_TYPE = "staticlib"
[env.production-mac-x86_64]
@@ -90,8 +93,17 @@ FLUTTER_OUTPUT_DIR = "Release"
PRODUCT_EXT = "app"
APP_ENVIRONMENT = "production"
BUILD_ARCHS = "x86_64"
BUILD_ACTIVE_ARCHS_ONLY = false
CRATE_TYPE = "staticlib"
[env.production-mac-universal]
BUILD_FLAG = "release"
TARGET_OS = "macos"
FLUTTER_OUTPUT_DIR = "Release"
PRODUCT_EXT = "app"
BUILD_ACTIVE_ARCHS_ONLY = false
APP_ENVIRONMENT = "production"
[env.development-windows-x86]
TARGET_OS = "windows"
RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc"
@@ -50,7 +50,7 @@ void main() {
await tester.tapFirstDayOfWeekStartFromMonday();
// Open the other page and open the new calendar page again
await tester.openPage(gettingStated);
await tester.openPage(gettingStarted);
await tester.pumpAndSettle(const Duration(milliseconds: 300));
await tester.openPage(name, layout: ViewLayoutPB.Calendar);
@@ -0,0 +1,47 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('copy and paste in document', () {
testWidgets('paste multiple lines at the first line', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName();
// mock the clipboard
const lines = 3;
AppFlowyClipboard.mockSetData(
AppFlowyClipboardData(
text: List.generate(lines, (index) => 'line $index').join('\n'),
),
);
// paste the text
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
expect(editorState.document.root.children.length, 4);
for (var i = 0; i < lines; i++) {
expect(
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
'line $i',
);
}
});
});
}
@@ -34,20 +34,20 @@ void main() {
// delete the readme page
await tester.hoverOnPageName(
gettingStated,
gettingStarted,
onHover: () async => await tester.tapDeletePageButton(),
);
// the banner should show up and the readme page should be gone
tester.expectToSeeDocumentBanner();
tester.expectNotToSeePageName(gettingStated);
tester.expectNotToSeePageName(gettingStarted);
// restore the readme page
await tester.tapRestoreButton();
// the banner should be gone and the readme page should be back
tester.expectNotToSeeDocumentBanner();
tester.expectToSeePageName(gettingStated);
tester.expectToSeePageName(gettingStarted);
});
testWidgets('delete the readme page and delete it permanently',
@@ -58,20 +58,20 @@ void main() {
// delete the readme page
await tester.hoverOnPageName(
gettingStated,
gettingStarted,
onHover: () async => await tester.tapDeletePageButton(),
);
// the banner should show up and the readme page should be gone
tester.expectToSeeDocumentBanner();
tester.expectNotToSeePageName(gettingStated);
tester.expectNotToSeePageName(gettingStarted);
// delete the page permanently
await tester.tapDeletePermanentlyButton();
// the banner should be gone and the readme page should be gone
tester.expectNotToSeeDocumentBanner();
tester.expectNotToSeePageName(gettingStated);
tester.expectNotToSeePageName(gettingStarted);
});
});
}
@@ -10,6 +10,7 @@ import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -23,4 +24,5 @@ void startTesting() {
document_with_cover_image_test.main();
document_with_outline_block.main();
document_with_toggle_list_test.main();
document_copy_and_paste_test.main();
}
@@ -59,7 +59,7 @@ void main() {
);
// switch to other page and switch back
await tester.openPage(gettingStated);
await tester.openPage(gettingStarted);
await tester.openPage(pageName);
// the numbered list should be kept
@@ -91,7 +91,7 @@ void main() {
}
// switch to other page and switch back
await tester.openPage(gettingStated);
await tester.openPage(gettingStarted);
await tester.openPage(pageName);
// this screenshots are different on different platform, so comment it out temporarily.
@@ -17,7 +17,7 @@ void main() {
await tester.tapGoButton();
// expect to see a getting started page
tester.expectToSeePageName(gettingStated);
tester.expectToSeePageName(gettingStarted);
await tester.tapAddViewButton();
await tester.tapImportButton();
@@ -16,6 +16,7 @@ import 'share_markdown_test.dart' as share_markdown_test;
import 'switch_folder_test.dart' as switch_folder_test;
import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'board/board_test_runner.dart' as board_test_runner;
import 'tabs_test.dart' as tabs_test;
/// The main task runner for all integration tests in AppFlowy.
///
@@ -51,6 +52,9 @@ void main() {
database_view_test.main();
database_calendar_test.main();
// Tabs
tabs_test.main();
// board_test.main();
// empty_document_test.main();
// smart_menu_test.main();
@@ -16,7 +16,7 @@ void main() {
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(gettingStated);
tester.expectToSeePageName(gettingStarted);
// mock the file picker
final path = await mockSaveFilePath(
@@ -43,11 +43,11 @@ void main() {
await tester.tapGoButton();
// expect to see a getting started page
tester.expectToSeePageName(gettingStated);
tester.expectToSeePageName(gettingStarted);
// rename the document
await tester.hoverOnPageName(
gettingStated,
gettingStarted,
onHover: () async {
await tester.renamePage('example');
},
@@ -94,6 +94,13 @@ const expectedMarkdown = r'''
1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts)
1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown)
1. Type `/code` to insert a code block
```rust
// This is the main function.
fn main() {
// Print text to the console.
println!("Hello World!");
}
```
## Have a question❓
> Click `?` at the bottom right for help and support.
@@ -2,6 +2,8 @@ import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -28,7 +30,7 @@ void main() {
2,
].map((e) => 'document_$e').toList();
for (var i = 0; i < names.length; i++) {
final parentName = i == 0 ? gettingStated : names[i - 1];
final parentName = i == 0 ? gettingStarted : names[i - 1];
await tester.createNewPageWithName(
name: names[i],
parentName: parentName,
@@ -42,9 +44,9 @@ void main() {
);
}
await tester.favoriteViewByName(gettingStated);
await tester.favoriteViewByName(gettingStarted);
expect(
tester.findFavoritePageName(gettingStated),
tester.findFavoritePageName(gettingStarted),
findsOneWidget,
);
@@ -54,9 +56,9 @@ void main() {
findsNWidgets(2),
);
await tester.unfavoriteViewByName(gettingStated);
await tester.unfavoriteViewByName(gettingStarted);
expect(
tester.findFavoritePageName(gettingStated),
tester.findFavoritePageName(gettingStarted),
findsNothing,
);
expect(
@@ -82,9 +84,9 @@ void main() {
await tester.tapGoButton();
const name = 'test';
await tester.favoriteViewByName(gettingStated);
await tester.favoriteViewByName(gettingStarted);
await tester.hoverOnPageName(
gettingStated,
gettingStarted,
layout: ViewLayoutPB.Document,
onHover: () async {
await tester.renamePage(name);
@@ -110,7 +112,7 @@ void main() {
final names = [1, 2].map((e) => 'document_$e').toList();
for (var i = 0; i < names.length; i++) {
final parentName = i == 0 ? gettingStated : names[i - 1];
final parentName = i == 0 ? gettingStarted : names[i - 1];
await tester.createNewPageWithName(
name: names[i],
parentName: parentName,
@@ -118,7 +120,7 @@ void main() {
);
tester.expectToSeePageName(names[i], parentName: parentName);
}
await tester.favoriteViewByName(gettingStated);
await tester.favoriteViewByName(gettingStarted);
await tester.favoriteViewByName(names[0]);
await tester.favoriteViewByName(names[1]);
@@ -152,7 +154,7 @@ void main() {
);
await tester.hoverOnPageName(
gettingStated,
gettingStarted,
layout: ViewLayoutPB.Document,
onHover: () async {
await tester.tapDeletePageButton();
@@ -171,5 +173,50 @@ void main() {
);
},
);
testWidgets(
'view selection is synced between favorites and personal folder',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName();
await tester.favoriteViewByName(gettingStarted);
expect(
find.byWidgetPredicate(
(widget) =>
widget is FlowyHover &&
widget.isSelected != null &&
widget.isSelected!(),
),
findsNWidgets(2),
);
},
);
testWidgets(
'context menu opens up for favorites',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName();
await tester.favoriteViewByName(gettingStarted);
await tester.hoverOnPageName(
gettingStarted,
layout: ViewLayoutPB.Document,
useLast: false,
onHover: () async {
await tester.tapPageOptionButton();
await tester.pumpAndSettle();
expect(
find.byType(PopoverContainer),
findsOneWidget,
);
},
);
await tester.pumpAndSettle();
},
);
});
}
@@ -70,7 +70,7 @@ void main() {
break;
}
await tester.openPage(gettingStated);
await tester.openPage(gettingStarted);
}
});
@@ -80,7 +80,7 @@ void main() {
final names = [1, 2, 3, 4].map((e) => 'document_$e').toList();
for (var i = 0; i < names.length; i++) {
final parentName = i == 0 ? gettingStated : names[i - 1];
final parentName = i == 0 ? gettingStarted : names[i - 1];
await tester.createNewPageWithName(
name: names[i],
parentName: parentName,
@@ -92,7 +92,7 @@ void main() {
// move the document_3 to the getting started page
await tester.movePageToOtherPage(
name: names[3],
parentName: gettingStated,
parentName: gettingStarted,
layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Document,
);
@@ -101,7 +101,7 @@ void main() {
.view
.parentViewId;
final toId = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view
.id;
expect(fromId, toId);
@@ -109,13 +109,13 @@ void main() {
// move the document_2 before document_1
await tester.movePageToOtherPage(
name: names[2],
parentName: gettingStated,
parentName: gettingStarted,
layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Document,
position: DraggableHoverPosition.bottom,
);
final childViews = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view
.childViews;
expect(
@@ -170,7 +170,7 @@ void main() {
// it should not be moved
final childViews = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view
.childViews;
expect(
@@ -1,23 +1,26 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/base.dart';
import 'util/common_operations.dart';
import 'util/expectation.dart';
import 'util/keyboard.dart';
const _readmeName = 'Read me';
const _documentName = 'Document';
const _calendarName = 'Calendar';
const _documentName = 'First Doc';
const _documentTwoName = 'Second Doc';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Tabs', () {
testWidgets('Open AppFlowy and open/navigate multiple tabs',
(tester) async {
testWidgets('Open AppFlowy and open/navigate/close tabs', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -29,31 +32,21 @@ void main() {
findsNothing,
);
await tester.createNewPageWithName(
name: _calendarName,
layout: ViewLayoutPB.Calendar,
);
await tester.createNewPageWithName(
name: _documentName,
layout: ViewLayoutPB.Document,
);
// Navigate current view to "Read me" document again
await tester.tapButtonWithName(_readmeName);
await tester.createNewPageWithName(
name: _documentTwoName,
layout: ViewLayoutPB.Document,
);
/// Open second menu item in a new tab
await tester.openAppInNewTab(_calendarName);
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
/// Open third menu item in a new tab
await tester.openAppInNewTab(_documentName);
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsOneWidget,
);
await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);
expect(
find.descendant(
@@ -63,13 +56,32 @@ void main() {
findsNWidgets(3),
);
/// Navigate to the first tab
/// Navigate to the second tab
await tester.tap(
find.descendant(
of: find.byType(FlowyTab),
matching: find.text(_readmeName),
matching: find.text(gettingStarted),
),
);
/// Close tab by shortcut
await FlowyTestKeyboard.simulateKeyDownEvent(
[
Platform.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control,
LogicalKeyboardKey.keyW,
],
tester: tester,
);
expect(
find.descendant(
of: find.byType(TabBar),
matching: find.byType(FlowyTab),
),
findsNWidgets(2),
);
});
});
}
@@ -8,7 +8,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -29,7 +28,7 @@ extension CommonOperations on WidgetTester {
/// Tap the + button on the home page.
Future<void> tapAddViewButton({
String name = gettingStated,
String name = gettingStarted,
}) async {
await hoverOnPageName(
name,
@@ -211,6 +210,12 @@ extension CommonOperations on WidgetTester {
await tapButtonWithName(ViewMoreActionType.unFavorite.name);
}
/// Tap the Open in a new tab button
Future<void> tapOpenInTabButton() async {
await tapPageOptionButton();
await tapButtonWithName(ViewMoreActionType.openInNewTab.name);
}
/// Rename the page.
Future<void> renamePage(String name) async {
await tapRenamePageButton();
@@ -272,7 +277,7 @@ extension CommonOperations on WidgetTester {
bool openAfterCreated = true,
}) async {
// create a new page
await tapAddViewButton(name: parentName ?? gettingStated);
await tapAddViewButton(name: parentName ?? gettingStarted);
await tapButtonWithName(layout.menuName);
await pumpAndSettle();
@@ -336,11 +341,14 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
Future<void> openAppInNewTab(String name) async {
await hoverOnPageName(name);
await tap(find.byType(ViewDisclosureButton));
await pumpAndSettle();
await tap(find.text(LocaleKeys.disclosureAction_openNewTab.tr()));
Future<void> openAppInNewTab(String name, ViewLayoutPB layout) async {
await hoverOnPageName(
name,
onHover: () async {
await tapOpenInTabButton();
await pumpAndSettle();
},
);
await pumpAndSettle();
}
@@ -31,7 +31,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/so
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
@@ -76,7 +76,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapGoButton();
// expect to see a readme page
expectToSeePageName(gettingStated);
expectToSeePageName(gettingStarted);
await tapAddViewButton();
await tapImportButton();
@@ -87,7 +87,7 @@ class EditorOperations {
Future<void> switchSolidColorBackground() async {
final findPurpleButton = find.byWidgetPredicate(
(widget) => widget is ColorItem && widget.option.colorHex == "ffe8e0ff",
(widget) => widget is ColorItem && widget.option.name == 'Purple',
);
await tester.tapButton(findPurpleButton);
}
@@ -11,13 +11,13 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
// const String readme = 'Read me';
const String gettingStated = '⭐️ Getting started';
const String gettingStarted = '⭐️ Getting started';
extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.
void expectToSeeHomePage() {
expect(find.byType(HomeStack), findsOneWidget);
expect(find.textContaining(gettingStated), findsWidgets);
expect(find.textContaining(gettingStarted), findsWidgets);
}
/// Expect to see the page name on the home page.
@@ -8,4 +8,12 @@ extension RawKeyboardExtension on RawKeyboard {
LogicalKeyboardKey.altRight,
].contains(key),
);
bool get isControlPressed => keysPressed.any(
(key) => [
LogicalKeyboardKey.control,
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.controlRight,
].contains(key),
);
}
@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
@@ -8,7 +9,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import '../field/field_controller.dart';
import '../field/field_service.dart';
import '../field/type_option/type_option_context.dart';
import 'cell_listener.dart';
@@ -104,7 +104,7 @@ class CellController<T, D> extends Equatable {
);
// Only the primary can listen on the row meta changes.
if (_cellContext.fieldInfo.isPrimary) {
if (_cellContext.fieldInfo.field.isPrimary) {
_rowMetaListener?.start(
callback: (newRowMeta) {
_cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
@@ -15,7 +15,7 @@ import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:convert' show utf8;
import '../field/field_controller.dart';
import '../field/field_info.dart';
import '../row/row_service.dart';
part 'cell_service.freezed.dart';
part 'cell_data_loader.dart';
@@ -62,14 +62,14 @@ class DatabaseCellContext with _$DatabaseCellContext {
String get rowId => rowMeta.id;
String get fieldId => fieldInfo.id;
String get fieldId => fieldInfo.field.id;
FieldType get fieldType => fieldInfo.fieldType;
FieldType get fieldType => fieldInfo.field.fieldType;
ValueKey key() {
return ValueKey("${rowMeta.id}$fieldId${fieldInfo.fieldType}");
return ValueKey("${rowMeta.id}$fieldId${fieldInfo.field.fieldType}");
}
/// Only the primary field can have an emoji.
String? get emoji => fieldInfo.isPrimary ? rowMeta.icon : null;
String? get emoji => fieldInfo.field.isPrimary ? rowMeta.icon : null;
}
@@ -13,8 +13,10 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:collection/collection.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'database_view_service.dart';
import 'defines.dart';
import 'field/field_info.dart';
import 'layout/layout_service.dart';
import 'layout/layout_setting_listener.dart';
import 'row/row_cache.dart';
@@ -92,6 +94,8 @@ class DatabaseController {
final DatabaseGroupListener _groupListener;
final DatabaseLayoutSettingListener _layoutListener;
final ValueNotifier<bool> _isLoading = ValueNotifier(true);
DatabaseController({required ViewPB view})
: viewId = view.id,
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
@@ -109,6 +113,12 @@ class DatabaseController {
_listenOnLayoutChanged();
}
void setIsLoading(bool isLoading) {
_isLoading.value = isLoading;
}
ValueNotifier<bool> get isLoading => _isLoading;
void addListener({
DatabaseCallbacks? onDatabaseChanged,
DatabaseLayoutSettingCallbacks? onLayoutChanged,
@@ -3,12 +3,16 @@ import 'dart:collection';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../grid/presentation/widgets/filter/filter_info.dart';
import 'field/field_controller.dart';
import 'field/field_info.dart';
import 'row/row_cache.dart';
import 'row/row_service.dart';
part 'defines.freezed.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnSortsChanged = void Function(List<SortInfo>);
@@ -27,3 +31,11 @@ typedef OnNumOfRowsChanged = void Function(
);
typedef OnError = void Function(FlowyError);
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}
@@ -19,6 +19,7 @@ import '../setting/setting_listener.dart';
import '../setting/setting_service.dart';
import '../sort/sort_listener.dart';
import '../sort/sort_service.dart';
import 'field_info.dart';
import 'field_listener.dart';
class _GridFieldNotifier extends ChangeNotifier {
@@ -438,12 +439,18 @@ class FieldController {
}
void _updateFieldInfos() {
final List<FieldInfo> newFieldInfos = [];
for (final field in _fieldNotifier.fieldInfos) {
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
field._hasFilter = _filterPBByFieldId[field.id] != null;
field._hasSort = _sortPBByFieldId[field.id] != null;
newFieldInfos.add(
field.copyWith(
isGroupField: _groupConfigurationByFieldId[field.id] != null,
hasFilter: _filterPBByFieldId[field.id] != null,
hasSort: _sortPBByFieldId[field.id] != null,
),
);
}
_fieldNotifier.notify();
_fieldNotifier.fieldInfos = newFieldInfos;
}
Future<void> dispose() async {
@@ -488,7 +495,7 @@ class FieldController {
}
_fieldNotifier.fieldInfos =
newFields.map((field) => FieldInfo(field: field)).toList();
newFields.map((field) => FieldInfo.initial(field)).toList();
_loadFilters();
_loadSorts();
_updateFieldInfos();
@@ -652,7 +659,7 @@ class FieldController {
}
final List<FieldInfo> newFieldInfos = fieldInfos;
for (final indexField in insertedFields) {
final fieldInfo = FieldInfo(field: indexField.field_1);
final fieldInfo = FieldInfo.initial(indexField.field_1);
if (newFieldInfos.length > indexField.index) {
newFieldInfos.insert(indexField.index, fieldInfo);
} else {
@@ -674,7 +681,7 @@ class FieldController {
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
if (index != -1) {
newFields.removeAt(index);
final fieldInfo = FieldInfo(field: updatedFieldPB);
final fieldInfo = FieldInfo.initial(updatedFieldPB);
newFields.insert(index, fieldInfo);
updatedFields.add(fieldInfo);
}
@@ -693,7 +700,7 @@ class RowCacheDependenciesImpl extends RowFieldsDelegate with RowLifeCycle {
RowCacheDependenciesImpl(FieldController cache) : _fieldController = cache;
@override
UnmodifiableListView<FieldInfo> get fields =>
UnmodifiableListView<FieldInfo> get fieldInfos =>
UnmodifiableListView(_fieldController.fieldInfos);
@override
@@ -729,73 +736,3 @@ FieldInfo? _findFieldInfo({
return null;
}
}
class FieldInfo {
final FieldPB _field;
bool _isGroupField = false;
bool _hasFilter = false;
bool _hasSort = false;
String get id => _field.id;
FieldType get fieldType => _field.fieldType;
bool get visibility => _field.visibility;
double get width => _field.width.toDouble();
bool get isPrimary => _field.isPrimary;
String get name => _field.name;
FieldPB get field => _field;
bool get isGroupField => _isGroupField;
bool get hasFilter => _hasFilter;
bool get canBeGroup {
switch (_field.fieldType) {
case FieldType.URL:
case FieldType.Checkbox:
case FieldType.MultiSelect:
case FieldType.SingleSelect:
return true;
default:
return false;
}
}
bool get canCreateFilter {
if (hasFilter) return false;
switch (_field.fieldType) {
case FieldType.Checkbox:
case FieldType.MultiSelect:
case FieldType.RichText:
case FieldType.SingleSelect:
case FieldType.Checklist:
return true;
default:
return false;
}
}
bool get canCreateSort {
if (_hasSort) return false;
switch (_field.fieldType) {
case FieldType.RichText:
case FieldType.Checkbox:
case FieldType.Number:
case FieldType.DateTime:
return true;
default:
return false;
}
}
FieldInfo({required FieldPB field}) : _field = field;
}
@@ -0,0 +1,71 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'field_info.freezed.dart';
@freezed
class FieldInfo with _$FieldInfo {
const FieldInfo._();
const factory FieldInfo({
required FieldPB field,
required bool isGroupField,
required bool hasFilter,
required bool hasSort,
}) = _FieldInfo;
String get id => field.id;
FieldType get fieldType => field.fieldType;
String get name => field.name;
factory FieldInfo.initial(FieldPB field) => FieldInfo(
field: field,
hasFilter: false,
hasSort: false,
isGroupField: false,
);
bool get canBeGroup {
switch (field.fieldType) {
case FieldType.URL:
case FieldType.Checkbox:
case FieldType.MultiSelect:
case FieldType.SingleSelect:
return true;
default:
return false;
}
}
bool get canCreateFilter {
if (hasFilter) return false;
switch (field.fieldType) {
case FieldType.Checkbox:
case FieldType.MultiSelect:
case FieldType.RichText:
case FieldType.SingleSelect:
case FieldType.Checklist:
return true;
default:
return false;
}
}
bool get canCreateSort {
if (hasSort) return false;
switch (field.fieldType) {
case FieldType.RichText:
case FieldType.Checkbox:
case FieldType.Number:
case FieldType.DateTime:
case FieldType.SingleSelect:
case FieldType.MultiSelect:
return true;
default:
return false;
}
}
}
@@ -1,4 +1,3 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -1,7 +1,6 @@
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
@@ -115,18 +114,6 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
(err) => Log.error(err),
);
});
// Set the icon and cover of the view
ViewBackendService.updateView(
viewId: viewId,
iconURL: iconURL,
coverURL: coverURL,
).then((result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
});
}
}
@@ -136,7 +123,7 @@ class RowBannerEvent with _$RowBannerEvent {
const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
_DidReceiveRowMeta;
const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUdate;
_DidReceiveFieldUpdate;
const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;
const factory RowBannerEvent.setCover(String coverURL) = _SetCover;
}
@@ -1,4 +1,5 @@
import 'dart:collection';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
@@ -6,7 +7,6 @@ import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../cell/cell_service.dart';
import '../field/field_controller.dart';
import 'row_list.dart';
import 'row_service.dart';
part 'row_cache.freezed.dart';
@@ -15,7 +15,7 @@ typedef RowUpdateCallback = void Function();
/// A delegate that provides the fields of the row.
abstract class RowFieldsDelegate {
UnmodifiableListView<FieldInfo> get fields;
UnmodifiableListView<FieldInfo> get fieldInfos;
void onFieldsChanged(void Function(List<FieldInfo>) callback);
}
@@ -244,12 +244,12 @@ class RowCache {
CellContextByFieldId _makeCells(RowMetaPB rowMeta) {
// ignore: prefer_collection_literals
final cellContextMap = CellContextByFieldId();
for (final field in _fieldDelegate.fields) {
if (field.visibility) {
cellContextMap[field.id] = DatabaseCellContext(
for (final fieldInfo in _fieldDelegate.fieldInfos) {
if (fieldInfo.field.visibility) {
cellContextMap[fieldInfo.id] = DatabaseCellContext(
rowMeta: rowMeta,
viewId: viewId,
fieldInfo: field,
fieldInfo: fieldInfo,
);
}
}
@@ -259,7 +259,7 @@ class RowCache {
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
return RowInfo(
viewId: viewId,
fields: _fieldDelegate.fields,
fields: _fieldDelegate.fieldInfos,
rowId: rowMetaPB.id,
rowMeta: rowMetaPB,
);
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -26,8 +27,8 @@ class DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {
initial: () {
_startListening();
},
didReceiveFieldUpdate: (fieldContexts) {
emit(state.copyWith(fieldContexts: fieldContexts));
didReceiveFieldUpdate: (fieldInfos) {
emit(state.copyWith(fieldInfos: fieldInfos));
},
setGroupByField: (String fieldId, FieldType fieldType) async {
final result = await _groupBackendSvc.groupByField(
@@ -50,8 +51,8 @@ class DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {
}
void _startListening() {
_onFieldsFn = (fieldContexts) =>
add(DatabaseGroupEvent.didReceiveFieldUpdate(fieldContexts));
_onFieldsFn = (fieldInfos) =>
add(DatabaseGroupEvent.didReceiveFieldUpdate(fieldInfos));
_fieldController.addListener(
onReceiveFields: _onFieldsFn,
listenWhen: () => !isClosed,
@@ -75,15 +76,15 @@ class DatabaseGroupEvent with _$DatabaseGroupEvent {
class DatabaseGroupState with _$DatabaseGroupState {
const factory DatabaseGroupState({
required String viewId,
required List<FieldInfo> fieldContexts,
required List<FieldInfo> fieldInfos,
}) = _DatabaseGroupState;
factory DatabaseGroupState.initial(
String viewId,
List<FieldInfo> fieldContexts,
List<FieldInfo> fieldInfos,
) =>
DatabaseGroupState(
viewId: viewId,
fieldContexts: fieldContexts,
fieldInfos: fieldInfos,
);
}
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:collection';
import 'package:appflowy/plugins/database_view/application/defines.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:dartz/dartz.dart';
@@ -254,11 +256,14 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
Future<void> _openGrid(Emitter<BoardState> emit) async {
final result = await databaseController.open();
result.fold(
(grid) => emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
),
(grid) {
databaseController.setIsLoading(false);
emit(
state.copyWith(loadingState: LoadingState.finish(left(unit))),
);
},
(err) => emit(
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
state.copyWith(loadingState: LoadingState.finish(right(err))),
),
);
}
@@ -323,7 +328,7 @@ class BoardState with _$BoardState {
required Option<DatabasePB> grid,
required List<String> groupIds,
required Option<BoardEditingRow> editingRow,
required GridLoadingState loadingState,
required LoadingState loadingState,
required Option<FlowyError> noneOrError,
}) = _BoardState;
@@ -333,18 +338,10 @@ class BoardState with _$BoardState {
groupIds: [],
editingRow: none(),
noneOrError: none(),
loadingState: const _Loading(),
loadingState: const LoadingState.loading(),
);
}
@freezed
class GridLoadingState with _$GridLoadingState {
const factory GridLoadingState.loading() = _Loading;
const factory GridLoadingState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}
class GridFieldEquatable extends Equatable {
final UnmodifiableListView<FieldPB> _fields;
const GridFieldEquatable(
@@ -12,10 +12,10 @@ class BoardSettingBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Spacer(),
SettingButton(databaseController: databaseController),
],
),
@@ -1,5 +1,7 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/defines.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
@@ -133,11 +135,14 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
Future<void> _openDatabase(Emitter<CalendarState> emit) async {
final result = await databaseController.open();
result.fold(
(database) => emit(
state.copyWith(loadingState: DatabaseLoadingState.finish(left(unit))),
),
(database) {
databaseController.setIsLoading(false);
emit(
state.copyWith(loadingState: LoadingState.finish(left(unit))),
);
},
(err) => emit(
state.copyWith(loadingState: DatabaseLoadingState.finish(right(err))),
state.copyWith(loadingState: LoadingState.finish(right(err))),
),
);
}
@@ -425,7 +430,7 @@ class CalendarState with _$CalendarState {
CalendarEventData<CalendarDayEvent>? updateEvent,
required List<String> deleteEventIds,
required Option<CalendarLayoutSettingPB> settings,
required DatabaseLoadingState loadingState,
required LoadingState loadingState,
required Option<FlowyError> noneOrError,
}) = _CalendarState;
@@ -436,18 +441,10 @@ class CalendarState with _$CalendarState {
deleteEventIds: [],
settings: none(),
noneOrError: none(),
loadingState: const _Loading(),
loadingState: const LoadingState.loading(),
);
}
@freezed
class DatabaseLoadingState with _$DatabaseLoadingState {
const factory DatabaseLoadingState.loading() = _Loading;
const factory DatabaseLoadingState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}
class CalendarEditingRow {
RowPB row;
int? index;
@@ -1,5 +1,6 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
@@ -254,18 +254,23 @@ class _EventList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Flexible(
child: ListView.separated(
itemBuilder: (BuildContext context, int index) => EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: true,
),
child: ListView.separated(
itemBuilder: (BuildContext context, int index) => EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
),
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
),
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
),
);
}
@@ -1,7 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -159,10 +158,21 @@ class _CalendarPageState extends State<CalendarPage> {
],
child: BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
return _buildCalendar(
context,
_eventController,
state.settings.foldLeft(0, (previous, a) => a.firstDayOfWeek),
return ValueListenableBuilder<bool>(
valueListenable: widget.databaseController.isLoading,
builder: (_, value, ___) {
if (value) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
return _buildCalendar(
context,
_eventController,
state.settings
.foldLeft(0, (previous, a) => a.firstDayOfWeek),
);
},
);
},
),
@@ -177,20 +187,23 @@ class _CalendarPageState extends State<CalendarPage> {
int firstDayOfWeek,
) {
return Padding(
padding: GridSize.contentInsets,
padding: CalendarSize.contentInsets,
child: LayoutBuilder(
// must specify MonthView width for useAvailableVerticalSpace to work properly
builder: (context, constraints) => MonthView(
key: _calendarState,
controller: _eventController,
width: constraints.maxWidth,
cellAspectRatio: 0.6,
startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor,
headerBuilder: _headerNavigatorBuilder,
weekDayBuilder: _headerWeekDayBuilder,
cellBuilder: _calendarDayBuilder,
useAvailableVerticalSpace: widget.shrinkWrap,
builder: (context, constraints) => ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: MonthView(
key: _calendarState,
controller: _eventController,
width: constraints.maxWidth,
cellAspectRatio: 0.6,
startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor,
headerBuilder: _headerNavigatorBuilder,
weekDayBuilder: _headerWeekDayBuilder,
cellBuilder: _calendarDayBuilder,
useAvailableVerticalSpace: widget.shrinkWrap,
),
),
),
);
@@ -1,8 +1,18 @@
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:flutter/widgets.dart';
class CalendarSize {
static double scale = 1;
static double get headerContainerPadding => 12 * scale;
static EdgeInsets get contentInsets => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding,
CalendarSize.headerContainerPadding,
GridSize.leadingHeaderPadding,
CalendarSize.headerContainerPadding,
);
static double get scrollBarSize => 8 * scale;
static double get navigatorButtonWidth => 20 * scale;
static double get navigatorButtonHeight => 25 * scale;
@@ -8,6 +8,7 @@ import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.da
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -23,11 +24,12 @@ class CalendarSettingBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
UnscheduleEventsButton(databaseController: databaseController),
const HSpace(2),
SettingButton(
databaseController: databaseController,
),
@@ -82,9 +84,14 @@ class _UnscheduleEventsButtonState extends State<UnscheduleEventsButton> {
builder: (context, state) {
return FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
fontSize: FontSizes.s11,
fontColor: AFThemeExtension.of(context).textColor,
fontWeight: FontWeight.w400,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border,
onPressed: () => _popoverController.show(),
);
},
),
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -1,16 +1,16 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/defines.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../application/field/field_controller.dart';
import '../../application/database_controller.dart';
import 'dart:collection';
@@ -134,12 +134,13 @@ class GridBloc extends Bloc<GridEvent, GridState> {
final result = await databaseController.open();
result.fold(
(grid) {
databaseController.setIsLoading(false);
emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
state.copyWith(loadingState: LoadingState.finish(left(unit))),
);
},
(err) => emit(
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
state.copyWith(loadingState: LoadingState.finish(right(err))),
),
);
}
@@ -177,7 +178,7 @@ class GridState with _$GridState {
required GridFieldEquatable fields,
required List<RowInfo> rowInfos,
required int rowCount,
required GridLoadingState loadingState,
required LoadingState loadingState,
required bool reorderable,
required ChangedReason reason,
required List<SortInfo> sorts,
@@ -191,40 +192,33 @@ class GridState with _$GridState {
grid: none(),
viewId: viewId,
reorderable: true,
loadingState: const _Loading(),
loadingState: const LoadingState.loading(),
reason: const InitialListState(),
filters: [],
sorts: [],
);
}
@freezed
class GridLoadingState with _$GridLoadingState {
const factory GridLoadingState.loading() = _Loading;
const factory GridLoadingState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}
class GridFieldEquatable extends Equatable {
final List<FieldInfo> _fields;
final List<FieldInfo> _fieldInfos;
const GridFieldEquatable(
List<FieldInfo> fields,
) : _fields = fields;
List<FieldInfo> fieldInfos,
) : _fieldInfos = fieldInfos;
@override
List<Object?> get props {
if (_fields.isEmpty) {
if (_fieldInfos.isEmpty) {
return [];
}
return [
_fields.length,
_fields
.map((field) => field.width)
_fieldInfos.length,
_fieldInfos
.map((fieldInfo) => fieldInfo.field.width)
.reduce((value, element) => value + element),
];
}
UnmodifiableListView<FieldInfo> get value => UnmodifiableListView(_fields);
UnmodifiableListView<FieldInfo> get value =>
UnmodifiableListView(_fieldInfos);
}
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -26,7 +27,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
emit(
state.copyWith(
fields: value.fields
.where((element) => element.visibility)
.where((element) => element.field.visibility)
.toList(),
),
);
@@ -1,4 +1,5 @@
import 'dart:collection';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:equatable/equatable.dart';
@@ -7,7 +8,6 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_service.dart';
import '../../../application/field/field_controller.dart';
import '../../../application/row/row_cache.dart';
import '../../../application/row/row_controller.dart';
import '../../../application/row/row_service.dart';
@@ -119,16 +119,16 @@ class RowState with _$RowState {
}
class GridCellEquatable extends Equatable {
final FieldInfo _fieldContext;
final FieldInfo _fieldInfo;
const GridCellEquatable(FieldInfo field) : _fieldContext = field;
const GridCellEquatable(FieldInfo field) : _fieldInfo = field;
@override
List<Object?> get props => [
_fieldContext.id,
_fieldContext.fieldType,
_fieldContext.visibility,
_fieldContext.width,
_fieldInfo.id,
_fieldInfo.fieldType,
_fieldInfo.field.visibility,
_fieldInfo.field.width,
];
}
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart';
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/sort/sort_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart';
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@@ -1,4 +1,4 @@
import '../../../application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
List<FieldInfo> getCreatableSorts(List<FieldInfo> fieldInfos) {
final List<FieldInfo> creatableFields = List.from(fieldInfos);
@@ -1,4 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'sizes.dart';
class GridLayout {
@@ -6,7 +6,7 @@ class GridLayout {
if (fields.isEmpty) return 0;
final fieldsWidth = fields
.map((field) => field.width.toDouble())
.map((fieldInfo) => fieldInfo.field.width.toDouble())
.reduce((value, element) => value + element);
return fieldsWidth +
@@ -30,6 +30,9 @@ class GridSize {
static EdgeInsets get typeOptionContentInsets => const EdgeInsets.all(4);
static EdgeInsets get toolbarSettingButtonInsets =>
const EdgeInsets.symmetric(horizontal: 8, vertical: 2);
static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding,
GridSize.headerContainerPadding,
@@ -109,7 +109,7 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
children: [
Expanded(
child: FlowyText(
state.filterInfo.fieldInfo.name,
state.filterInfo.fieldInfo.field.name,
overflow: TextOverflow.ellipsis,
),
),
@@ -93,7 +93,7 @@ class ChecklistState extends State<ChecklistFilterEditor> {
children: [
Expanded(
child: FlowyText(
state.filterInfo.fieldInfo.name,
state.filterInfo.fieldInfo.field.name,
overflow: TextOverflow.ellipsis,
),
),
@@ -39,7 +39,7 @@ class ChoiceChipButton extends StatelessWidget {
decoration: decoration,
useIntrinsicWidth: true,
text: FlowyText(
filterInfo.fieldInfo.name,
filterInfo.fieldInfo.field.name,
color: AFThemeExtension.of(context).textColor,
),
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
@@ -144,7 +144,7 @@ class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
children: [
Expanded(
child: FlowyText(
state.filterInfo.fieldInfo.name,
state.filterInfo.fieldInfo.field.name,
overflow: TextOverflow.ellipsis,
),
),
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -161,7 +162,7 @@ class GridFilterPropertyCell extends StatelessWidget {
return FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
fieldInfo.name,
fieldInfo.field.name,
color: AFThemeExtension.of(context).textColor,
),
onTap: () => onTap(fieldInfo),
@@ -1,4 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pb.dart';
@@ -96,7 +96,7 @@ class _GridHeaderState extends State<_GridHeader> {
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final cells = state.fields
.where((field) => field.visibility)
.where((fieldInfo) => fieldInfo.field.visibility)
.map(
(field) => FieldContext(
viewId: widget.viewId,
@@ -1,6 +1,6 @@
import 'dart:typed_data';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_controller.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_entities.pb.dart';
@@ -276,13 +276,13 @@ class RowContent extends StatelessWidget {
final GridCellWidget child = builder.build(cellId);
return CellContainer(
width: cellId.fieldInfo.width.toDouble(),
isPrimary: cellId.fieldInfo.isPrimary,
width: cellId.fieldInfo.field.width.toDouble(),
isPrimary: cellId.fieldInfo.field.isPrimary,
cellContainerNotifier: CellContainerNotifier(child),
accessoryBuilder: (buildContext) {
final builder = child.accessoryBuilder;
final List<GridCellAccessoryBuilder> accessories = [];
if (cellId.fieldInfo.isPrimary) {
if (cellId.fieldInfo.field.isPrimary) {
accessories.add(
GridCellAccessoryBuilder(
builder: (key) => PrimaryCellAccessory(
@@ -1,5 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_create_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
@@ -1,4 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';
class SortInfo {
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -30,23 +31,23 @@ class _FilterButtonState extends State<FilterButton> {
return _wrapPopover(
context,
SizedBox(
height: 26,
child: FlowyTextButton(
LocaleKeys.grid_settings_filter.tr(),
fontColor: textColor,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
onPressed: () {
final bloc = context.read<GridFilterMenuBloc>();
if (bloc.state.filters.isEmpty) {
_popoverController.show();
} else {
bloc.add(const GridFilterMenuEvent.toggleMenu());
}
},
),
FlowyTextButton(
LocaleKeys.grid_settings_filter.tr(),
fontColor: textColor,
fontSize: FontSizes.s11,
fontWeight: FontWeight.w400,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border,
onPressed: () {
final bloc = context.read<GridFilterMenuBloc>();
if (bloc.state.filters.isEmpty) {
_popoverController.show();
} else {
bloc.add(const GridFilterMenuEvent.toggleMenu());
}
},
),
);
},
@@ -2,8 +2,8 @@ import 'package:appflowy/plugins/database_view/application/database_controller.d
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -47,20 +47,28 @@ class GridSettingBar extends StatelessWidget {
listener: (context, state) => toggleExtension.toggle(),
),
],
child: SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: GridSize.leadingHeaderPadding),
const Spacer(),
const FilterButton(),
const SortButton(),
SettingButton(
databaseController: controller,
child: ValueListenableBuilder<bool>(
valueListenable: controller.isLoading,
builder: (context, value, child) {
if (value) {
return const SizedBox.shrink();
}
return SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const FilterButton(),
const HSpace(2),
const SortButton(),
const HSpace(2),
SettingButton(
databaseController: controller,
),
],
),
],
),
);
},
),
),
);
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -30,23 +31,23 @@ class _SortButtonState extends State<SortButton> {
return wrapPopover(
context,
SizedBox(
height: 26,
child: FlowyTextButton(
LocaleKeys.grid_settings_sort.tr(),
fontColor: textColor,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
onPressed: () {
final bloc = context.read<SortMenuBloc>();
if (bloc.state.sortInfos.isEmpty) {
_popoverController.show();
} else {
bloc.add(const SortMenuEvent.toggleMenu());
}
},
),
FlowyTextButton(
LocaleKeys.grid_settings_sort.tr(),
fontColor: textColor,
fontSize: FontSizes.s11,
fontWeight: FontWeight.w400,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border,
onPressed: () {
final bloc = context.read<SortMenuBloc>();
if (bloc.state.sortInfos.isEmpty) {
_popoverController.show();
} else {
bloc.add(const SortMenuEvent.toggleMenu());
}
},
),
);
},
@@ -1,7 +1,6 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -38,7 +37,7 @@ class DatabaseViewSettingExtension extends StatelessWidget {
),
);
} else {
return const SizedBox();
return const SizedBox.shrink();
}
},
),
@@ -58,29 +57,18 @@ class _DatabaseViewSettingContent extends StatelessWidget {
return BlocBuilder<DatabaseViewSettingExtensionBloc,
DatabaseViewSettingExtensionState>(
builder: (context, state) {
final children = <Widget>[
Divider(
height: 1.0,
color: AFThemeExtension.of(context).toggleOffFill,
),
const VSpace(6),
IntrinsicHeight(
child: Row(
children: [
SortMenu(
fieldController: fieldController,
),
const HSpace(6),
FilterMenu(
fieldController: fieldController,
),
],
),
)
];
return _wrapPadding(
Column(children: children),
Row(
children: [
SortMenu(
fieldController: fieldController,
),
const HSpace(6),
FilterMenu(
fieldController: fieldController,
),
],
),
);
},
);
@@ -90,7 +78,7 @@ class _DatabaseViewSettingContent extends StatelessWidget {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
vertical: 6,
vertical: 8,
),
child: child,
);
@@ -0,0 +1,286 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/tar_bar_bloc.dart';
import 'tar_bar_add_button.dart';
class TabBarHeader extends StatefulWidget {
const TabBarHeader({super.key});
@override
State<TabBarHeader> createState() => _TabBarHeaderState();
}
class _TabBarHeaderState extends State<TabBarHeader> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Divider(
color: Theme.of(context).dividerColor,
height: 1,
thickness: 1,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return const Flexible(
child: DatabaseTabBar(),
);
},
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return SizedBox(
width: 200,
child: Column(
children: [
const VSpace(3),
pageSettingBarFromState(state),
],
),
);
},
),
],
),
],
);
}
Widget pageSettingBarFromState(GridTabBarState state) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}
final tarBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
return tarBar.builder.settingBar(
context,
controller,
);
}
}
class DatabaseTabBar extends StatefulWidget {
const DatabaseTabBar({super.key});
@override
State<DatabaseTabBar> createState() => _DatabaseTabBarState();
}
class _DatabaseTabBarState extends State<DatabaseTabBar> {
final _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
final children = state.tabBars.indexed.map((indexed) {
final isSelected = state.selectedIndex == indexed.$1;
final tabBar = indexed.$2;
return DatabaseTabBarItem(
key: ValueKey(tabBar.viewId),
view: tabBar.view,
isSelected: isSelected,
onTap: (selectedView) {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.selectView(selectedView.id),
);
},
);
}).toList();
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: ListView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
children: children,
),
),
AddDatabaseViewButton(
onTap: (action) async {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.createView(action),
);
},
),
],
);
},
);
}
}
class DatabaseTabBarItem extends StatelessWidget {
final bool isSelected;
final ViewPB view;
final Function(ViewPB) onTap;
const DatabaseTabBarItem({
required this.view,
required this.isSelected,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 160),
child: Stack(
children: [
SizedBox(
height: 26,
child: TabBarItemButton(
view: view,
isSelected: isSelected,
onTap: () => onTap(view),
),
),
if (isSelected)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Divider(
height: 2,
thickness: 2,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
);
}
}
class TabBarItemButton extends StatelessWidget {
final ViewPB view;
final bool isSelected;
final VoidCallback onTap;
const TabBarItemButton({
required this.view,
required this.onTap,
super.key,
required this.isSelected,
});
@override
Widget build(BuildContext context) {
return PopoverActionList<TabBarViewAction>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: TabBarViewAction.values,
buildChild: (controller) {
Color? color;
if (!isSelected) {
color = Theme.of(context).hintColor;
}
if (Theme.of(context).brightness == Brightness.dark) {
color = null;
}
return IntrinsicWidth(
child: FlowyButton(
radius: Corners.s6Border,
hoverColor: AFThemeExtension.of(context).greyHover,
onTap: onTap,
onSecondaryTap: () {
controller.show();
},
leftIcon: FlowySvg(
name: view.iconName,
size: const Size(14, 14),
color: color,
),
text: FlowyText(
view.name,
fontSize: FontSizes.s11,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
color: color,
fontWeight: isSelected ? null : FontWeight.w400,
),
),
);
},
onSelected: (action, controller) {
switch (action) {
case TabBarViewAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
value: view.name,
confirm: (newValue) {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.renameView(view.id, newValue),
);
},
).show(context);
break;
case TabBarViewAction.delete:
NavigatorAlertDialog(
title: LocaleKeys.grid_deleteView.tr(),
confirm: () {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.deleteView(view.id),
);
},
).show(context);
break;
}
controller.close();
},
);
}
}
enum TabBarViewAction implements ActionCell {
rename,
delete;
@override
String get name {
switch (this) {
case TabBarViewAction.rename:
return LocaleKeys.disclosureAction_rename.tr();
case TabBarViewAction.delete:
return LocaleKeys.disclosureAction_delete.tr();
}
}
Widget icon(Color iconColor) {
switch (this) {
case TabBarViewAction.rename:
return const FlowySvg(name: 'editor/edit');
case TabBarViewAction.delete:
return const FlowySvg(name: 'editor/delete');
}
}
@override
Widget? leftIcon(Color iconColor) => icon(iconColor);
@override
Widget? rightIcon(Color iconColor) => null;
}
@@ -1,26 +1,16 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/database_controller.dart';
import '../grid/presentation/layout/sizes.dart';
import 'tar_bar_add_button.dart';
import 'tab_bar_header.dart';
abstract class DatabaseTabBarItemBuilder {
const DatabaseTabBarItemBuilder();
@@ -93,30 +83,29 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
],
child: Column(
children: [
Row(
children: [
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return const Flexible(
child: Padding(
padding: EdgeInsets.only(left: 50),
child: DatabaseTabBar(),
),
);
},
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return ValueListenableBuilder<bool>(
valueListenable: state
.tabBarControllerByViewId[state.parentView.id]!
.controller
.isLoading,
builder: (_, value, ___) {
if (value) {
return const SizedBox.shrink();
}
return SizedBox(
width: 300,
height: 30,
child: Padding(
padding: const EdgeInsets.only(right: 50),
child: pageSettingBarFromState(state),
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
),
child: const TabBarHeader(),
),
);
},
),
],
);
},
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
@@ -154,19 +143,6 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
}).toList();
}
Widget pageSettingBarFromState(GridTabBarState state) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}
final tarBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
return tarBar.builder.settingBar(
context,
controller,
);
}
Widget pageSettingBarExtensionFromState(GridTabBarState state) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
@@ -237,188 +213,3 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
@override
List<NavigationItem> get navigationItems => [this];
}
class DatabaseTabBar extends StatefulWidget {
const DatabaseTabBar({super.key});
@override
State<DatabaseTabBar> createState() => _DatabaseTabBarState();
}
class _DatabaseTabBarState extends State<DatabaseTabBar> {
final _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
final children = state.tabBars.indexed.map((indexed) {
final isSelected = state.selectedIndex == indexed.$1;
final tabBar = indexed.$2;
return DatabaseTabBarItem(
key: ValueKey(tabBar.viewId),
view: tabBar.view,
isSelected: isSelected,
onTap: (selectedView) {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.selectView(selectedView.id),
);
},
);
}).toList();
return Row(
children: [
Flexible(
child: SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: IntrinsicWidth(
child: Row(children: children),
),
),
),
AddDatabaseViewButton(
onTap: (action) async {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.createView(action),
);
},
),
],
);
},
);
}
}
class DatabaseTabBarItem extends StatelessWidget {
final bool isSelected;
final ViewPB view;
final Function(ViewPB) onTap;
const DatabaseTabBarItem({
required this.view,
required this.isSelected,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: 80, maxWidth: 160),
child: IntrinsicWidth(
child: Column(
children: [
TabBarItemButton(
view: view,
onTap: () => onTap(view),
),
if (isSelected)
Divider(
height: 1,
thickness: 2,
color: Theme.of(context).colorScheme.secondary,
),
],
),
),
);
}
}
class TabBarItemButton extends StatelessWidget {
final ViewPB view;
final VoidCallback onTap;
const TabBarItemButton({
required this.view,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return PopoverActionList<TabBarViewAction>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: TabBarViewAction.values,
buildChild: (controller) {
return FlowyButton(
radius: Corners.s5Border,
hoverColor: AFThemeExtension.of(context).greyHover,
onTap: onTap,
onSecondaryTap: () {
controller.show();
},
text: FlowyText.medium(
view.name,
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
margin: GridSize.cellContentInsets,
leftIcon: svgWidget(
view.iconName,
color: Theme.of(context).iconTheme.color,
),
);
},
onSelected: (action, controller) {
switch (action) {
case TabBarViewAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
value: view.name,
confirm: (newValue) {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.renameView(view.id, newValue),
);
},
).show(context);
break;
case TabBarViewAction.delete:
NavigatorAlertDialog(
title: LocaleKeys.grid_deleteView.tr(),
confirm: () {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.deleteView(view.id),
);
},
).show(context);
break;
}
controller.close();
},
);
}
}
enum TabBarViewAction implements ActionCell {
rename,
delete;
@override
String get name {
switch (this) {
case TabBarViewAction.rename:
return LocaleKeys.disclosureAction_rename.tr();
case TabBarViewAction.delete:
return LocaleKeys.disclosureAction_delete.tr();
}
}
Widget icon(Color iconColor) {
switch (this) {
case TabBarViewAction.rename:
return const FlowySvg(name: 'editor/edit');
case TabBarViewAction.delete:
return const FlowySvg(name: 'editor/delete');
}
}
@override
Widget? leftIcon(Color iconColor) => icon(iconColor);
@override
Widget? rightIcon(Color iconColor) => null;
}
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
@@ -32,13 +33,27 @@ class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
offset: const Offset(0, 8),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none,
child: FlowyIconButton(
iconPadding: const EdgeInsets.all(4),
hoverColor: AFThemeExtension.of(context).greyHover,
onPressed: () => popoverController.show(),
icon: svgWidget(
'home/add',
color: Theme.of(context).colorScheme.tertiary,
child: SizedBox(
height: 26,
child: Row(
children: [
VerticalDivider(
width: 1.0,
thickness: 1.0,
indent: 4.0,
endIndent: 4.0,
color: Theme.of(context).dividerColor,
),
FlowyIconButton(
width: 26,
iconPadding: const EdgeInsets.all(5),
hoverColor: AFThemeExtension.of(context).greyHover,
onPressed: () => popoverController.show(),
radius: Corners.s4Border,
icon: const FlowySvg(name: 'home/add'),
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
),
],
),
),
popupBuilder: (BuildContext context) {
@@ -1,10 +1,10 @@
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_controller_builder.dart';
import '../../../application/field/field_controller.dart';
part 'date_card_cell_bloc.freezed.dart';
class DateCardCellBloc extends Bloc<DateCardCellEvent, DateCardCellState> {
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
@@ -95,7 +96,7 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
@override
Widget build(BuildContext context) {
final checkmark = svgWidget(
widget.fieldInfo.visibility ? 'home/show' : 'home/hide',
widget.fieldInfo.field.visibility ? 'home/show' : 'home/hide',
color: Theme.of(context).iconTheme.color,
);
@@ -130,7 +131,7 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
context.read<DatabasePropertyBloc>().add(
DatabasePropertyEvent.setFieldVisibility(
widget.fieldInfo.id,
!widget.fieldInfo.visibility,
!widget.fieldInfo.field.visibility,
),
);
},
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
@@ -30,8 +31,9 @@ class DatabaseGroupList extends StatelessWidget {
fieldController: fieldController,
)..add(const DatabaseGroupEvent.initial()),
child: BlocBuilder<DatabaseGroupBloc, DatabaseGroupState>(
buildWhen: (previous, current) => true,
builder: (context, state) {
final cells = state.fieldContexts.map((fieldInfo) {
final cells = state.fieldInfos.map((fieldInfo) {
Widget cell = _GridGroupCell(
fieldInfo: fieldInfo,
onSelected: () => onDismissed(),
@@ -141,7 +141,7 @@ class GridDateCellText extends StatelessWidget {
padding: GridSize.cellContentInsets,
child: FlowyText.medium(
dateStr,
overflow: TextOverflow.ellipsis,
maxLines: null,
),
),
),
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -54,7 +54,7 @@ class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: 1,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
@@ -152,7 +152,7 @@ class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
child: TextField(
controller: _controller,
focusNode: focusNode,
maxLines: 1,
maxLines: null,
style: (widget.cellStyle?.textStyle ??
Theme.of(context).textTheme.bodyMedium)
?.copyWith(
@@ -44,7 +44,7 @@ class RowPropertyList extends StatelessWidget {
// The rest of the fields are displayed in the order of the field
// list
...state.cells
.where((element) => !element.fieldInfo.isPrimary)
.where((element) => !element.fieldInfo.field.isPrimary)
.map(
(cell) => _PropertyCell(
cellContext: cell,
@@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.d
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -32,29 +33,29 @@ class _SettingButtonState extends State<SettingButton> {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 26,
child: AppFlowyPopover(
controller: _popoverController,
constraints: BoxConstraints.loose(const Size(200, 400)),
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 8),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none,
child: FlowyTextButton(
LocaleKeys.settings_title.tr(),
fontColor: AFThemeExtension.of(context).textColor,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
onPressed: () => _popoverController.show(),
),
popupBuilder: (BuildContext context) {
return _DatabaseSettingListPopover(
databaseController: widget.databaseController,
);
},
return AppFlowyPopover(
controller: _popoverController,
constraints: BoxConstraints.loose(const Size(200, 400)),
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 8),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none,
child: FlowyTextButton(
LocaleKeys.settings_title.tr(),
fontColor: AFThemeExtension.of(context).textColor,
fontSize: FontSizes.s11,
fontWeight: FontWeight.w400,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border,
onPressed: () => _popoverController.show(),
),
popupBuilder: (BuildContext context) {
return _DatabaseSettingListPopover(
databaseController: widget.databaseController,
);
},
);
}
}
@@ -1,5 +1,6 @@
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/doc_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
show
@@ -31,13 +32,13 @@ class TransactionAdapter {
final String documentId;
Future<void> apply(Transaction transaction, EditorState editorState) async {
// Log.debug('transaction => ${transaction.toJson()}');
Log.debug('transaction => ${transaction.toJson()}');
final actions = transaction.operations
.map((op) => op.toBlockAction(editorState))
.whereNotNull()
.expand((element) => element)
.toList(growable: false); // avoid lazy evaluation
// Log.debug('actions => $actions');
Log.debug('actions => $actions');
await documentService.applyAction(
documentId: documentId,
actions: actions,
@@ -72,7 +73,7 @@ extension on InsertOperation {
editorState.getNodeAtPath(path.previous)?.id ??
'';
assert(parentId.isNotEmpty);
if (path.equals(path.previous)) {
if (path.equals(path.previous) && !path.equals([0])) {
prevId = '';
} else {
assert(prevId.isNotEmpty && prevId != node.id);
@@ -65,7 +65,8 @@ class _DocumentPageState extends State<DocumentPage> {
child: BlocBuilder<DocumentBloc, DocumentState>(
builder: (context, state) {
return state.loadingState.when(
loading: () => const SizedBox.shrink(),
loading: () =>
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) => result.fold(
(error) {
Log.error(error);
@@ -5,6 +5,7 @@ import 'package:appflowy/workspace/application/settings/shortcuts/settings_short
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -185,6 +186,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
// OptionAction.moveDown,
];
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
final configuration = BlockComponentConfiguration(
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
);
@@ -250,6 +253,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
),
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
configuration: configuration,
defaultColor: calloutBGColor,
),
DividerBlockKeys.type: DividerBlockComponentBuilder(
configuration: configuration,
@@ -16,6 +16,8 @@ enum OptionAction {
turnInto,
moveUp,
moveDown,
/// callout background color
color,
divider,
align;
@@ -241,11 +243,12 @@ class ColorOptionAction extends PopoverActionCell {
final bgColor =
node.attributes[blockComponentBackgroundColor] as String?;
final selectedColor = bgColor?.toColor();
// get default background color from themeExtension
final defaultColor = AFThemeExtension.of(context).calloutBGColor;
final colors = [
// clear background color.
// reset to default background color
FlowyColorOption(
color: Colors.transparent,
color: defaultColor,
name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
),
...FlowyTint.values.map(
@@ -265,8 +268,7 @@ class ColorOptionAction extends PopoverActionCell {
),
onTap: (color, index) async {
final transaction = editorState.transaction;
final backgroundColor =
color == Colors.transparent ? null : color.toHex();
final backgroundColor = color.toHex();
transaction.updateNode(node, {
blockComponentBackgroundColor: backgroundColor,
});
@@ -1,4 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -26,16 +27,16 @@ class CalloutBlockKeys {
static const String icon = 'icon';
}
// creating a new callout node
// The one is inserted through selection menu
Node calloutNode({
Delta? delta,
String emoji = '📌',
String backgroundColor = '#F0F0F0',
Color? defaultColor,
}) {
final attributes = {
CalloutBlockKeys.delta: (delta ?? Delta()).toJson(),
CalloutBlockKeys.icon: emoji,
CalloutBlockKeys.backgroundColor: backgroundColor,
CalloutBlockKeys.backgroundColor: defaultColor?.toHex() ?? '#f2f2f2',
};
return Node(
type: CalloutBlockKeys.type,
@@ -43,12 +44,13 @@ Node calloutNode({
);
}
// defining the callout block menu item for selection
// defining the callout block menu item in selection menu
SelectionMenuItem calloutItem = SelectionMenuItem.node(
name: 'Callout',
iconData: Icons.note,
keywords: ['callout'],
nodeBuilder: (editorState) => calloutNode(),
keywords: [CalloutBlockKeys.type],
nodeBuilder: (editorState, context) =>
calloutNode(defaultColor: AFThemeExtension.of(context).calloutBGColor),
replace: (_, node) => node.delta?.isEmpty ?? false,
updateSelection: (_, path, __, ___) {
return Selection.single(path: path, startOffset: 0);
@@ -59,17 +61,21 @@ SelectionMenuItem calloutItem = SelectionMenuItem.node(
class CalloutBlockComponentBuilder extends BlockComponentBuilder {
CalloutBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
required this.defaultColor,
});
@override
final BlockComponentConfiguration configuration;
final Color defaultColor;
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
return CalloutBlockComponentWidget(
key: node.key,
node: node,
defaultColor: defaultColor,
configuration: configuration,
showActions: showActions(node),
actionBuilder: (context, state) => actionBuilder(
@@ -96,8 +102,11 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget {
super.showActions,
super.actionBuilder,
super.configuration = const BlockComponentConfiguration(),
required this.defaultColor,
});
final Color defaultColor;
@override
State<CalloutBlockComponentWidget> createState() =>
_CalloutBlockComponentWidgetState();
@@ -128,17 +137,8 @@ class _CalloutBlockComponentWidgetState
// get the background color of the note block from the node's attributes
Color get backgroundColor {
final colorString =
node.attributes[CalloutBlockKeys.backgroundColor] as String?;
if (colorString == null) {
return Colors.transparent;
}
final brightness = Theme.of(context).brightness;
final bool isDarkMode = brightness == Brightness.dark;
final Color bgColor = colorString.toColor();
return isDarkMode ? bgColor.withOpacity(0.3) : bgColor;
node.attributes[CalloutBlockKeys.backgroundColor] as String;
return colorString.toColor();
}
// get the emoji of the note block from the node's attributes or default to '📌'
@@ -162,9 +162,9 @@ class _CalloutBlockComponentWidgetState
// the emoji picker button for the note
Padding(
padding: const EdgeInsets.only(
top: 6.0,
left: 2.0,
right: 2.0,
top: 8.0,
left: 4.0,
right: 4.0,
),
child: EmojiPickerButton(
key: ValueKey(
@@ -183,7 +183,7 @@ class _CalloutBlockComponentWidgetState
child: buildCalloutBlockComponent(context),
),
),
const VSpace(10),
const HSpace(8.0),
],
),
);
@@ -43,7 +43,7 @@ SelectionMenuItem codeBlockItem = SelectionMenuItem.node(
name: 'Code Block',
iconData: Icons.abc,
keywords: ['code', 'codeblock'],
nodeBuilder: (editorState) => codeBlockNode(),
nodeBuilder: (editorState, _) => codeBlockNode(),
replace: (_, node) => node.delta?.isEmpty ?? false,
);
@@ -37,7 +37,7 @@ SelectionMenuItem mathEquationItem = SelectionMenuItem.node(
name: 'MathEquation',
iconData: Icons.text_fields_rounded,
keywords: ['tex, latex, katex', 'math equation', 'formula'],
nodeBuilder: (editorState) => mathEquationNode(),
nodeBuilder: (editorState, _) => mathEquationNode(),
replace: (_, node) => node.delta?.isEmpty ?? false,
updateSelection: (editorState, path, __, ___) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
@@ -45,7 +45,7 @@ SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
iconData: Icons.generating_tokens,
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
nodeBuilder: (editorState) {
nodeBuilder: (editorState, _) {
final node = autoCompletionNode(start: editorState.selection!);
return node;
},
@@ -19,7 +19,7 @@ SelectionMenuItem outlineItem = SelectionMenuItem.node(
name: LocaleKeys.document_selectionMenu_outline.tr(),
iconData: Icons.list_alt,
keywords: ['outline', 'table of contents'],
nodeBuilder: (editorState) => outlineBlockNode(),
nodeBuilder: (editorState, _) => outlineBlockNode(),
replace: (_, node) => node.delta?.isEmpty ?? false,
);
@@ -180,14 +180,16 @@ class OutlineItemWidget extends StatelessWidget {
);
}
void updateBlockSelection(BuildContext context) {
void updateBlockSelection(BuildContext context) async {
final editorState = context.read<EditorState>();
editorState.selectionType = SelectionType.block;
editorState.selection = Selection.collapse(
node.path,
node.delta?.length ?? 0,
);
editorState.selectionType = null;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
editorState.selectionType = null;
});
}
}
@@ -49,7 +49,7 @@ SelectionMenuItem toggleListBlockItem = SelectionMenuItem.node(
name: LocaleKeys.document_plugins_toggleList.tr(),
iconData: Icons.arrow_right,
keywords: ['collapsed list', 'toggle list', 'list'],
nodeBuilder: (editorState) => toggleListBlockNode(),
nodeBuilder: (editorState, _) => toggleListBlockNode(),
replace: (_, node) => node.delta?.isEmpty ?? false,
);
@@ -22,7 +22,11 @@ class InitAppWidgetTask extends LaunchTask {
final widget = context.getIt<EntryPoint>().create(context.config);
final appearanceSetting =
await UserSettingsBackendService().getAppearanceSetting();
// If the passed-in context is not the same as the context of the
// application widget, the application widget will be rebuilt.
final app = ApplicationWidget(
key: ValueKey(context),
appearanceSetting: appearanceSetting,
child: widget,
);
@@ -1,9 +1,27 @@
import 'package:appflowy/env/env.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:async';
import 'dart:io';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:url_protocol/url_protocol.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path/path.dart' as p;
import '../startup.dart';
bool isSupabaseInitialized = false;
// ONLY supports in macOS and Windows now.
//
// If you need to update the schema, please update the following files:
// - appflowy_flutter/macos/Runner/Info.plist (macOS)
// - the callback url in Supabase dashboard
const appflowyDeepLinkSchema = 'appflowy-flutter';
const supabaseLoginCallback = '$appflowyDeepLinkSchema://login-callback';
const hiveBoxName = 'appflowy_supabase_authentication';
// Used to store the session of the supabase in case of the user switch the different folder.
Supabase? supabase;
class InitSupabaseTask extends LaunchTask {
@override
@@ -12,16 +30,70 @@ class InitSupabaseTask extends LaunchTask {
return;
}
if (isSupabaseInitialized) {
return;
// register deep link for Windows
if (Platform.isWindows) {
registerProtocolHandler(appflowyDeepLinkSchema);
}
await Supabase.initialize(
supabase?.dispose();
supabase = null;
supabase = await Supabase.initialize(
url: Env.supabaseUrl,
anonKey: Env.supabaseAnonKey,
debug: true,
// authFlowType: AuthFlowType.pkce,
debug: kDebugMode,
localStorage: const SupabaseLocalStorage(),
);
}
}
/// customize the supabase auth storage
///
/// We don't use the default one because it always save the session in the document directory.
/// When we switch to the different folder, the session still exists.
class SupabaseLocalStorage extends LocalStorage {
const SupabaseLocalStorage()
: super(
initialize: _initialize,
hasAccessToken: _hasAccessToken,
accessToken: _accessToken,
removePersistedSession: _removePersistedSession,
persistSession: _persistSession,
);
static Future<void> _initialize() async {
HiveCipher? encryptionCipher;
// customize the path for Hive
final path = await getIt<ApplicationDataStorage>().getPath();
Hive.init(p.join(path, 'supabase_auth'));
await Hive.openBox(
hiveBoxName,
encryptionCipher: encryptionCipher,
);
}
static Future<bool> _hasAccessToken() {
return Future.value(
Hive.box(hiveBoxName).containsKey(
supabasePersistSessionKey,
),
);
}
static Future<String?> _accessToken() {
return Future.value(
Hive.box(hiveBoxName).get(supabasePersistSessionKey) as String?,
);
}
static Future<void> _removePersistedSession() {
return Hive.box(hiveBoxName).delete(supabasePersistSessionKey);
}
static Future<void> _persistSession(String persistSessionString) {
return Hive.box(hiveBoxName).put(
supabasePersistSessionKey,
persistSessionString,
);
isSupabaseInitialized = true;
}
}
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/startup/tasks/prelude.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';
@@ -13,9 +14,6 @@ import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'auth_error.dart';
// can't use underscore here.
const loginCallback = 'io.appflowy.appflowy-flutter://login-callback';
class SupabaseAuthService implements AuthService {
SupabaseAuthService();
@@ -123,7 +121,7 @@ class SupabaseAuthService implements AuthService {
final response = await _auth.signInWithOAuth(
provider,
queryParams: queryParamsForProvider(provider),
redirectTo: loginCallback,
redirectTo: supabaseLoginCallback,
);
if (!response) {
completer.complete(left(AuthError.supabaseSignInWithOauthError));
@@ -171,7 +169,7 @@ class SupabaseAuthService implements AuthService {
await _auth.signInWithOtp(
email: email,
emailRedirectTo: kIsWeb ? null : loginCallback,
emailRedirectTo: kIsWeb ? null : supabaseLoginCallback,
);
return completer.future;
}
@@ -217,6 +215,22 @@ extension on String {
}
}
/// Creates a completer that listens to Supabase authentication state changes and
/// completes when a user signs in.
///
/// This function sets up a listener on Supabase's authentication state. When a user
/// signs in, it triggers the provided [onSuccess] callback with the user's `id` and
/// `email`. Once the [onSuccess] callback is executed and a response is received,
/// the completer completes with the response, and the listener is canceled.
///
/// Parameters:
/// - [onSuccess]: A callback function that's executed when a user signs in. It
/// should take in a user's `id` and `email` and return a `Future` containing either
/// a `FlowyError` or a `UserProfilePB`.
///
/// Returns:
/// A completer of type `Either<FlowyError, UserProfilePB>`. This completer completes
/// with the response from the [onSuccess] callback when a user signs in.
Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
required Future<Either<FlowyError, UserProfilePB>> Function(
String userId,
@@ -229,16 +243,15 @@ Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
subscription = auth.onAuthStateChange.listen((event) async {
final user = event.session?.user;
if (event.event != AuthChangeEvent.signedIn || user == null) {
completer.complete(left(AuthError.supabaseSignInWithOauthError));
} else {
if (event.event == AuthChangeEvent.signedIn && user != null) {
final response = await onSuccess(
user.id,
user.email ?? user.newEmail ?? '',
);
// Only cancel the subscription if the Event is signedIn.
subscription.cancel();
completer.complete(response);
}
subscription.cancel();
});
return completer;
}
@@ -0,0 +1,64 @@
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'historical_user_bloc.freezed.dart';
class HistoricalUserBloc
extends Bloc<HistoricalUserEvent, HistoricalUserState> {
HistoricalUserBloc() : super(HistoricalUserState.initial()) {
on<HistoricalUserEvent>((event, emit) async {
await event.when(
initial: () async {
await _loadHistoricalUsers();
},
didLoadHistoricalUsers: (List<HistoricalUserPB> historicalUsers) {
emit(state.copyWith(historicalUsers: historicalUsers));
},
openHistoricalUser: (HistoricalUserPB historicalUser) async {
await UserBackendService.openHistoricalUser(historicalUser);
emit(state.copyWith(openedHistoricalUser: historicalUser));
},
);
});
}
Future<void> _loadHistoricalUsers() async {
final result = await UserBackendService.loadHistoricalUsers();
result.fold(
(historicalUsers) {
historicalUsers
.retainWhere((element) => element.authType == AuthTypePB.Local);
add(HistoricalUserEvent.didLoadHistoricalUsers(historicalUsers));
},
(error) => Log.error(error),
);
}
}
@freezed
class HistoricalUserEvent with _$HistoricalUserEvent {
const factory HistoricalUserEvent.initial() = _Initial;
const factory HistoricalUserEvent.didLoadHistoricalUsers(
List<HistoricalUserPB> historicalUsers,
) = _DidLoadHistoricalUsers;
const factory HistoricalUserEvent.openHistoricalUser(
HistoricalUserPB historicalUser,
) = _OpenHistoricalUser;
}
@freezed
class HistoricalUserState with _$HistoricalUserState {
const factory HistoricalUserState({
required List<HistoricalUserPB> historicalUsers,
required HistoricalUserPB? openedHistoricalUser,
}) = _HistoricalUserState;
factory HistoricalUserState.initial() => const HistoricalUserState(
historicalUsers: [],
openedHistoricalUser: null,
);
}
@@ -18,7 +18,7 @@ class UserBackendService {
static Future<Either<FlowyError, UserProfilePB>>
getCurrentUserProfile() async {
final result = await UserEventGetUserProfile().send().then((value) {
value.fold((l) => null, (r) => Log.error(r));
value.fold((l) => null, (r) => Log.info(r));
return value;
});
return result.swap();
@@ -70,6 +70,24 @@ class UserBackendService {
return UserEventInitUser().send();
}
static Future<Either<List<HistoricalUserPB>, FlowyError>>
loadHistoricalUsers() async {
return UserEventGetHistoricalUsers().send().then(
(result) {
return result.fold(
(historicalUsers) => left(historicalUsers.items),
(error) => right(error),
);
},
);
}
static Future<Either<Unit, FlowyError>> openHistoricalUser(
HistoricalUserPB user,
) async {
return UserEventOpenHistoricalUser(user).send();
}
Future<Either<List<WorkspacePB>, FlowyError>> getWorkspaces() {
final request = WorkspaceIdPB.create();
@@ -0,0 +1,100 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class HistoricalUserList extends StatelessWidget {
final VoidCallback didOpenUser;
const HistoricalUserList({required this.didOpenUser, super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => HistoricalUserBloc()
..add(
const HistoricalUserEvent.initial(),
),
child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
builder: (context, state) {
if (state.historicalUsers.isEmpty) {
return const SizedBox.shrink();
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Opacity(
opacity: 0.6,
child: FlowyText.regular(
LocaleKeys.settings_menu_historicalUserListTooltip.tr(),
fontSize: 13,
maxLines: null,
),
),
const VSpace(6),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final user = state.historicalUsers[index];
return HistoricalUserItem(
key: ValueKey(user.userId),
user: user,
isSelected: false,
didOpenUser: didOpenUser,
);
},
itemCount: state.historicalUsers.length,
),
)
],
);
}
},
),
);
}
}
class HistoricalUserItem extends StatelessWidget {
final VoidCallback didOpenUser;
final bool isSelected;
final HistoricalUserPB user;
const HistoricalUserItem({
required this.user,
required this.isSelected,
required this.didOpenUser,
super.key,
});
@override
Widget build(BuildContext context) {
final icon = isSelected ? const FlowySvg(name: "grid/checkmark") : null;
final isDisabled = isSelected || user.authType != AuthTypePB.Local;
final outputFormat = DateFormat('MM/dd/yyyy hh:mm a');
final date =
DateTime.fromMillisecondsSinceEpoch(user.lastTime.toInt() * 1000);
final lastTime = outputFormat.format(date);
final desc = "${user.userName}\t ${user.authType}\t$lastTime";
final child = SizedBox(
height: 30,
child: FlowyButton(
disable: isDisabled,
text: FlowyText.medium(
desc,
fontSize: 12,
),
rightIcon: icon,
onTap: () {
context
.read<HistoricalUserBloc>()
.add(HistoricalUserEvent.openHistoricalUser(user));
didOpenUser();
},
),
);
return child;
}
}
@@ -13,6 +13,13 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flutter/material.dart';
const routerNameRoot = '/';
const routerNameSignUp = '/signUp';
const routerNameSignIn = '/signIn';
const routerNameSkipLogIn = '/skipLogIn';
const routerNameWelcome = '/welcome';
const routerNameHome = '/home';
class AuthRouter {
void pushForgetPasswordScreen(BuildContext context) {}
@@ -24,6 +31,7 @@ class AuthRouter {
Navigator.of(context).push(
PageRoutes.fade(
() => SignUpScreen(router: getIt<AuthRouter>()),
const RouteSettings(name: routerNameSignUp),
),
);
}
@@ -41,6 +49,7 @@ class AuthRouter {
workspaceSetting,
key: ValueKey(profile.id),
),
const RouteSettings(name: routerNameHome),
RouteDurations.slow.inMilliseconds * .001,
),
);
@@ -71,6 +80,7 @@ class SplashRoute {
await Navigator.of(context).push(
PageRoutes.fade(
() => screen,
const RouteSettings(name: routerNameWelcome),
RouteDurations.slow.inMilliseconds * .001,
),
);
@@ -97,6 +107,7 @@ class SplashRoute {
workspaceSetting,
key: ValueKey(userProfile.id),
),
const RouteSettings(name: routerNameWelcome),
RouteDurations.slow.inMilliseconds * .001,
),
);
@@ -107,6 +118,7 @@ class SplashRoute {
context,
PageRoutes.fade(
() => SignInScreen(router: getIt<AuthRouter>()),
const RouteSettings(name: routerNameSignIn),
RouteDurations.slow.inMilliseconds * .001,
),
);
@@ -120,6 +132,7 @@ class SplashRoute {
router: getIt<AuthRouter>(),
authService: getIt<AuthService>(),
),
const RouteSettings(name: routerNameSkipLogIn),
RouteDurations.slow.inMilliseconds * .001,
),
);
@@ -1,7 +1,9 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/background.dart';
@@ -71,6 +73,8 @@ class SignInForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isSubmitting = context.read<SignInBloc>().state.isSubmitting;
const indicatorMinHeight = 4.0;
return Align(
alignment: Alignment.center,
child: AuthFormContainer(
@@ -104,13 +108,30 @@ class SignInForm extends StatelessWidget {
const VSpace(10),
const ThirdPartySignInButtons(),
const VSpace(20),
// loading status
if (context.read<SignInBloc>().state.isSubmitting) ...[
const SizedBox(height: 8),
const LinearProgressIndicator(value: null),
const VSpace(20),
],
...isSubmitting
? [
const VSpace(indicatorMinHeight),
const LinearProgressIndicator(
value: null,
minHeight: indicatorMinHeight,
),
]
: [
const VSpace(indicatorMinHeight * 2.0)
], // add the same space when there's no loading status.
// ConstrainedBox(
// constraints: const BoxConstraints(maxHeight: 140),
// child: HistoricalUserList(
// didOpenUser: () async {
// await FlowyRunner.run(
// FlowyApp(),
// integrationEnv(),
// );
// },
// ),
// ),
const VSpace(20),
],
),
);
@@ -175,14 +196,49 @@ class SignInAsGuestButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RoundedTextButton(
title: LocaleKeys.signIn_loginAsGuestButtonText.tr(),
height: 48,
borderRadius: Corners.s6Border,
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
context.read<SignInBloc>().add(const SignInEvent.signedInAsGuest());
},
return BlocProvider(
create: (context) => HistoricalUserBloc()
..add(
const HistoricalUserEvent.initial(),
),
child: BlocListener<HistoricalUserBloc, HistoricalUserState>(
listenWhen: (previous, current) =>
previous.openedHistoricalUser != current.openedHistoricalUser,
listener: (context, state) async {
await FlowyRunner.run(
FlowyApp(),
integrationEnv(),
);
},
child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
builder: (context, state) {
if (state.historicalUsers.isEmpty) {
return RoundedTextButton(
title: LocaleKeys.signIn_loginAsGuestButtonText.tr(),
height: 48,
borderRadius: Corners.s6Border,
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
},
);
} else {
return RoundedTextButton(
title: LocaleKeys.signIn_continueAnonymousUser.tr(),
height: 48,
borderRadius: Corners.s6Border,
onPressed: () {
final bloc = context.read<HistoricalUserBloc>();
final user = bloc.state.historicalUsers.first;
bloc.add(HistoricalUserEvent.openHistoricalUser(user));
},
);
}
},
),
),
);
}
}
@@ -320,14 +376,16 @@ class ThirdPartySignInButton extends StatelessWidget {
}
class ThirdPartySignInButtons extends StatelessWidget {
final MainAxisAlignment mainAxisAlignment;
const ThirdPartySignInButtons({
this.mainAxisAlignment = MainAxisAlignment.center,
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: mainAxisAlignment,
children: [
ThirdPartySignInButton(
icon: 'login/google-mark',
@@ -346,6 +346,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
fontSize: FontSizes.s11,
fontColor: theme.shader3,
),
calloutBGColor: theme.hoverBG3,
caption: _getFontStyle(
fontFamily: fontFamily,
fontSize: FontSizes.s11,
@@ -35,9 +35,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
_listener.start(appsChanged: _handleAppsOrFail);
await _fetchApps(emit);
},
openPage: (e) async {
emit(state.copyWith(plugin: e.plugin));
},
createApp: (_CreateApp event) async {
final result = await _workspaceService.createApp(
name: event.name,
@@ -110,7 +107,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
@freezed
class MenuEvent with _$MenuEvent {
const factory MenuEvent.initial() = _Initial;
const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
const factory MenuEvent.createApp(String name, {String? desc, int? index}) =
_CreateApp;
const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;
@@ -8,10 +8,9 @@ import 'package:protobuf/protobuf.dart';
part 'setting_supabase_bloc.freezed.dart';
class SettingSupabaseBloc
extends Bloc<SettingSupabaseEvent, SettingSupabaseState> {
SettingSupabaseBloc() : super(SettingSupabaseState.initial()) {
on<SettingSupabaseEvent>((event, emit) async {
class SyncSettingBloc extends Bloc<SyncSettingEvent, SyncSettingState> {
SyncSettingBloc() : super(SyncSettingState.initial()) {
on<SyncSettingEvent>((event, emit) async {
await event.when(
initial: () async {
await getSupabaseConfig();
@@ -27,7 +26,7 @@ class SettingSupabaseBloc
emit(state.copyWith(config: newConfig));
}
},
didReceiveSupabseConfig: (SupabaseConfigPB config) {
didReceiveSyncConfig: (SupabaseConfigPB config) {
emit(state.copyWith(config: config));
},
);
@@ -43,7 +42,7 @@ class SettingSupabaseBloc
result.fold(
(config) {
if (!isClosed) {
add(SettingSupabaseEvent.didReceiveSupabseConfig(config));
add(SyncSettingEvent.didReceiveSyncConfig(config));
}
},
(r) => Log.error(r),
@@ -52,22 +51,22 @@ class SettingSupabaseBloc
}
@freezed
class SettingSupabaseEvent with _$SettingSupabaseEvent {
const factory SettingSupabaseEvent.initial() = _Initial;
const factory SettingSupabaseEvent.didReceiveSupabseConfig(
class SyncSettingEvent with _$SyncSettingEvent {
const factory SyncSettingEvent.initial() = _Initial;
const factory SyncSettingEvent.didReceiveSyncConfig(
SupabaseConfigPB config,
) = _DidReceiveSupabaseConfig;
const factory SettingSupabaseEvent.enableSync(bool enable) = _EnableSync;
) = _DidSyncSupabaseConfig;
const factory SyncSettingEvent.enableSync(bool enable) = _EnableSync;
}
@freezed
class SettingSupabaseState with _$SettingSupabaseState {
const factory SettingSupabaseState({
class SyncSettingState with _$SyncSettingState {
const factory SyncSettingState({
SupabaseConfigPB? config,
required Either<Unit, String> successOrFailure,
}) = _SettingSupabaseState;
}) = _SyncSettingState;
factory SettingSupabaseState.initial() => SettingSupabaseState(
factory SyncSettingState.initial() => SyncSettingState(
successOrFailure: left(unit),
);
}
@@ -13,7 +13,7 @@ enum SettingsPage {
language,
files,
user,
supabaseSetting,
syncSetting,
shortcuts,
}
@@ -1,6 +1,7 @@
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -21,7 +22,9 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
on<TabsEvent>((event, emit) async {
event.when(
selectTab: (int index) {
if (index != state.currentIndex) {
if (index != state.currentIndex &&
index >= 0 &&
index < state.pages) {
emit(state.copyWith(newIndex: index));
_setLatestOpenView();
}
@@ -31,6 +34,10 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
emit(state.closeView(pluginId));
_setLatestOpenView();
},
closeCurrentTab: () {
emit(state.closeView(state.currentPageManager.plugin.id));
_setLatestOpenView();
},
openTab: (Plugin plugin, ViewPB view) {
emit(state.openView(plugin, view));
_setLatestOpenView(view);
@@ -54,4 +61,12 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
}
}
}
/// Adds a [TabsEvent.openTab] event for the provided [ViewPB]
void openTab(ViewPB view) =>
add(TabsEvent.openTab(plugin: view.plugin(), view: view));
/// Adds a [TabsEvent.openPlugin] event for the provided [ViewPB]
void openPlugin(ViewPB view) =>
add(TabsEvent.openPlugin(plugin: view.plugin(), view: view));
}
@@ -4,6 +4,7 @@ part of 'tabs_bloc.dart';
class TabsEvent with _$TabsEvent {
const factory TabsEvent.moveTab() = _MoveTab;
const factory TabsEvent.closeTab(String pluginId) = _CloseTab;
const factory TabsEvent.closeCurrentTab() = _CloseCurrentTab;
const factory TabsEvent.selectTab(int index) = _SelectTab;
const factory TabsEvent.openTab({
required Plugin plugin,
Loaded 100 of 334 files, more files were not shown because too many files have changed in this diff. Show more