Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf98a627b9 | ||
|
|
6e5b346f25 | ||
|
|
a1b183f330 | ||
|
|
b8e3de97a5 | ||
|
|
84559afefa | ||
|
|
de3e7ca9be | ||
|
|
701384cd74 | ||
|
|
477e43b5e6 | ||
|
|
755083509f | ||
|
|
91e00d3782 | ||
|
|
3f0b3478c1 | ||
|
|
b307312a71 | ||
|
|
c0642d3ff3 | ||
|
|
99ee60a60d | ||
|
|
27ff5f07ab | ||
|
|
e2e38f72bb | ||
|
|
c1006c18c3 | ||
|
|
4e99952b0e | ||
|
|
98876b149f | ||
|
|
370f8a6558 | ||
|
|
40b710d140 | ||
|
|
8c8be741b0 | ||
|
|
96af517c65 | ||
|
|
5f8ef3856a | ||
|
|
37f521ae57 | ||
|
|
ef9891abfe | ||
|
|
9201cd6347 | ||
|
|
73df51f35f | ||
|
|
b2fb631500 | ||
|
|
7a9cb03c8e | ||
|
|
d19fa5a34a | ||
|
|
822d62b562 | ||
|
|
cb617cd9d3 | ||
|
|
7375349626 | ||
|
|
fdc105a3e8 | ||
|
|
b557f89829 | ||
|
|
0f006fa60b | ||
|
|
452974ab99 | ||
|
|
a16196eee5 | ||
|
|
31a33fdbfd | ||
|
|
bb414c3fd6 | ||
|
|
8d01d54e7f | ||
|
|
6d4cfe7316 | ||
|
|
57e3a2ce68 | ||
|
|
ac34617e51 | ||
|
|
8776ac5c36 | ||
|
|
3e1b4d81c1 |
No files matched your search
@@ -37,13 +37,6 @@ runs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Export pub environment variables and add to PATH
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- "release/*"
|
||||
paths:
|
||||
- ".github/workflows/flutter_ci.yaml"
|
||||
- ".github/actions/flutter_build/**"
|
||||
- "frontend/rust-lib/**"
|
||||
- "frontend/appflowy_flutter/**"
|
||||
- "frontend/resources/**"
|
||||
@@ -17,6 +18,7 @@ on:
|
||||
- "release/*"
|
||||
paths:
|
||||
- ".github/workflows/flutter_ci.yaml"
|
||||
- ".github/actions/flutter_build/**"
|
||||
- "frontend/rust-lib/**"
|
||||
- "frontend/appflowy_flutter/**"
|
||||
- "frontend/resources/**"
|
||||
|
||||
@@ -54,13 +54,6 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Export pub environment variable on Windows
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@@ -336,6 +329,7 @@ jobs:
|
||||
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
|
||||
LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage
|
||||
LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -412,7 +406,8 @@ jobs:
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }}
|
||||
cp -r ${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
|
||||
cd ..
|
||||
cp -r frontend/${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
|
||||
|
||||
- name: Upload Asset
|
||||
id: upload-release-asset
|
||||
@@ -422,7 +417,7 @@ jobs:
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_ZIP_NAME }}
|
||||
asset_name: ${{ env.LINUX_ZIP_NAME }}
|
||||
asset_name: ${{ env.LINUX_PACKAGE_ZIP_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Debian package
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest]
|
||||
platform: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -110,4 +110,5 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
args: "--debug"
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- platform: macos-latest
|
||||
args: "--target x86_64-apple-darwin"
|
||||
target: "macos-x86_64"
|
||||
- platform: ubuntu-latest
|
||||
- platform: ubuntu-20.04
|
||||
args: "--target x86_64-unknown-linux-gnu"
|
||||
target: "linux-x86_64"
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -140,14 +140,14 @@ jobs:
|
||||
|
||||
- name: Upload Deb package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.deb
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
|
||||
|
||||
- name: Upload AppImage package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.AppImage
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
|
||||
@@ -1,4 +1,17 @@
|
||||
# Release Notes
|
||||
## Version 0.5.3 - 03/21/2024
|
||||
### New Features
|
||||
- Added build support for 32-bit Android devices
|
||||
- Introduced filters for KanBan boards for enhanced organization
|
||||
- Introduced the new "Relations" column type in Grids
|
||||
- Expanded language support with the addition of Greek
|
||||
- Enhanced toolbar design for Mobile devices
|
||||
- Introduced a command palette feature with initial support for page search
|
||||
### Bug Fixes
|
||||
- Rectified the issue of incomplete row data in Grids when adding new rows with active filters
|
||||
- Enhanced the logic governing the filtering of number and select/multi-select fields for improved accuracy
|
||||
- Implemented UI refinements on both Desktop and Mobile platforms, enriching the overall user experience of AppFlowy
|
||||
|
||||
## Version 0.5.2 - 03/13/2024
|
||||
### Bug Fixes
|
||||
- Import csv file.
|
||||
|
||||
@@ -31,15 +31,15 @@ You are in charge of your data and customizations.
|
||||
|
||||
## User Installation
|
||||
|
||||
* [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages)
|
||||
* [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker)
|
||||
* [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source)
|
||||
- [Windows/Mac/Linux](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/mac-windows-linux-packages)
|
||||
- [Docker](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/installing-with-docker)
|
||||
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
|
||||
|
||||
## Built With
|
||||
|
||||
* [Flutter](https://flutter.dev/)
|
||||
- [Flutter](https://flutter.dev/)
|
||||
|
||||
* [Rust](https://www.rust-lang.org/)
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
|
||||
## Stay Up-to-Date
|
||||
|
||||
@@ -51,8 +51,8 @@ Please view the [documentation](https://docs.appflowy.io/docs/documentation/appf
|
||||
|
||||
## Roadmap
|
||||
|
||||
* [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap)
|
||||
* [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
|
||||
- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)
|
||||
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
|
||||
|
||||
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
|
||||
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
|
||||
@@ -63,19 +63,17 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
|
||||
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details.
|
||||
|
||||
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
|
||||
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
|
||||
|
||||
|
||||
## Translations 🌎🗺
|
||||
|
||||
[](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
|
||||
|
||||
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
|
||||
|
||||
|
||||
## Join the community to build AppFlowy together
|
||||
|
||||
<a href="https://github.com/AppFlowy-IO/AppFlowy/graphs/contributors">
|
||||
@@ -92,14 +90,14 @@ When a customer's evolving core needs are not satisfied, they either switch to a
|
||||
|
||||
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
|
||||
|
||||
* To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
|
||||
* To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
|
||||
- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
|
||||
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
|
||||
|
||||
We decided to achieve this mission by upholding the three most fundamental values:
|
||||
|
||||
* Data privacy first
|
||||
* Reliable native experience
|
||||
* Community-driven extensibility
|
||||
- Data privacy first
|
||||
- Reliable native experience
|
||||
- Community-driven extensibility
|
||||
|
||||
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
|
||||
|
||||
@@ -111,6 +109,6 @@ Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppF
|
||||
|
||||
Special thanks to these amazing projects which help power AppFlowy.IO:
|
||||
|
||||
* [flutter-quill](https://github.com/singerdmx/flutter-quill)
|
||||
* [cargo-make](https://github.com/sagiegurari/cargo-make)
|
||||
* [contrib.rocks](https://contrib.rocks)
|
||||
- [flutter-quill](https://github.com/singerdmx/flutter-quill)
|
||||
- [cargo-make](https://github.com/sagiegurari/cargo-make)
|
||||
- [contrib.rocks](https://contrib.rocks)
|
||||
Vendored
+4
-1
@@ -115,9 +115,12 @@
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"request": "attach",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
{
|
||||
// https://tauri.app/v1/guides/debugging/vs-code
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.5.2"
|
||||
APPFLOWY_VERSION = "0.5.3"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
@@ -50,7 +50,7 @@ APP_ENVIRONMENT = "local"
|
||||
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
|
||||
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
|
||||
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
|
||||
WEB_LIB_PATH= "appflowy_web/wasm-libs/af-wasm"
|
||||
WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm"
|
||||
# Test default config
|
||||
TEST_CRATE_TYPE = "cdylib"
|
||||
TEST_LIB_EXT = "dylib"
|
||||
@@ -226,9 +226,8 @@ script = ['''
|
||||
echo FEATURES: ${FLUTTER_DESKTOP_FEATURES}
|
||||
echo PRODUCT_EXT: ${PRODUCT_EXT}
|
||||
echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
|
||||
echo ${platforms}
|
||||
echo ${BUILD_ARCHS}
|
||||
echo ${BUILD_VERSION}
|
||||
echo BUILD_ARCHS: ${BUILD_ARCHS}
|
||||
echo BUILD_VERSION: ${BUILD_VERSION}
|
||||
''']
|
||||
script_runner = "@shell"
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ file(COPY
|
||||
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/arm64-v8a
|
||||
)
|
||||
|
||||
# armeabi-v7a
|
||||
file(COPY
|
||||
${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_shared.so
|
||||
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/armeabi-v7a
|
||||
)
|
||||
|
||||
# x86_64
|
||||
file(COPY
|
||||
${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/x86_64/libc++_shared.so
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# AppFlowy Test Markdown import with table
|
||||
|
||||
# Table
|
||||
|
||||
| S.No. | Column 2 |
|
||||
| --- | --- |
|
||||
| 1. | row 1 |
|
||||
| 2. | row 2 |
|
||||
| 3. | row 3 |
|
||||
| 4. | row 4 |
|
||||
| 5. | row 5 |
|
||||
@@ -1,6 +1,7 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@@ -14,8 +15,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../shared/dir.dart';
|
||||
import '../shared/mock/mock_file_picker.dart';
|
||||
import '../shared/util.dart';
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'anon_user_continue_test.dart' as anon_user_continue_test;
|
||||
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
|
||||
import 'collaborative_workspace_test.dart' as collaboration_workspace_test;
|
||||
import 'empty_test.dart' as preset_af_cloud_env_test;
|
||||
// import 'document_sync_test.dart' as document_sync_test;
|
||||
import 'user_setting_sync_test.dart' as user_sync_test;
|
||||
import 'workspace/change_name_and_icon_test.dart'
|
||||
as change_workspace_name_and_icon_test;
|
||||
import 'workspace/collaborative_workspace_test.dart'
|
||||
as collaboration_workspace_test;
|
||||
|
||||
Future<void> main() async {
|
||||
preset_af_cloud_env_test.main();
|
||||
@@ -16,5 +19,7 @@ Future<void> main() async {
|
||||
|
||||
anon_user_continue_test.main();
|
||||
|
||||
// workspace
|
||||
collaboration_workspace_test.main();
|
||||
change_workspace_name_and_icon_test.main();
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
import '../../shared/workspace.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const icon = '😄';
|
||||
const name = 'AppFlowy';
|
||||
final email = '${uuid()}@appflowy.io';
|
||||
|
||||
testWidgets('change name and icon', (tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
email: email, // use the same email to check the next test
|
||||
);
|
||||
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
var workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspace.icon, '');
|
||||
|
||||
await tester.openWorkspaceMenu();
|
||||
await tester.changeWorkspaceIcon(icon);
|
||||
await tester.changeWorkspaceName(name);
|
||||
|
||||
workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspace.icon, icon);
|
||||
expect(find.findTextInFlowyText(name), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('verify the result again after relaunching', (tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
email: email, // use the same email to check the next test
|
||||
);
|
||||
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// check the result again
|
||||
final workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspace.icon, icon);
|
||||
expect(workspaceIcon.workspace.name, name);
|
||||
});
|
||||
}
|
||||
+10
-10
@@ -23,11 +23,11 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../shared/database_test_op.dart';
|
||||
import '../shared/dir.dart';
|
||||
import '../shared/emoji.dart';
|
||||
import '../shared/mock/mock_file_picker.dart';
|
||||
import '../shared/util.dart';
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/dir.dart';
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -35,14 +35,14 @@ void main() {
|
||||
final email = '${uuid()}@appflowy.io';
|
||||
|
||||
group('collaborative workspace', () {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// combine the create and delete workspace test to reduce the time
|
||||
testWidgets('create a new workspace, open it and then delete it',
|
||||
(tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
email: email,
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -13,10 +13,10 @@ void main() {
|
||||
|
||||
group('sidebar expand test', () {
|
||||
bool isExpanded({required FolderCategoryType type}) {
|
||||
if (type == FolderCategoryType.personal) {
|
||||
if (type == FolderCategoryType.private) {
|
||||
return find
|
||||
.descendant(
|
||||
of: find.byType(PersonalFolder),
|
||||
of: find.byType(PrivateSectionFolder),
|
||||
matching: find.byType(ViewItem),
|
||||
)
|
||||
.evaluate()
|
||||
@@ -30,19 +30,19 @@ void main() {
|
||||
await tester.tapGoButton();
|
||||
|
||||
// first time is expanded
|
||||
expect(isExpanded(type: FolderCategoryType.personal), true);
|
||||
expect(isExpanded(type: FolderCategoryType.private), true);
|
||||
|
||||
// collapse the personal folder
|
||||
await tester.tapButton(
|
||||
find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
|
||||
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
|
||||
);
|
||||
expect(isExpanded(type: FolderCategoryType.personal), false);
|
||||
expect(isExpanded(type: FolderCategoryType.private), false);
|
||||
|
||||
// expand the personal folder
|
||||
await tester.tapButton(
|
||||
find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
|
||||
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
|
||||
);
|
||||
expect(isExpanded(type: FolderCategoryType.personal), true);
|
||||
expect(isExpanded(type: FolderCategoryType.private), true);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
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/sidebar/folder/_favorite_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'sidebar_expand_test.dart' as sidebar_expanded_test;
|
||||
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
|
||||
import 'sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar_test.dart' as sidebar_test;
|
||||
@@ -10,7 +9,7 @@ void startTesting() {
|
||||
|
||||
// Sidebar integration tests
|
||||
sidebar_test.main();
|
||||
sidebar_expanded_test.main();
|
||||
// sidebar_expanded_test.main();
|
||||
sidebar_favorite_test.main();
|
||||
sidebar_icon_test.main();
|
||||
}
|
||||
+40
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -44,5 +45,44 @@ void main() {
|
||||
tester.expectToSeePageName('test1');
|
||||
tester.expectToSeePageName('test2');
|
||||
});
|
||||
|
||||
testWidgets('import markdown file with table', (tester) async {
|
||||
final context = await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a getting started page
|
||||
tester.expectToSeePageName(gettingStarted);
|
||||
|
||||
await tester.tapAddViewButton();
|
||||
await tester.tapImportButton();
|
||||
|
||||
const testFileName = 'markdown_with_table.md';
|
||||
final paths = <String>[];
|
||||
final str = await rootBundle.loadString(
|
||||
'assets/test/workspaces/markdowns/$testFileName',
|
||||
);
|
||||
final path = p.join(context.applicationDataDirectory, testFileName);
|
||||
paths.add(path);
|
||||
File(path).writeAsStringSync(str);
|
||||
// mock get files
|
||||
mockPickFilePaths(
|
||||
paths: paths,
|
||||
);
|
||||
|
||||
await tester.tapTextAndMarkdownButton();
|
||||
|
||||
tester.expectToSeePageName('markdown_with_table');
|
||||
|
||||
// expect to see all content of markdown file along with table
|
||||
await tester.openPage('markdown_with_table');
|
||||
|
||||
final importedPageEditorState = tester.editor.getCurrentEditorState();
|
||||
expect(importedPageEditorState.getNodeAtPath([0])!.type,
|
||||
HeadingBlockKeys.type,);
|
||||
expect(importedPageEditorState.getNodeAtPath([2])!.type,
|
||||
HeadingBlockKeys.type,);
|
||||
expect(importedPageEditorState.getNodeAtPath([4])!.type,
|
||||
TableBlockKeys.type,);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/home.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../shared/dir.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('create new page', () {
|
||||
testWidgets('create document', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.local,
|
||||
);
|
||||
|
||||
// click the anonymousSignInButton
|
||||
final anonymousSignInButton = find.byType(SignInAnonymousButton);
|
||||
expect(anonymousSignInButton, findsOneWidget);
|
||||
await tester.tapButton(anonymousSignInButton);
|
||||
|
||||
// tap the create page button
|
||||
final createPageButton = find.byKey(mobileCreateNewPageButtonKey);
|
||||
await tester.tapButton(createPageButton);
|
||||
expect(find.byType(MobileDocumentScreen), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
extension AppFlowyWorkspace on WidgetTester {
|
||||
/// Open workspace menu
|
||||
Future<void> openWorkspaceMenu() async {
|
||||
final workspaceWrapper = find.byType(SidebarSwitchWorkspaceButton);
|
||||
expect(workspaceWrapper, findsOneWidget);
|
||||
await tapButton(workspaceWrapper);
|
||||
final workspaceMenu = find.byType(WorkspacesMenu);
|
||||
expect(workspaceMenu, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Open a workspace
|
||||
Future<void> openWorkspace(String name) async {
|
||||
final workspace = find.descendant(
|
||||
of: find.byType(WorkspaceMenuItem),
|
||||
matching: find.findTextInFlowyText(name),
|
||||
);
|
||||
expect(workspace, findsOneWidget);
|
||||
await tapButton(workspace);
|
||||
}
|
||||
|
||||
Future<void> changeWorkspaceName(String name) async {
|
||||
final moreButton = find.descendant(
|
||||
of: find.byType(WorkspaceMenuItem),
|
||||
matching: find.byType(WorkspaceMoreActionList),
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tapButton(moreButton);
|
||||
await tapButton(find.findTextInFlowyText(LocaleKeys.button_rename.tr()));
|
||||
final input = find.byType(TextFormField);
|
||||
expect(input, findsOneWidget);
|
||||
await enterText(input, name);
|
||||
await tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
}
|
||||
|
||||
Future<void> changeWorkspaceIcon(String icon) async {
|
||||
final iconButton = find.descendant(
|
||||
of: find.byType(WorkspaceMenuItem),
|
||||
matching: find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(iconButton, findsOneWidget);
|
||||
await tapButton(iconButton);
|
||||
final iconPicker = find.byType(FlowyIconPicker);
|
||||
expect(iconPicker, findsOneWidget);
|
||||
await tapButton(find.findTextInFlowyText(icon));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ extension on ViewPB {
|
||||
String get routeName {
|
||||
switch (layout) {
|
||||
case ViewLayoutPB.Document:
|
||||
return MobileEditorScreen.routeName;
|
||||
return MobileDocumentScreen.routeName;
|
||||
case ViewLayoutPB.Grid:
|
||||
return MobileGridScreen.routeName;
|
||||
case ViewLayoutPB.Calendar:
|
||||
@@ -42,8 +42,8 @@ extension on ViewPB {
|
||||
switch (layout) {
|
||||
case ViewLayoutPB.Document:
|
||||
return {
|
||||
MobileEditorScreen.viewId: id,
|
||||
MobileEditorScreen.viewTitle: name,
|
||||
MobileDocumentScreen.viewId: id,
|
||||
MobileDocumentScreen.viewTitle: name,
|
||||
};
|
||||
case ViewLayoutPB.Grid:
|
||||
return {
|
||||
|
||||
@@ -77,7 +77,7 @@ class AppBarDoneButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
onTap: onTap,
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: FlowyText(
|
||||
LocaleKeys.button_done.tr(),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
@@ -93,7 +93,7 @@ class AppBarSaveButton extends StatelessWidget {
|
||||
super.key,
|
||||
required this.onTap,
|
||||
this.enable = true,
|
||||
this.padding = const EdgeInsets.fromLTRB(12, 12, 8, 12),
|
||||
this.padding = const EdgeInsets.all(12),
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
@@ -165,7 +165,7 @@ class AppBarMoreButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
onTap: () => onTap(context),
|
||||
child: const FlowySvg(FlowySvgs.three_dots_s),
|
||||
);
|
||||
|
||||
@@ -4,7 +4,9 @@ import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/document_sync_indicator.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
@@ -70,7 +72,13 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
} else {
|
||||
body = state.data!.fold((view) {
|
||||
viewPB = view;
|
||||
actions.add(_buildAppBarMoreButton(view));
|
||||
actions.addAll([
|
||||
if (FeatureFlag.syncDocument.isOn) ...[
|
||||
DocumentSyncIndicator(view: view),
|
||||
const HSpace(8.0),
|
||||
],
|
||||
_buildAppBarMoreButton(view),
|
||||
]);
|
||||
final plugin = view.plugin(arguments: widget.arguments ?? const {})
|
||||
..init();
|
||||
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
|
||||
@@ -144,6 +152,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
Widget _buildAppBarMoreButton(ViewPB view) {
|
||||
return AppBarMoreButton(
|
||||
onTap: (context) {
|
||||
EditorNotification.exitEditing().post();
|
||||
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDragHandle: true,
|
||||
@@ -183,14 +193,12 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
|
||||
break;
|
||||
case MobileViewBottomSheetBodyAction.undo:
|
||||
context.dispatchNotification(
|
||||
const EditorNotification(type: EditorNotificationType.redo),
|
||||
);
|
||||
EditorNotification.undo().post();
|
||||
context.pop();
|
||||
break;
|
||||
case MobileViewBottomSheetBodyAction.redo:
|
||||
EditorNotification.redo().post();
|
||||
context.pop();
|
||||
context.dispatchNotification(EditorNotification.redo());
|
||||
break;
|
||||
case MobileViewBottomSheetBodyAction.helpCenter:
|
||||
// unimplemented
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomSheetCloseButton extends StatelessWidget {
|
||||
const BottomSheetCloseButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap ?? () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_bottom_sheet_close_m,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BottomSheetDoneButton extends StatelessWidget {
|
||||
const BottomSheetDoneButton({
|
||||
super.key,
|
||||
this.onDone,
|
||||
});
|
||||
|
||||
final VoidCallback? onDone;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onDone ?? () => Navigator.pop(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.button_done.tr(),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BottomSheetBackButton extends StatelessWidget {
|
||||
const BottomSheetBackButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap ?? () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_app_bar_back_s,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-17
@@ -1,6 +1,4 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -26,7 +24,7 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
left: 0,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AppBarCloseButton(
|
||||
child: BottomSheetCloseButton(
|
||||
onTap: onClose,
|
||||
),
|
||||
),
|
||||
@@ -41,19 +39,8 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
if (onDone != null)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
color: Color(0xFF00BCF0),
|
||||
),
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.button_done.tr(),
|
||||
color: Colors.white,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
onTap: onDone,
|
||||
child: BottomSheetDoneButton(
|
||||
onDone: onDone,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -195,12 +195,12 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
if (showBackButton)
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AppBarBackButton(),
|
||||
child: BottomSheetBackButton(),
|
||||
),
|
||||
if (showCloseButton)
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AppBarCloseButton(),
|
||||
child: BottomSheetCloseButton(),
|
||||
),
|
||||
Align(
|
||||
child: FlowyText(
|
||||
@@ -212,8 +212,8 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
if (showDoneButton)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: AppBarDoneButton(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: BottomSheetDoneButton(
|
||||
onDone: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+1
-1
@@ -199,7 +199,7 @@ class MobileHiddenGroup extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
group.groupName,
|
||||
context.read<BoardBloc>().generateGroupNameFromGroup(group),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileEditorScreen extends StatelessWidget {
|
||||
const MobileEditorScreen({
|
||||
class MobileDocumentScreen extends StatelessWidget {
|
||||
const MobileDocumentScreen({
|
||||
super.key,
|
||||
required this.id,
|
||||
this.title,
|
||||
|
||||
+49
-42
@@ -3,8 +3,8 @@ import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -16,22 +16,22 @@ class MobileFavoritePageFolder extends StatelessWidget {
|
||||
const MobileFavoritePageFolder({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
required this.workspaceSetting,
|
||||
required this.workspaceId,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
final String workspaceId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => SidebarRootViewsBloc()
|
||||
create: (_) => SidebarSectionsBloc()
|
||||
..add(
|
||||
SidebarRootViewsEvent.initial(
|
||||
SidebarSectionsEvent.initial(
|
||||
userProfile,
|
||||
workspaceSetting.workspaceId,
|
||||
workspaceId,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -39,45 +39,52 @@ class MobileFavoritePageFolder extends StatelessWidget {
|
||||
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) =>
|
||||
context.pushView(state.lastCreatedRootView!),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final favoriteState = context.watch<FavoriteBloc>().state;
|
||||
if (favoriteState.views.isEmpty) {
|
||||
return FlowyMobileStateContainer.info(
|
||||
emoji: '😁',
|
||||
title: LocaleKeys.favorite_noFavorite.tr(),
|
||||
description: LocaleKeys.favorite_noFavoriteHintText.tr(),
|
||||
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: (context, state) {
|
||||
context.read<FavoriteBloc>().add(
|
||||
const FavoriteEvent.initial(),
|
||||
);
|
||||
}
|
||||
return Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SlidableAutoCloseBehavior(
|
||||
child: Column(
|
||||
children: [
|
||||
MobileFavoriteFolder(
|
||||
showHeader: false,
|
||||
forceExpanded: true,
|
||||
views: favoriteState.views,
|
||||
),
|
||||
const VSpace(100.0),
|
||||
],
|
||||
},
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) =>
|
||||
context.pushView(state.lastCreatedRootView!),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final favoriteState = context.watch<FavoriteBloc>().state;
|
||||
if (favoriteState.views.isEmpty) {
|
||||
return FlowyMobileStateContainer.info(
|
||||
emoji: '😁',
|
||||
title: LocaleKeys.favorite_noFavorite.tr(),
|
||||
description: LocaleKeys.favorite_noFavoriteHintText.tr(),
|
||||
);
|
||||
}
|
||||
return Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SlidableAutoCloseBehavior(
|
||||
child: Column(
|
||||
children: [
|
||||
MobileFavoriteFolder(
|
||||
showHeader: false,
|
||||
forceExpanded: true,
|
||||
views: favoriteState.views,
|
||||
),
|
||||
const VSpace(100.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+22
-6
@@ -4,11 +4,13 @@ import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_folder.dar
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileFavoriteScreen extends StatelessWidget {
|
||||
const MobileFavoriteScreen({
|
||||
@@ -50,9 +52,23 @@ class MobileFavoriteScreen extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: MobileFavoritePage(
|
||||
userProfile: userProfile,
|
||||
workspaceSetting: workspaceSetting,
|
||||
child: BlocProvider(
|
||||
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(
|
||||
const UserWorkspaceEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
builder: (context, state) {
|
||||
return MobileFavoritePage(
|
||||
userProfile: userProfile,
|
||||
workspaceId: state.currentWorkspace?.workspaceId ??
|
||||
workspaceSetting.workspaceId,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -65,11 +81,11 @@ class MobileFavoritePage extends StatelessWidget {
|
||||
const MobileFavoritePage({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
required this.workspaceSetting,
|
||||
required this.workspaceId,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
final String workspaceId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -92,7 +108,7 @@ class MobileFavoritePage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: MobileFavoritePageFolder(
|
||||
userProfile: userProfile,
|
||||
workspaceSetting: workspaceSetting,
|
||||
workspaceId: workspaceId,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
|
||||
// Contains Public And Private Sections
|
||||
class MobileFolders extends StatelessWidget {
|
||||
const MobileFolders({
|
||||
super.key,
|
||||
required this.user,
|
||||
required this.workspaceSetting,
|
||||
required this.workspaceId,
|
||||
required this.showFavorite,
|
||||
});
|
||||
|
||||
final UserProfilePB user;
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
final String workspaceId;
|
||||
final bool showFavorite;
|
||||
|
||||
@override
|
||||
@@ -26,11 +30,11 @@ class MobileFolders extends StatelessWidget {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => SidebarRootViewsBloc()
|
||||
create: (_) => SidebarSectionsBloc()
|
||||
..add(
|
||||
SidebarRootViewsEvent.initial(
|
||||
SidebarSectionsEvent.initial(
|
||||
user,
|
||||
workspaceSetting.workspaceId,
|
||||
workspaceId,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -38,24 +42,51 @@ class MobileFolders extends StatelessWidget {
|
||||
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) =>
|
||||
context.pushView(state.lastCreatedRootView!),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final menuState = context.watch<SidebarRootViewsBloc>().state;
|
||||
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: (context, state) {
|
||||
context.read<SidebarSectionsBloc>().add(
|
||||
SidebarSectionsEvent.initial(
|
||||
user,
|
||||
state.currentWorkspace?.workspaceId ?? workspaceId,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) {
|
||||
final lastCreatedRootView = state.lastCreatedRootView;
|
||||
if (lastCreatedRootView != null) {
|
||||
context.pushView(lastCreatedRootView);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isCollaborativeWorkspace =
|
||||
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: Column(
|
||||
children: [
|
||||
MobilePersonalFolder(
|
||||
views: menuState.views,
|
||||
),
|
||||
...isCollaborativeWorkspace
|
||||
? [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_public.tr(),
|
||||
categoryType: FolderCategoryType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
const VSpace(8.0),
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_private.tr(),
|
||||
categoryType: FolderCategoryType.private,
|
||||
views: state.section.privateViews,
|
||||
),
|
||||
]
|
||||
: [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_personal.tr(),
|
||||
categoryType: FolderCategoryType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
],
|
||||
const VSpace(8.0),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
@@ -15,6 +16,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -82,55 +84,70 @@ class MobileHomePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: Platform.isAndroid ? 8.0 : 0.0,
|
||||
),
|
||||
child: MobileHomePageHeader(
|
||||
userProfile: userProfile,
|
||||
),
|
||||
return BlocProvider(
|
||||
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(
|
||||
const UserWorkspaceEvent.initial(),
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
// Folder
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Recent files
|
||||
const MobileRecentFolder(),
|
||||
|
||||
// Folders
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: MobileFolders(
|
||||
user: userProfile,
|
||||
workspaceSetting: workspaceSetting,
|
||||
showFavorite: false,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: _TrashButton(),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: Platform.isAndroid ? 8.0 : 0.0,
|
||||
),
|
||||
child: MobileHomePageHeader(
|
||||
userProfile: userProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Divider(),
|
||||
|
||||
// Folder
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Recent files
|
||||
const MobileRecentFolder(),
|
||||
|
||||
// Folders
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: MobileFolders(
|
||||
user: userProfile,
|
||||
workspaceId:
|
||||
state.currentWorkspace?.workspaceId ??
|
||||
workspaceSetting.workspaceId,
|
||||
showFavorite: false,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: _TrashButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+158
-22
@@ -1,12 +1,16 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -14,7 +18,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class MobileHomePageHeader extends StatelessWidget {
|
||||
const MobileHomePageHeader({super.key, required this.userProfile});
|
||||
const MobileHomePageHeader({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@@ -25,33 +32,22 @@ class MobileHomePageHeader extends StatelessWidget {
|
||||
..add(const SettingsUserEvent.initial()),
|
||||
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
|
||||
builder: (context, state) {
|
||||
final userIcon = state.userProfile.iconUrl;
|
||||
final isCollaborativeWorkspace =
|
||||
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 52),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_UserIcon(userIcon: userIcon),
|
||||
const HSpace(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const FlowyText.medium('AppFlowy', fontSize: 18),
|
||||
const VSpace(4),
|
||||
FlowyText.regular(
|
||||
userProfile.email.isNotEmpty
|
||||
? state.userProfile.email
|
||||
: state.userProfile.name,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: isCollaborativeWorkspace
|
||||
? _MobileWorkspace(userProfile: userProfile)
|
||||
: _MobileUser(userProfile: userProfile),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
context.push(MobileHomeSettingPage.routeName),
|
||||
onPressed: () => context.push(
|
||||
MobileHomeSettingPage.routeName,
|
||||
),
|
||||
icon: const FlowySvg(FlowySvgs.m_setting_m),
|
||||
),
|
||||
],
|
||||
@@ -63,6 +59,146 @@ class MobileHomePageHeader extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileUser extends StatelessWidget {
|
||||
const _MobileUser({
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userIcon = userProfile.iconUrl;
|
||||
return Row(
|
||||
children: [
|
||||
_UserIcon(userIcon: userIcon),
|
||||
const HSpace(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const FlowyText.medium('AppFlowy', fontSize: 18),
|
||||
const VSpace(4),
|
||||
FlowyText.regular(
|
||||
userProfile.email.isNotEmpty
|
||||
? userProfile.email
|
||||
: userProfile.name,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileWorkspace extends StatelessWidget {
|
||||
const _MobileWorkspace({
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, state) {
|
||||
final currentWorkspace = state.currentWorkspace;
|
||||
final workspaces = state.workspaces;
|
||||
if (currentWorkspace == null || workspaces.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showSwitchWorkspacesBottomSheet(
|
||||
context,
|
||||
currentWorkspace,
|
||||
workspaces,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(2.0),
|
||||
SizedBox.square(
|
||||
dimension: 34.0,
|
||||
child: WorkspaceIcon(
|
||||
workspace: currentWorkspace,
|
||||
iconSize: 26,
|
||||
enableEdit: false,
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
currentWorkspace.name,
|
||||
fontSize: 16.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const HSpace(4.0),
|
||||
const FlowySvg(FlowySvgs.list_dropdown_s),
|
||||
],
|
||||
),
|
||||
FlowyText.medium(
|
||||
userProfile.email.isNotEmpty
|
||||
? userProfile.email
|
||||
: userProfile.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showSwitchWorkspacesBottomSheet(
|
||||
BuildContext context,
|
||||
UserWorkspacePB currentWorkspace,
|
||||
List<UserWorkspacePB> workspaces,
|
||||
) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDivider: false,
|
||||
showHeader: true,
|
||||
showDragHandle: true,
|
||||
title: LocaleKeys.workspace_menuTitle.tr(),
|
||||
builder: (_) {
|
||||
return MobileWorkspaceMenu(
|
||||
userProfile: userProfile,
|
||||
currentWorkspace: currentWorkspace,
|
||||
workspaces: workspaces,
|
||||
onWorkspaceSelected: (workspace) {
|
||||
context.pop();
|
||||
|
||||
if (workspace == currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspace.workspaceId,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserIcon extends StatelessWidget {
|
||||
const _UserIcon({
|
||||
required this.userIcon,
|
||||
|
||||
+32
-24
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
|
||||
import 'package:appflowy/workspace/application/recent/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -22,31 +23,38 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
|
||||
..add(
|
||||
const RecentViewsEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
|
||||
builder: (context, state) {
|
||||
final ids = <String>{};
|
||||
|
||||
List<ViewPB> recentViews = state.views.reversed.toList();
|
||||
recentViews.retainWhere((element) => ids.add(element.id));
|
||||
|
||||
// only keep the first 20 items.
|
||||
recentViews = recentViews.take(20).toList();
|
||||
|
||||
if (recentViews.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_RecentViews(
|
||||
key: ValueKey(recentViews),
|
||||
// the recent views are in reverse order
|
||||
recentViews: recentViews,
|
||||
),
|
||||
const VSpace(12.0),
|
||||
],
|
||||
);
|
||||
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: (context, state) {
|
||||
context.read<RecentViewsBloc>().add(
|
||||
const RecentViewsEvent.fetchRecentViews(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
|
||||
builder: (context, state) {
|
||||
final ids = <String>{};
|
||||
|
||||
List<ViewPB> recentViews = state.views.reversed.toList();
|
||||
recentViews.retainWhere((element) => ids.add(element.id));
|
||||
|
||||
// only keep the first 20 items.
|
||||
recentViews = recentViews.take(20).toList();
|
||||
|
||||
if (recentViews.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_RecentViews(
|
||||
key: ValueKey(recentViews),
|
||||
// the recent views are in reverse order
|
||||
recentViews: recentViews,
|
||||
),
|
||||
const VSpace(12.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+28
-10
@@ -1,26 +1,33 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobilePersonalFolder extends StatelessWidget {
|
||||
const MobilePersonalFolder({
|
||||
class MobileSectionFolder extends StatelessWidget {
|
||||
const MobileSectionFolder({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.views,
|
||||
required this.categoryType,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final List<ViewPB> views;
|
||||
final FolderCategoryType categoryType;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<FolderBloc>(
|
||||
create: (context) => FolderBloc(type: FolderCategoryType.personal)
|
||||
create: (context) => FolderBloc(type: categoryType)
|
||||
..add(
|
||||
const FolderEvent.initial(),
|
||||
),
|
||||
@@ -28,14 +35,25 @@ class MobilePersonalFolder extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
MobilePersonalFolderHeader(
|
||||
MobileSectionFolderHeader(
|
||||
title: title,
|
||||
isExpanded: context.read<FolderBloc>().state.isExpanded,
|
||||
onPressed: () => context
|
||||
.read<FolderBloc>()
|
||||
.add(const FolderEvent.expandOrUnExpand()),
|
||||
onAdded: () => context.read<FolderBloc>().add(
|
||||
const FolderEvent.expandOrUnExpand(isExpanded: true),
|
||||
),
|
||||
onAdded: () {
|
||||
context.read<SidebarSectionsBloc>().add(
|
||||
SidebarSectionsEvent.createRootViewInSection(
|
||||
name:
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
index: 0,
|
||||
viewSection: categoryType.toViewSectionPB,
|
||||
),
|
||||
);
|
||||
context.read<FolderBloc>().add(
|
||||
const FolderEvent.expandOrUnExpand(isExpanded: true),
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(8.0),
|
||||
const Divider(
|
||||
@@ -45,9 +63,9 @@ class MobilePersonalFolder extends StatelessWidget {
|
||||
...views.map(
|
||||
(view) => MobileViewItem(
|
||||
key: ValueKey(
|
||||
'${FolderCategoryType.personal.name} ${view.id}',
|
||||
'${FolderCategoryType.private.name} ${view.id}',
|
||||
),
|
||||
categoryType: FolderCategoryType.personal,
|
||||
categoryType: categoryType,
|
||||
isFirstChild: view.id == views.first.id,
|
||||
view: view,
|
||||
level: 0,
|
||||
+13
-19
@@ -1,30 +1,30 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobilePersonalFolderHeader extends StatefulWidget {
|
||||
const MobilePersonalFolderHeader({
|
||||
@visibleForTesting
|
||||
const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
|
||||
|
||||
class MobileSectionFolderHeader extends StatefulWidget {
|
||||
const MobileSectionFolderHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onPressed,
|
||||
required this.onAdded,
|
||||
required this.isExpanded,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback onAdded;
|
||||
final bool isExpanded;
|
||||
|
||||
@override
|
||||
State<MobilePersonalFolderHeader> createState() =>
|
||||
_MobilePersonalFolderHeaderState();
|
||||
State<MobileSectionFolderHeader> createState() =>
|
||||
_MobileSectionFolderHeaderState();
|
||||
}
|
||||
|
||||
class _MobilePersonalFolderHeaderState
|
||||
extends State<MobilePersonalFolderHeader> {
|
||||
class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
|
||||
double _turns = 0;
|
||||
|
||||
@override
|
||||
@@ -35,7 +35,7 @@ class _MobilePersonalFolderHeaderState
|
||||
Expanded(
|
||||
child: FlowyButton(
|
||||
text: FlowyText.semibold(
|
||||
LocaleKeys.sideBar_personal.tr(),
|
||||
widget.title,
|
||||
fontSize: 20.0,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
@@ -58,6 +58,7 @@ class _MobilePersonalFolderHeaderState
|
||||
),
|
||||
),
|
||||
FlowyIconButton(
|
||||
key: mobileCreateNewPageButtonKey,
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
iconPadding: const EdgeInsets.all(2),
|
||||
height: iconSize,
|
||||
@@ -66,14 +67,7 @@ class _MobilePersonalFolderHeaderState
|
||||
FlowySvgs.add_s,
|
||||
size: Size.square(iconSize),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<SidebarRootViewsBloc>().add(
|
||||
SidebarRootViewsEvent.createRootView(
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
index: 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
onPressed: widget.onAdded,
|
||||
),
|
||||
],
|
||||
);
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
// Only works on mobile.
|
||||
class MobileWorkspaceMenu extends StatelessWidget {
|
||||
const MobileWorkspaceMenu({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
required this.currentWorkspace,
|
||||
required this.workspaces,
|
||||
required this.onWorkspaceSelected,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final UserWorkspacePB currentWorkspace;
|
||||
final List<UserWorkspacePB> workspaces;
|
||||
final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> children = [];
|
||||
for (var i = 0; i < workspaces.length; i++) {
|
||||
final workspace = workspaces[i];
|
||||
children.add(
|
||||
_WorkspaceMenuItem(
|
||||
userProfile: userProfile,
|
||||
workspace: workspace,
|
||||
showTopBorder: i == 0,
|
||||
currentWorkspace: currentWorkspace,
|
||||
onWorkspaceSelected: onWorkspaceSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WorkspaceMenuItem extends StatelessWidget {
|
||||
const _WorkspaceMenuItem({
|
||||
required this.userProfile,
|
||||
required this.workspace,
|
||||
required this.showTopBorder,
|
||||
required this.currentWorkspace,
|
||||
required this.onWorkspaceSelected,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final UserWorkspacePB workspace;
|
||||
final bool showTopBorder;
|
||||
final UserWorkspacePB currentWorkspace;
|
||||
final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => WorkspaceMemberBloc(
|
||||
userProfile: userProfile,
|
||||
workspace: workspace,
|
||||
)..add(const WorkspaceMemberEvent.initial()),
|
||||
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||
builder: (context, state) {
|
||||
final members = state.members;
|
||||
return FlowyOptionTile.text(
|
||||
content: Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FlowyText(
|
||||
workspace.name,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
FlowyText(
|
||||
state.isLoading
|
||||
? ''
|
||||
: LocaleKeys.settings_appearance_members_membersCount
|
||||
.plural(
|
||||
members.length,
|
||||
),
|
||||
fontSize: 10.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
height: 60,
|
||||
showTopBorder: showTopBorder,
|
||||
leftIcon: WorkspaceIcon(
|
||||
enableEdit: false,
|
||||
iconSize: 26,
|
||||
workspace: workspace,
|
||||
),
|
||||
trailing: workspace.workspaceId == currentWorkspace.workspaceId
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_blue_check_s,
|
||||
blendMode: null,
|
||||
)
|
||||
: null,
|
||||
onTap: () => onWorkspaceSelected(workspace),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notifi
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';
|
||||
@@ -80,15 +80,15 @@ class _NotificationScreenContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => SidebarRootViewsBloc()
|
||||
create: (_) => SidebarSectionsBloc()
|
||||
..add(
|
||||
SidebarRootViewsEvent.initial(
|
||||
SidebarSectionsEvent.initial(
|
||||
userProfile,
|
||||
workspaceSetting.workspaceId,
|
||||
),
|
||||
),
|
||||
child: BlocBuilder<SidebarRootViewsBloc, SidebarRootViewState>(
|
||||
builder: (context, menuState) =>
|
||||
child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
builder: (context, sectionState) =>
|
||||
BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
||||
builder: (context, filterState) =>
|
||||
BlocBuilder<ReminderBloc, ReminderState>(
|
||||
@@ -122,7 +122,7 @@ class _NotificationScreenContent extends StatelessWidget {
|
||||
NotificationsView(
|
||||
shownReminders: pastReminders,
|
||||
reminderBloc: reminderBloc,
|
||||
views: menuState.views,
|
||||
views: sectionState.section.publicViews,
|
||||
onAction: _onAction,
|
||||
onDelete: _onDelete,
|
||||
onReadChanged: _onReadChanged,
|
||||
@@ -134,7 +134,7 @@ class _NotificationScreenContent extends StatelessWidget {
|
||||
NotificationsView(
|
||||
shownReminders: upcomingReminders,
|
||||
reminderBloc: reminderBloc,
|
||||
views: menuState.views,
|
||||
views: sectionState.section.publicViews,
|
||||
isUpcoming: true,
|
||||
onAction: _onAction,
|
||||
),
|
||||
|
||||
@@ -406,6 +406,10 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
ViewEvent.createView(
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
layout,
|
||||
section:
|
||||
widget.categoryType != FolderCategoryType.favorite
|
||||
? widget.categoryType.toViewSectionPB
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+14
-2
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
@@ -32,6 +34,16 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
|
||||
),
|
||||
if (kDebugMode)
|
||||
MobileSettingItem(
|
||||
name: 'Feature Flags',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () {
|
||||
context.push(FeatureFlagScreen.routeName);
|
||||
},
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_version.tr(),
|
||||
trailing: FlowyText(
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ class MobileSettingItem extends StatelessWidget {
|
||||
trailing: trailing,
|
||||
onTap: onTap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.only(left: 8.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,26 +36,31 @@ class FlowyOptionTile extends StatelessWidget {
|
||||
this.content,
|
||||
this.backgroundColor,
|
||||
this.fontFamily,
|
||||
this.height,
|
||||
});
|
||||
|
||||
factory FlowyOptionTile.text({
|
||||
required String text,
|
||||
String? text,
|
||||
Widget? content,
|
||||
Color? textColor,
|
||||
bool showTopBorder = true,
|
||||
bool showBottomBorder = true,
|
||||
Widget? leftIcon,
|
||||
Widget? trailing,
|
||||
VoidCallback? onTap,
|
||||
double? height,
|
||||
}) {
|
||||
return FlowyOptionTile._(
|
||||
type: FlowyOptionTileType.text,
|
||||
text: text,
|
||||
content: content,
|
||||
textColor: textColor,
|
||||
onTap: onTap,
|
||||
showTopBorder: showTopBorder,
|
||||
showBottomBorder: showBottomBorder,
|
||||
leading: leftIcon,
|
||||
trailing: trailing,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,6 +179,8 @@ class FlowyOptionTile extends StatelessWidget {
|
||||
final Color? backgroundColor;
|
||||
final String? fontFamily;
|
||||
|
||||
final double? height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final leadingWidget = _buildLeading();
|
||||
@@ -182,16 +189,19 @@ class FlowyOptionTile extends StatelessWidget {
|
||||
color: backgroundColor,
|
||||
showTopBorder: showTopBorder,
|
||||
showBottomBorder: showBottomBorder,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (leadingWidget != null) leadingWidget,
|
||||
if (content != null) content!,
|
||||
if (content == null) _buildText(),
|
||||
if (content == null) _buildTextField(),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (leadingWidget != null) leadingWidget,
|
||||
if (content != null) content!,
|
||||
if (content == null) _buildText(),
|
||||
if (content == null) _buildTextField(),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+80
-24
@@ -1,10 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@@ -35,12 +39,14 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
didUpdateCell: (RelationCellDataPB? cellData) async {
|
||||
if (cellData == null || cellData.rowIds.isEmpty) {
|
||||
if (cellData == null ||
|
||||
cellData.rowIds.isEmpty ||
|
||||
state.relatedDatabaseMeta == null) {
|
||||
emit(state.copyWith(rows: const []));
|
||||
return;
|
||||
}
|
||||
final payload = RepeatedRowIdPB(
|
||||
databaseId: state.relatedDatabaseId,
|
||||
databaseId: state.relatedDatabaseMeta!.databaseId,
|
||||
rowIds: cellData.rowIds,
|
||||
);
|
||||
final result =
|
||||
@@ -54,8 +60,16 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
|
||||
);
|
||||
emit(state.copyWith(rows: rows));
|
||||
},
|
||||
didUpdateRelationDatabaseId: (databaseId) {
|
||||
emit(state.copyWith(relatedDatabaseId: databaseId));
|
||||
didUpdateRelationTypeOption: (typeOption) async {
|
||||
if (typeOption.databaseId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final meta = await _loadDatabaseMeta(typeOption.databaseId);
|
||||
emit(state.copyWith(relatedDatabaseMeta: meta));
|
||||
_loadCellData();
|
||||
},
|
||||
selectDatabaseId: (databaseId) async {
|
||||
await _updateTypeOption(databaseId);
|
||||
},
|
||||
selectRow: (rowId) async {
|
||||
await _handleSelectRow(rowId);
|
||||
@@ -73,29 +87,30 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
|
||||
}
|
||||
},
|
||||
onCellFieldChanged: (field) {
|
||||
if (!isClosed) {
|
||||
// hack: SingleFieldListener receives notification before
|
||||
// FieldController's copy is updated.
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
// hack: SingleFieldListener receives notification before
|
||||
// FieldController's copy is updated.
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
if (!isClosed) {
|
||||
final RelationTypeOptionPB typeOption =
|
||||
cellController.getTypeOption(RelationTypeOptionDataParser());
|
||||
add(
|
||||
RelationCellEvent.didUpdateRelationDatabaseId(
|
||||
typeOption.databaseId,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
add(RelationCellEvent.didUpdateRelationTypeOption(typeOption));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _init() {
|
||||
final RelationTypeOptionPB typeOption =
|
||||
final typeOption =
|
||||
cellController.getTypeOption(RelationTypeOptionDataParser());
|
||||
add(RelationCellEvent.didUpdateRelationDatabaseId(typeOption.databaseId));
|
||||
add(RelationCellEvent.didUpdateRelationTypeOption(typeOption));
|
||||
}
|
||||
|
||||
void _loadCellData() {
|
||||
final cellData = cellController.getCellData();
|
||||
add(RelationCellEvent.didUpdateCell(cellData));
|
||||
if (!isClosed) {
|
||||
add(RelationCellEvent.didUpdateCell(cellData));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSelectRow(String rowId) async {
|
||||
@@ -115,25 +130,66 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
|
||||
final result = await DatabaseEventUpdateRelationCell(payload).send();
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
Future<DatabaseMeta?> _loadDatabaseMeta(String databaseId) async {
|
||||
final getDatabaseResult = await DatabaseEventGetDatabases().send();
|
||||
final databaseMeta = getDatabaseResult.fold<DatabaseMetaPB?>(
|
||||
(s) => s.items.firstWhereOrNull(
|
||||
(metaPB) => metaPB.databaseId == databaseId,
|
||||
),
|
||||
(f) => null,
|
||||
);
|
||||
if (databaseMeta != null) {
|
||||
final result =
|
||||
await ViewBackendService.getView(databaseMeta.inlineViewId);
|
||||
return result.fold(
|
||||
(s) => DatabaseMeta(
|
||||
databaseId: databaseId,
|
||||
inlineViewId: databaseMeta.inlineViewId,
|
||||
databaseName: s.name,
|
||||
),
|
||||
(f) => null,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _updateTypeOption(String databaseId) async {
|
||||
final newDateTypeOption = RelationTypeOptionPB(
|
||||
databaseId: databaseId,
|
||||
);
|
||||
|
||||
final result = await FieldBackendService.updateFieldTypeOption(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldInfo.id,
|
||||
typeOptionData: newDateTypeOption.writeToBuffer(),
|
||||
);
|
||||
result.fold((s) => null, (err) => Log.error(err));
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RelationCellEvent with _$RelationCellEvent {
|
||||
const factory RelationCellEvent.didUpdateRelationDatabaseId(
|
||||
String databaseId,
|
||||
) = _DidUpdateRelationDatabaseId;
|
||||
const factory RelationCellEvent.didUpdateRelationTypeOption(
|
||||
RelationTypeOptionPB typeOption,
|
||||
) = _DidUpdateRelationTypeOption;
|
||||
const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) =
|
||||
_DidUpdateCell;
|
||||
const factory RelationCellEvent.selectDatabaseId(
|
||||
String databaseId,
|
||||
) = _SelectDatabaseId;
|
||||
const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RelationCellState with _$RelationCellState {
|
||||
const factory RelationCellState({
|
||||
required String relatedDatabaseId,
|
||||
required DatabaseMeta? relatedDatabaseMeta,
|
||||
required List<RelatedRowDataPB> rows,
|
||||
}) = _RelationCellState;
|
||||
|
||||
factory RelationCellState.initial() =>
|
||||
const RelationCellState(relatedDatabaseId: "", rows: []);
|
||||
factory RelationCellState.initial() => const RelationCellState(
|
||||
relatedDatabaseMeta: null,
|
||||
rows: [],
|
||||
);
|
||||
}
|
||||
+38
-6
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
@@ -29,15 +30,17 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
void _dispatch() {
|
||||
on<URLCellEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
await event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
didReceiveCellUpdate: (cellData) async {
|
||||
final content = cellData?.content ?? "";
|
||||
final isValid = await isUrlValid(content);
|
||||
emit(
|
||||
state.copyWith(
|
||||
content: cellData?.content ?? "",
|
||||
url: cellData?.url ?? "",
|
||||
content: content,
|
||||
isValid: isValid,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -58,6 +61,35 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isUrlValid(String content) async {
|
||||
if (content.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// check protocol is provided
|
||||
const linkPrefix = [
|
||||
'http://',
|
||||
'https://',
|
||||
'file://',
|
||||
'ftp://',
|
||||
'ftps://',
|
||||
'mailto:',
|
||||
];
|
||||
final shouldAddScheme =
|
||||
!linkPrefix.any((pattern) => content.startsWith(pattern));
|
||||
final url = shouldAddScheme ? 'http://$content' : content;
|
||||
|
||||
// get hostname and check validity
|
||||
final uri = Uri.parse(url);
|
||||
final hostName = uri.host;
|
||||
await InternetAddress.lookup(hostName);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -72,14 +104,14 @@ class URLCellEvent with _$URLCellEvent {
|
||||
class URLCellState with _$URLCellState {
|
||||
const factory URLCellState({
|
||||
required String content,
|
||||
required String url,
|
||||
required bool isValid,
|
||||
}) = _URLCellState;
|
||||
|
||||
factory URLCellState.initial(URLCellController context) {
|
||||
final cellData = context.getCellData();
|
||||
return URLCellState(
|
||||
content: cellData?.content ?? "",
|
||||
url: cellData?.url ?? "",
|
||||
isValid: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class FieldInfo with _$FieldInfo {
|
||||
}
|
||||
|
||||
bool get canCreateFilter {
|
||||
if (hasFilter) {
|
||||
if (isGroupField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ class FieldInfo with _$FieldInfo {
|
||||
case FieldType.RichText:
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.Checklist:
|
||||
case FieldType.URL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'relation_type_option_cubit.freezed.dart';
|
||||
|
||||
class RelationDatabaseListCubit extends Cubit<RelationDatabaseListState> {
|
||||
RelationDatabaseListCubit() : super(RelationDatabaseListState.initial()) {
|
||||
_loadDatabaseMetas();
|
||||
}
|
||||
|
||||
void _loadDatabaseMetas() async {
|
||||
final getDatabaseResult = await DatabaseEventGetDatabases().send();
|
||||
final metaPBs = getDatabaseResult.fold<List<DatabaseMetaPB>>(
|
||||
(s) => s.items,
|
||||
(f) => [],
|
||||
);
|
||||
final futures = metaPBs.map((meta) {
|
||||
return ViewBackendService.getView(meta.inlineViewId).then(
|
||||
(result) => result.fold(
|
||||
(s) => DatabaseMeta(
|
||||
databaseId: meta.databaseId,
|
||||
inlineViewId: meta.inlineViewId,
|
||||
databaseName: s.name,
|
||||
),
|
||||
(f) => null,
|
||||
),
|
||||
);
|
||||
});
|
||||
final databaseMetas = await Future.wait(futures);
|
||||
emit(
|
||||
RelationDatabaseListState(
|
||||
databaseMetas: databaseMetas.nonNulls.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DatabaseMeta with _$DatabaseMeta {
|
||||
factory DatabaseMeta({
|
||||
/// id of the database
|
||||
required String databaseId,
|
||||
|
||||
/// id of the inline view
|
||||
required String inlineViewId,
|
||||
|
||||
/// name of the database, currently identical to the name of the inline view
|
||||
required String databaseName,
|
||||
}) = _DatabaseMeta;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RelationDatabaseListState with _$RelationDatabaseListState {
|
||||
factory RelationDatabaseListState({
|
||||
required List<DatabaseMeta> databaseMetas,
|
||||
}) = _RelationDatabaseListState;
|
||||
|
||||
factory RelationDatabaseListState.initial() =>
|
||||
RelationDatabaseListState(databaseMetas: []);
|
||||
}
|
||||
@@ -11,9 +11,11 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
||||
|
||||
import '../../application/database_controller.dart';
|
||||
@@ -383,21 +385,28 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
groupList.insert(insertGroups.index, group);
|
||||
add(BoardEvent.didReceiveGroups(groupList));
|
||||
},
|
||||
onUpdateGroup: (updatedGroups) {
|
||||
onUpdateGroup: (updatedGroups) async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// workaround: update group most of the time gets called before fields in
|
||||
// field controller are updated. For single and multi-select group
|
||||
// renames, this is required before generating the new group name.
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
for (final group in updatedGroups) {
|
||||
// see if the column is already in the board
|
||||
|
||||
final index = groupList.indexWhere((g) => g.groupId == group.groupId);
|
||||
if (index == -1) continue;
|
||||
if (index == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final columnController =
|
||||
boardController.getGroupController(group.groupId);
|
||||
if (columnController != null) {
|
||||
// remove the group or update its name
|
||||
columnController.updateGroupName(group.groupName);
|
||||
columnController.updateGroupName(generateGroupNameFromGroup(group));
|
||||
if (!group.isVisible) {
|
||||
boardController.removeGroup(group.groupId);
|
||||
}
|
||||
@@ -491,7 +500,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
AppFlowyGroupData _initializeGroupData(GroupPB group) {
|
||||
return AppFlowyGroupData(
|
||||
id: group.groupId,
|
||||
name: group.groupName,
|
||||
name: generateGroupNameFromGroup(group),
|
||||
items: _buildGroupItems(group),
|
||||
customData: GroupData(
|
||||
group: group,
|
||||
@@ -499,6 +508,72 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String generateGroupNameFromGroup(GroupPB group) {
|
||||
final field = fieldController.getField(group.fieldId);
|
||||
if (field == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// if the group is the default group, then
|
||||
if (group.isDefault) {
|
||||
return "No ${field.name}";
|
||||
}
|
||||
|
||||
switch (field.fieldType) {
|
||||
case FieldType.SingleSelect:
|
||||
final options =
|
||||
SingleSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)
|
||||
.options;
|
||||
final option =
|
||||
options.firstWhereOrNull((option) => option.id == group.groupId);
|
||||
return option == null ? "" : option.name;
|
||||
case FieldType.MultiSelect:
|
||||
final options =
|
||||
MultiSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)
|
||||
.options;
|
||||
final option =
|
||||
options.firstWhereOrNull((option) => option.id == group.groupId);
|
||||
return option == null ? "" : option.name;
|
||||
case FieldType.Checkbox:
|
||||
return group.groupId;
|
||||
case FieldType.URL:
|
||||
return group.groupId;
|
||||
case FieldType.DateTime:
|
||||
// Assume DateCondition::Relative as there isn't an option for this
|
||||
// right now.
|
||||
final dateFormat = DateFormat("y/MM/dd");
|
||||
try {
|
||||
final targetDateTime = dateFormat.parseLoose(group.groupId);
|
||||
final targetDateTimeDay = DateTime(
|
||||
targetDateTime.year,
|
||||
targetDateTime.month,
|
||||
targetDateTime.day,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final nowDay = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
);
|
||||
final diff = targetDateTimeDay.difference(nowDay).inDays;
|
||||
return switch (diff) {
|
||||
0 => "Today",
|
||||
-1 => "Yesterday",
|
||||
1 => "Tomorrow",
|
||||
-7 => "Last 7 days",
|
||||
2 => "Next 7 days",
|
||||
-30 => "Last 30 days",
|
||||
8 => "Next 30 days",
|
||||
_ => DateFormat("MMM y").format(targetDateTimeDay)
|
||||
};
|
||||
} on FormatException {
|
||||
return "";
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+3
-15
@@ -31,23 +31,11 @@ class GroupController {
|
||||
final GroupControllerDelegate delegate;
|
||||
final void Function(GroupPB group) onGroupChanged;
|
||||
|
||||
RowMetaPB? rowAtIndex(int index) {
|
||||
if (index < group.rows.length) {
|
||||
return group.rows[index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
RowMetaPB? rowAtIndex(int index) => group.rows.elementAtOrNull(index);
|
||||
|
||||
RowMetaPB? firstRow() {
|
||||
if (group.rows.isEmpty) return null;
|
||||
return group.rows.first;
|
||||
}
|
||||
RowMetaPB? firstRow() => group.rows.firstOrNull;
|
||||
|
||||
RowMetaPB? lastRow() {
|
||||
if (group.rows.isEmpty) return null;
|
||||
return group.rows.last;
|
||||
}
|
||||
RowMetaPB? lastRow() => group.rows.lastOrNull;
|
||||
|
||||
void startListening() {
|
||||
_listener.start(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
|
||||
import 'package:flutter/material.dart' hide Card;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -34,6 +36,8 @@ import 'toolbar/board_setting_bar.dart';
|
||||
import 'widgets/board_hidden_groups.dart';
|
||||
|
||||
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
final _toggleExtension = ToggleExtensionNotifier();
|
||||
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
@@ -49,14 +53,27 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
BoardSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
toggleExtension: _toggleExtension,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget settingBarExtension(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
) =>
|
||||
const SizedBox.shrink();
|
||||
) {
|
||||
return DatabaseViewSettingExtension(
|
||||
key: _makeValueKey(controller),
|
||||
viewId: controller.viewId,
|
||||
databaseController: controller,
|
||||
toggleExtension: _toggleExtension,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_toggleExtension.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) =>
|
||||
ValueKey(controller.viewId);
|
||||
|
||||
+36
-7
@@ -1,24 +1,53 @@
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class BoardSettingBar extends StatelessWidget {
|
||||
const BoardSettingBar({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
required this.toggleExtension,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final ToggleExtensionNotifier toggleExtension;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SettingButton(databaseController: databaseController),
|
||||
],
|
||||
return BlocProvider<DatabaseFilterMenuBloc>(
|
||||
create: (context) => DatabaseFilterMenuBloc(
|
||||
viewId: databaseController.viewId,
|
||||
fieldController: databaseController.fieldController,
|
||||
)..add(const DatabaseFilterMenuEvent.initial()),
|
||||
child: BlocListener<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
|
||||
listenWhen: (p, c) => p.isVisible != c.isVisible,
|
||||
listener: (context, state) => toggleExtension.toggle(),
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: databaseController.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),
|
||||
SettingButton(
|
||||
databaseController: databaseController,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -269,7 +269,7 @@ class HiddenGroupButtonContent extends StatelessWidget {
|
||||
),
|
||||
const HSpace(4),
|
||||
FlowyText.medium(
|
||||
group.groupName,
|
||||
bloc.generateGroupNameFromGroup(group),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const HSpace(6),
|
||||
@@ -369,7 +369,7 @@ class HiddenGroupPopupItemList extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: FlowyText.medium(
|
||||
group.groupName,
|
||||
context.read<BoardBloc>().generateGroupNameFromGroup(group),
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
class DatabaseBackendService {
|
||||
static Future<FlowyResult<List<DatabaseDescriptionPB>, FlowyError>>
|
||||
static Future<FlowyResult<List<DatabaseMetaPB>, FlowyError>>
|
||||
getAllDatabases() {
|
||||
return DatabaseEventGetDatabases().send().then((result) {
|
||||
return result.fold(
|
||||
|
||||
@@ -62,6 +62,19 @@ class FieldBackendService {
|
||||
return DatabaseEventDeleteField(payload).send();
|
||||
}
|
||||
|
||||
// Clear all data of all cells in a Field
|
||||
static Future<FlowyResult<void, FlowyError>> clearField({
|
||||
required String viewId,
|
||||
required String fieldId,
|
||||
}) {
|
||||
final payload = ClearFieldPayloadPB(
|
||||
viewId: viewId,
|
||||
fieldId: fieldId,
|
||||
);
|
||||
|
||||
return DatabaseEventClearField(payload).send();
|
||||
}
|
||||
|
||||
/// Duplicate a field
|
||||
static Future<FlowyResult<void, FlowyError>> duplicateField({
|
||||
required String viewId,
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:fixnum/fixnum.dart' as $fixnum;
|
||||
@@ -109,25 +100,21 @@ class FilterBackendService {
|
||||
int? timestamp,
|
||||
}) {
|
||||
assert(
|
||||
[
|
||||
FieldType.DateTime,
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
].contains(fieldType),
|
||||
fieldType == FieldType.DateTime ||
|
||||
fieldType == FieldType.LastEditedTime ||
|
||||
fieldType == FieldType.CreatedTime,
|
||||
);
|
||||
|
||||
final filter = DateFilterPB();
|
||||
|
||||
if (timestamp != null) {
|
||||
filter.timestamp = $fixnum.Int64(timestamp);
|
||||
} else {
|
||||
if (start != null && end != null) {
|
||||
filter.start = $fixnum.Int64(start);
|
||||
filter.end = $fixnum.Int64(end);
|
||||
} else {
|
||||
throw Exception(
|
||||
"Start and end should not be null if the timestamp is null",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (start != null) {
|
||||
filter.start = $fixnum.Int64(start);
|
||||
}
|
||||
if (end != null) {
|
||||
filter.end = $fixnum.Int64(end);
|
||||
}
|
||||
|
||||
return filterId == null
|
||||
|
||||
+19
-18
@@ -8,11 +8,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'filter_menu_bloc.freezed.dart';
|
||||
|
||||
class GridFilterMenuBloc
|
||||
extends Bloc<GridFilterMenuEvent, GridFilterMenuState> {
|
||||
GridFilterMenuBloc({required this.viewId, required this.fieldController})
|
||||
class DatabaseFilterMenuBloc
|
||||
extends Bloc<DatabaseFilterMenuEvent, DatabaseFilterMenuState> {
|
||||
DatabaseFilterMenuBloc({required this.viewId, required this.fieldController})
|
||||
: super(
|
||||
GridFilterMenuState.initial(
|
||||
DatabaseFilterMenuState.initial(
|
||||
viewId,
|
||||
fieldController.filterInfos,
|
||||
fieldController.fieldInfos,
|
||||
@@ -27,7 +27,7 @@ class GridFilterMenuBloc
|
||||
void Function(List<FieldInfo>)? _onFieldFn;
|
||||
|
||||
void _dispatch() {
|
||||
on<GridFilterMenuEvent>(
|
||||
on<DatabaseFilterMenuEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
@@ -55,11 +55,11 @@ class GridFilterMenuBloc
|
||||
|
||||
void _startListening() {
|
||||
_onFilterFn = (filters) {
|
||||
add(GridFilterMenuEvent.didReceiveFilters(filters));
|
||||
add(DatabaseFilterMenuEvent.didReceiveFilters(filters));
|
||||
};
|
||||
|
||||
_onFieldFn = (fields) {
|
||||
add(GridFilterMenuEvent.didReceiveFields(fields));
|
||||
add(DatabaseFilterMenuEvent.didReceiveFields(fields));
|
||||
};
|
||||
|
||||
fieldController.addListener(
|
||||
@@ -87,32 +87,33 @@ class GridFilterMenuBloc
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridFilterMenuEvent with _$GridFilterMenuEvent {
|
||||
const factory GridFilterMenuEvent.initial() = _Initial;
|
||||
const factory GridFilterMenuEvent.didReceiveFilters(
|
||||
class DatabaseFilterMenuEvent with _$DatabaseFilterMenuEvent {
|
||||
const factory DatabaseFilterMenuEvent.initial() = _Initial;
|
||||
const factory DatabaseFilterMenuEvent.didReceiveFilters(
|
||||
List<FilterInfo> filters,
|
||||
) = _DidReceiveFilters;
|
||||
const factory GridFilterMenuEvent.didReceiveFields(List<FieldInfo> fields) =
|
||||
_DidReceiveFields;
|
||||
const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
|
||||
const factory DatabaseFilterMenuEvent.didReceiveFields(
|
||||
List<FieldInfo> fields,
|
||||
) = _DidReceiveFields;
|
||||
const factory DatabaseFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridFilterMenuState with _$GridFilterMenuState {
|
||||
const factory GridFilterMenuState({
|
||||
class DatabaseFilterMenuState with _$DatabaseFilterMenuState {
|
||||
const factory DatabaseFilterMenuState({
|
||||
required String viewId,
|
||||
required List<FilterInfo> filters,
|
||||
required List<FieldInfo> fields,
|
||||
required List<FieldInfo> creatableFields,
|
||||
required bool isVisible,
|
||||
}) = _GridFilterMenuState;
|
||||
}) = _DatabaseFilterMenuState;
|
||||
|
||||
factory GridFilterMenuState.initial(
|
||||
factory DatabaseFilterMenuState.initial(
|
||||
String viewId,
|
||||
List<FilterInfo> filterInfos,
|
||||
List<FieldInfo> fields,
|
||||
) =>
|
||||
GridFilterMenuState(
|
||||
DatabaseFilterMenuState(
|
||||
viewId: viewId,
|
||||
filters: filterInfos,
|
||||
fields: fields,
|
||||
|
||||
+30
-16
@@ -3,8 +3,7 @@ import 'dart:async';
|
||||
import 'package:appflowy/plugins/database/domain/filter_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/filter_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/util.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';
|
||||
|
||||
@@ -12,7 +11,7 @@ part 'text_filter_editor_bloc.freezed.dart';
|
||||
|
||||
class TextFilterEditorBloc
|
||||
extends Bloc<TextFilterEditorEvent, TextFilterEditorState> {
|
||||
TextFilterEditorBloc({required this.filterInfo})
|
||||
TextFilterEditorBloc({required this.filterInfo, required this.fieldType})
|
||||
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
@@ -23,6 +22,7 @@ class TextFilterEditorBloc
|
||||
}
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
final FieldType fieldType;
|
||||
final FilterBackendService _filterBackendSvc;
|
||||
final FilterListener _listener;
|
||||
|
||||
@@ -34,20 +34,34 @@ class TextFilterEditorBloc
|
||||
_startListening();
|
||||
},
|
||||
updateCondition: (TextFilterConditionPB condition) {
|
||||
_filterBackendSvc.insertTextFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
content: state.filter.content,
|
||||
);
|
||||
fieldType == FieldType.RichText
|
||||
? _filterBackendSvc.insertTextFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
content: state.filter.content,
|
||||
)
|
||||
: _filterBackendSvc.insertURLFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
content: state.filter.content,
|
||||
);
|
||||
},
|
||||
updateContent: (content) {
|
||||
_filterBackendSvc.insertTextFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
content: content,
|
||||
);
|
||||
updateContent: (String content) {
|
||||
fieldType == FieldType.RichText
|
||||
? _filterBackendSvc.insertTextFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
content: content,
|
||||
)
|
||||
: _filterBackendSvc.insertURLFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
content: content,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_filterBackendSvc.deleteFilter(
|
||||
|
||||
+2
-2
@@ -96,9 +96,9 @@ extension SelectOptionFilterConditionPBExtension
|
||||
SelectOptionFilterConditionPB.OptionIsNot =>
|
||||
LocaleKeys.grid_selectOptionFilter_isNot.tr(),
|
||||
SelectOptionFilterConditionPB.OptionContains =>
|
||||
LocaleKeys.grid_selectOptionFilter_isNot.tr(),
|
||||
LocaleKeys.grid_selectOptionFilter_contains.tr(),
|
||||
SelectOptionFilterConditionPB.OptionDoesNotContain =>
|
||||
LocaleKeys.grid_selectOptionFilter_isNot.tr(),
|
||||
LocaleKeys.grid_selectOptionFilter_doesNotContain.tr(),
|
||||
SelectOptionFilterConditionPB.OptionIsEmpty =>
|
||||
LocaleKeys.grid_selectOptionFilter_isEmpty.tr(),
|
||||
SelectOptionFilterConditionPB.OptionIsNotEmpty =>
|
||||
|
||||
+32
-52
@@ -1,59 +1,44 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../application/filter/text_filter_editor_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
import 'choicechip.dart';
|
||||
|
||||
class TextFilterChoicechip extends StatefulWidget {
|
||||
class TextFilterChoicechip extends StatelessWidget {
|
||||
const TextFilterChoicechip({required this.filterInfo, super.key});
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
|
||||
@override
|
||||
State<TextFilterChoicechip> createState() => _TextFilterChoicechipState();
|
||||
}
|
||||
|
||||
class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
|
||||
late TextFilterEditorBloc bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo)
|
||||
..add(const TextFilterEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
return BlocProvider(
|
||||
create: (_) => TextFilterEditorBloc(
|
||||
filterInfo: filterInfo,
|
||||
fieldType: FieldType.RichText,
|
||||
)..add(const TextFilterEditorEvent.initial()),
|
||||
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
||||
builder: (blocContext, state) {
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
controller: PopoverController(),
|
||||
constraints: BoxConstraints.loose(const Size(200, 76)),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
popupBuilder: (BuildContext context) {
|
||||
return TextFilterEditor(bloc: bloc);
|
||||
popupBuilder: (popoverContext) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<TextFilterEditorBloc>(),
|
||||
child: const TextFilterEditor(),
|
||||
);
|
||||
},
|
||||
child: ChoiceChipButton(
|
||||
filterInfo: widget.filterInfo,
|
||||
filterInfo: filterInfo,
|
||||
filterDesc: _makeFilterDesc(state),
|
||||
),
|
||||
);
|
||||
@@ -78,9 +63,7 @@ class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
|
||||
}
|
||||
|
||||
class TextFilterEditor extends StatefulWidget {
|
||||
const TextFilterEditor({required this.bloc, super.key});
|
||||
|
||||
final TextFilterEditorBloc bloc;
|
||||
const TextFilterEditor({super.key});
|
||||
|
||||
@override
|
||||
State<TextFilterEditor> createState() => _TextFilterEditorState();
|
||||
@@ -91,26 +74,23 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: widget.bloc,
|
||||
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [
|
||||
_buildFilterPanel(context, state),
|
||||
];
|
||||
return BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [
|
||||
_buildFilterPanel(context, state),
|
||||
];
|
||||
|
||||
if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
|
||||
state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) {
|
||||
children.add(const VSpace(4));
|
||||
children.add(_buildFilterTextField(context, state));
|
||||
}
|
||||
if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
|
||||
state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) {
|
||||
children.add(const VSpace(4));
|
||||
children.add(_buildFilterTextField(context, state));
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
child: IntrinsicHeight(child: Column(children: children)),
|
||||
);
|
||||
},
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
child: IntrinsicHeight(child: Column(children: children)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+47
-3
@@ -1,14 +1,58 @@
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../filter_info.dart';
|
||||
import 'choicechip.dart';
|
||||
|
||||
class URLFilterChoicechip extends StatelessWidget {
|
||||
const URLFilterChoicechip({required this.filterInfo, super.key});
|
||||
class URLFilterChoiceChip extends StatelessWidget {
|
||||
const URLFilterChoiceChip({required this.filterInfo, super.key});
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChoiceChipButton(filterInfo: filterInfo);
|
||||
return BlocProvider(
|
||||
create: (_) => TextFilterEditorBloc(
|
||||
filterInfo: filterInfo,
|
||||
fieldType: FieldType.URL,
|
||||
),
|
||||
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(200, 76)),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
popupBuilder: (popoverContext) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<TextFilterEditorBloc>(),
|
||||
child: const TextFilterEditor(),
|
||||
);
|
||||
},
|
||||
child: ChoiceChipButton(
|
||||
filterInfo: filterInfo,
|
||||
filterDesc: _makeFilterDesc(state),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _makeFilterDesc(TextFilterEditorState state) {
|
||||
String filterDesc = state.filter.condition.choicechipPrefix;
|
||||
if (state.filter.condition == TextFilterConditionPB.TextIsEmpty ||
|
||||
state.filter.condition == TextFilterConditionPB.TextIsNotEmpty) {
|
||||
return filterDesc;
|
||||
}
|
||||
|
||||
if (state.filter.content.isNotEmpty) {
|
||||
filterDesc += " ${state.filter.content}";
|
||||
}
|
||||
|
||||
return filterDesc;
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -21,13 +21,17 @@ class FilterInfo {
|
||||
String get fieldId => filter.data.fieldId;
|
||||
|
||||
DateFilterPB? dateFilter() {
|
||||
return filter.data.fieldType == FieldType.DateTime
|
||||
final fieldType = filter.data.fieldType;
|
||||
return fieldType == FieldType.DateTime ||
|
||||
fieldType == FieldType.CreatedTime ||
|
||||
fieldType == FieldType.LastEditedTime
|
||||
? DateFilterPB.fromBuffer(filter.data.data)
|
||||
: null;
|
||||
}
|
||||
|
||||
TextFilterPB? textFilter() {
|
||||
return filter.data.fieldType == FieldType.RichText
|
||||
return filter.data.fieldType == FieldType.RichText ||
|
||||
filter.data.fieldType == FieldType.URL
|
||||
? TextFilterPB.fromBuffer(filter.data.data)
|
||||
: null;
|
||||
}
|
||||
|
||||
+5
-5
@@ -23,14 +23,14 @@ class FilterMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<GridFilterMenuBloc>(
|
||||
create: (context) => GridFilterMenuBloc(
|
||||
return BlocProvider<DatabaseFilterMenuBloc>(
|
||||
create: (context) => DatabaseFilterMenuBloc(
|
||||
viewId: fieldController.viewId,
|
||||
fieldController: fieldController,
|
||||
)..add(
|
||||
const GridFilterMenuEvent.initial(),
|
||||
const DatabaseFilterMenuEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
|
||||
child: BlocBuilder<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [];
|
||||
children.addAll(
|
||||
@@ -115,7 +115,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: child,
|
||||
popupBuilder: (BuildContext context) {
|
||||
final bloc = buildContext.read<GridFilterMenuBloc>();
|
||||
final bloc = buildContext.read<DatabaseFilterMenuBloc>();
|
||||
return GridCreateFilterList(
|
||||
viewId: widget.viewId,
|
||||
fieldController: bloc.fieldController,
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ class FilterMenuItem extends StatelessWidget {
|
||||
FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.SingleSelect =>
|
||||
SelectOptionFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.URL => URLFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
|
||||
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
|
||||
_ => const SizedBox(),
|
||||
};
|
||||
|
||||
+23
@@ -104,6 +104,8 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
_actionCell(FieldAction.duplicate),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
_actionCell(FieldAction.clearData),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
_actionCell(FieldAction.delete),
|
||||
],
|
||||
).padding(all: 8.0),
|
||||
@@ -195,6 +197,7 @@ enum FieldAction {
|
||||
insertRight,
|
||||
toggleVisibility,
|
||||
duplicate,
|
||||
clearData,
|
||||
delete;
|
||||
|
||||
Widget icon(FieldInfo fieldInfo, Color? color) {
|
||||
@@ -213,6 +216,8 @@ enum FieldAction {
|
||||
}
|
||||
case FieldAction.duplicate:
|
||||
svgData = FlowySvgs.copy_s;
|
||||
case FieldAction.clearData:
|
||||
svgData = FlowySvgs.reload_s;
|
||||
case FieldAction.delete:
|
||||
svgData = FlowySvgs.delete_s;
|
||||
}
|
||||
@@ -241,6 +246,8 @@ enum FieldAction {
|
||||
}
|
||||
case FieldAction.duplicate:
|
||||
return LocaleKeys.grid_field_duplicate.tr();
|
||||
case FieldAction.clearData:
|
||||
return LocaleKeys.grid_field_clear.tr();
|
||||
case FieldAction.delete:
|
||||
return LocaleKeys.grid_field_delete.tr();
|
||||
}
|
||||
@@ -273,6 +280,22 @@ enum FieldAction {
|
||||
fieldId: fieldInfo.id,
|
||||
);
|
||||
break;
|
||||
case FieldAction.clearData:
|
||||
NavigatorAlertDialog(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 250,
|
||||
maxHeight: 260,
|
||||
),
|
||||
title: LocaleKeys.grid_field_clearFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
FieldBackendService.clearField(
|
||||
viewId: viewId,
|
||||
fieldId: fieldInfo.id,
|
||||
);
|
||||
},
|
||||
).show(context);
|
||||
PopoverContainer.of(context).close();
|
||||
break;
|
||||
case FieldAction.delete:
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ const List<FieldType> _supportedFieldTypes = [
|
||||
FieldType.URL,
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Relation,
|
||||
];
|
||||
|
||||
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
||||
|
||||
+93
-93
@@ -1,15 +1,15 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
import 'builder.dart';
|
||||
@@ -27,55 +27,76 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
}) {
|
||||
final typeOption = _parseTypeOptionData(field.typeOptionData);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 14, right: 8),
|
||||
height: GridSize.popoverItemHeight,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
AppFlowyPopover(
|
||||
mutex: popoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(6, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText(
|
||||
typeOption.databaseId.isEmpty
|
||||
? LocaleKeys.grid_relation_relatedDatabasePlaceholder.tr()
|
||||
: typeOption.databaseId,
|
||||
color: typeOption.databaseId.isEmpty
|
||||
? Theme.of(context).hintColor
|
||||
: null,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
return BlocProvider(
|
||||
create: (_) => RelationDatabaseListCubit(),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 14, right: 8),
|
||||
height: GridSize.popoverItemHeight,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||
),
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
return _DatabaseList(
|
||||
onSelectDatabase: (newDatabaseId) {
|
||||
final newTypeOption = _updateTypeOption(
|
||||
typeOption: typeOption,
|
||||
databaseId: newDatabaseId,
|
||||
);
|
||||
onTypeOptionUpdated(newTypeOption.writeToBuffer());
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
currentDatabaseId:
|
||||
typeOption.databaseId.isEmpty ? null : typeOption.databaseId,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
AppFlowyPopover(
|
||||
mutex: popoverMutex,
|
||||
triggerActions:
|
||||
PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(6, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: BlocBuilder<RelationDatabaseListCubit,
|
||||
RelationDatabaseListState>(
|
||||
builder: (context, state) {
|
||||
final databaseMeta =
|
||||
state.databaseMetas.firstWhereOrNull(
|
||||
(meta) => meta.databaseId == typeOption.databaseId,
|
||||
);
|
||||
return FlowyText(
|
||||
databaseMeta == null
|
||||
? LocaleKeys
|
||||
.grid_relation_relatedDatabasePlaceholder
|
||||
.tr()
|
||||
: databaseMeta.databaseName,
|
||||
color: databaseMeta == null
|
||||
? Theme.of(context).hintColor
|
||||
: null,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
),
|
||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||
),
|
||||
),
|
||||
popupBuilder: (popoverContext) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<RelationDatabaseListCubit>(),
|
||||
child: _DatabaseList(
|
||||
onSelectDatabase: (newDatabaseId) {
|
||||
final newTypeOption = _updateTypeOption(
|
||||
typeOption: typeOption,
|
||||
databaseId: newDatabaseId,
|
||||
);
|
||||
onTypeOptionUpdated(newTypeOption.writeToBuffer());
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
currentDatabaseId: typeOption.databaseId,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,65 +115,44 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
}
|
||||
}
|
||||
|
||||
class _DatabaseList extends StatefulWidget {
|
||||
class _DatabaseList extends StatelessWidget {
|
||||
const _DatabaseList({
|
||||
required this.onSelectDatabase,
|
||||
required this.currentDatabaseId,
|
||||
});
|
||||
|
||||
final String? currentDatabaseId;
|
||||
final String currentDatabaseId;
|
||||
final void Function(String databaseId) onSelectDatabase;
|
||||
|
||||
@override
|
||||
State<_DatabaseList> createState() => _DatabaseListState();
|
||||
}
|
||||
|
||||
class _DatabaseListState extends State<_DatabaseList> {
|
||||
late Future<FlowyResult<RepeatedDatabaseDescriptionPB, FlowyError>> future;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
future = DatabaseEventGetDatabases().send();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, snapshot) {
|
||||
final data = snapshot.data;
|
||||
if (!snapshot.hasData ||
|
||||
snapshot.connectionState != ConnectionState.done ||
|
||||
data!.isFailure()) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final databaseIds = data
|
||||
.fold<List<DatabaseDescriptionPB>>((l) => l.items, (r) => [])
|
||||
.map((databaseDescription) {
|
||||
final databaseId = databaseDescription.databaseId;
|
||||
return FlowyButton(
|
||||
onTap: () => widget.onSelectDatabase(databaseId),
|
||||
text: FlowyText.medium(
|
||||
databaseId,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
return BlocBuilder<RelationDatabaseListCubit, RelationDatabaseListState>(
|
||||
builder: (context, state) {
|
||||
final children = state.databaseMetas.map((meta) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
onTap: () => onSelectDatabase(meta.databaseId),
|
||||
text: FlowyText.medium(
|
||||
meta.databaseName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
rightIcon: meta.databaseId == currentDatabaseId
|
||||
? const FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
rightIcon: databaseId == widget.currentDatabaseId
|
||||
? FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
separatorBuilder: (_, __) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
itemCount: databaseIds.length,
|
||||
itemBuilder: (context, index) => databaseIds[index],
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+5
-5
@@ -23,7 +23,7 @@ class _FilterButtonState extends State<FilterButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
|
||||
return BlocBuilder<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
|
||||
builder: (context, state) {
|
||||
final textColor = state.filters.isEmpty
|
||||
? AFThemeExtension.of(context).textColor
|
||||
@@ -41,11 +41,11 @@ class _FilterButtonState extends State<FilterButton> {
|
||||
padding: GridSize.toolbarSettingButtonInsets,
|
||||
radius: Corners.s4Border,
|
||||
onPressed: () {
|
||||
final bloc = context.read<GridFilterMenuBloc>();
|
||||
final bloc = context.read<DatabaseFilterMenuBloc>();
|
||||
if (bloc.state.filters.isEmpty) {
|
||||
_popoverController.show();
|
||||
} else {
|
||||
bloc.add(const GridFilterMenuEvent.toggleMenu());
|
||||
bloc.add(const DatabaseFilterMenuEvent.toggleMenu());
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -63,14 +63,14 @@ class _FilterButtonState extends State<FilterButton> {
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: child,
|
||||
popupBuilder: (BuildContext context) {
|
||||
final bloc = buildContext.read<GridFilterMenuBloc>();
|
||||
final bloc = buildContext.read<DatabaseFilterMenuBloc>();
|
||||
return GridCreateFilterList(
|
||||
viewId: bloc.viewId,
|
||||
fieldController: bloc.fieldController,
|
||||
onClosed: () => _popoverController.close(),
|
||||
onCreateFilter: () {
|
||||
if (!bloc.state.isVisible) {
|
||||
bloc.add(const GridFilterMenuEvent.toggleMenu());
|
||||
bloc.add(const DatabaseFilterMenuEvent.toggleMenu());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
+4
-4
@@ -24,11 +24,11 @@ class GridSettingBar extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<GridFilterMenuBloc>(
|
||||
create: (context) => GridFilterMenuBloc(
|
||||
BlocProvider<DatabaseFilterMenuBloc>(
|
||||
create: (context) => DatabaseFilterMenuBloc(
|
||||
viewId: controller.viewId,
|
||||
fieldController: controller.fieldController,
|
||||
)..add(const GridFilterMenuEvent.initial()),
|
||||
)..add(const DatabaseFilterMenuEvent.initial()),
|
||||
),
|
||||
BlocProvider<SortEditorBloc>(
|
||||
create: (context) => SortEditorBloc(
|
||||
@@ -37,7 +37,7 @@ class GridSettingBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
|
||||
child: BlocListener<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
|
||||
listenWhen: (p, c) => p.isVisible != c.isVisible,
|
||||
listener: (context, state) => toggleExtension.toggle(),
|
||||
child: ValueListenableBuilder<bool>(
|
||||
|
||||
+13
-8
@@ -1,8 +1,9 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -52,15 +53,19 @@ class _RelationCellState extends State<RelationCardCell> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final children = state.rows
|
||||
.map(
|
||||
(row) => FlowyText.medium(
|
||||
row.name,
|
||||
final children = state.rows.map(
|
||||
(row) {
|
||||
final isEmpty = row.name.isEmpty;
|
||||
return Text(
|
||||
isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,
|
||||
style: widget.style.textStyle.copyWith(
|
||||
color: isEmpty ? Theme.of(context).hintColor : null,
|
||||
decoration: TextDecoration.underline,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
|
||||
+14
-14
@@ -1,10 +1,12 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/relation.dart';
|
||||
@@ -29,10 +31,6 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin {
|
||||
value: bloc,
|
||||
child: RelationCellEditor(
|
||||
selectedRowIds: state.rows.map((row) => row.rowId).toList(),
|
||||
databaseId: state.relatedDatabaseId,
|
||||
onSelectRow: (rowId) {
|
||||
bloc.add(RelationCellEvent.selectRow(rowId));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -42,15 +40,17 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin {
|
||||
child: Wrap(
|
||||
runSpacing: 4.0,
|
||||
spacing: 4.0,
|
||||
children: state.rows
|
||||
.map(
|
||||
(row) => FlowyText.medium(
|
||||
row.name,
|
||||
decoration: TextDecoration.underline,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
children: state.rows.map(
|
||||
(row) {
|
||||
final isEmpty = row.name.isEmpty;
|
||||
return FlowyText.medium(
|
||||
isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,
|
||||
color: isEmpty ? Theme.of(context).hintColor : null,
|
||||
decoration: TextDecoration.underline,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+34
-24
@@ -1,9 +1,12 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/relation.dart';
|
||||
@@ -26,36 +29,43 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin {
|
||||
popupBuilder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<RelationCellBloc, RelationCellState>(
|
||||
builder: (context, state) => RelationCellEditor(
|
||||
selectedRowIds: state.rows.map((row) => row.rowId).toList(),
|
||||
databaseId: state.relatedDatabaseId,
|
||||
onSelectRow: (rowId) {
|
||||
context
|
||||
.read<RelationCellBloc>()
|
||||
.add(RelationCellEvent.selectRow(rowId));
|
||||
},
|
||||
),
|
||||
child: RelationCellEditor(
|
||||
selectedRowIds: state.rows.map((row) => row.rowId).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
child: Wrap(
|
||||
runSpacing: 4.0,
|
||||
spacing: 4.0,
|
||||
children: state.rows
|
||||
.map(
|
||||
(row) => FlowyText.medium(
|
||||
row.name,
|
||||
decoration: TextDecoration.underline,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
child: state.rows.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildRows(context, state.rows),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlaceholder(BuildContext context) {
|
||||
return FlowyText(
|
||||
LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRows(BuildContext context, List<RelatedRowDataPB> rows) {
|
||||
return Wrap(
|
||||
runSpacing: 4.0,
|
||||
spacing: 4.0,
|
||||
children: rows.map(
|
||||
(row) {
|
||||
final isEmpty = row.name.isEmpty;
|
||||
return FlowyText.medium(
|
||||
isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,
|
||||
color: isEmpty ? Theme.of(context).hintColor : null,
|
||||
decoration: TextDecoration.underline,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+21
-13
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@@ -24,9 +25,6 @@ import '../desktop_row_detail/desktop_row_detail_url_cell.dart';
|
||||
import '../mobile_grid/mobile_grid_url_cell.dart';
|
||||
import '../mobile_row_detail/mobile_row_detail_url_cell.dart';
|
||||
|
||||
const regexUrl =
|
||||
r"[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:._\+-~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:_\+.~#?&\/\/=]*)";
|
||||
|
||||
abstract class IEditableURLCellSkin {
|
||||
const IEditableURLCellSkin();
|
||||
|
||||
@@ -134,8 +132,8 @@ class _GridURLCellState extends GridEditableTextCell<EditableURLCell> {
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted &&
|
||||
!cellBloc.isClosed &&
|
||||
cellBloc.state.content != _textEditingController.text.trim()) {
|
||||
cellBloc.add(URLCellEvent.updateURL(_textEditingController.text.trim()));
|
||||
cellBloc.state.content != _textEditingController.text) {
|
||||
cellBloc.add(URLCellEvent.updateURL(_textEditingController.text));
|
||||
}
|
||||
return super.focusChanged();
|
||||
}
|
||||
@@ -169,6 +167,9 @@ class MobileURLEditor extends StatelessWidget {
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
keyboardType: TextInputType.url,
|
||||
hintTextConstraints: const BoxConstraints(maxHeight: 52),
|
||||
error: context.watch<URLCellBloc>().state.isValid
|
||||
? null
|
||||
: const SizedBox.shrink(),
|
||||
onChanged: (_) {
|
||||
if (textEditingController.value.composing.isCollapsed) {
|
||||
context
|
||||
@@ -212,8 +213,11 @@ class MobileURLEditor extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void openUrlCellLink(String content) {
|
||||
if (RegExp(regexUrl).hasMatch(content)) {
|
||||
void openUrlCellLink(String content) async {
|
||||
String url = "";
|
||||
|
||||
try {
|
||||
// check protocol is provided
|
||||
const linkPrefix = [
|
||||
'http://',
|
||||
'https://',
|
||||
@@ -224,11 +228,15 @@ void openUrlCellLink(String content) {
|
||||
];
|
||||
final shouldAddScheme =
|
||||
!linkPrefix.any((pattern) => content.startsWith(pattern));
|
||||
final url = shouldAddScheme ? 'https://$content' : content;
|
||||
afLaunchUrlString(url);
|
||||
} else {
|
||||
afLaunchUrlString(
|
||||
"https://www.google.com/search?q=${Uri.encodeComponent(content)}",
|
||||
);
|
||||
url = shouldAddScheme ? 'http://$content' : content;
|
||||
|
||||
// get hostname and check validity
|
||||
final uri = Uri.parse(url);
|
||||
final hostName = uri.host;
|
||||
await InternetAddress.lookup(hostName);
|
||||
} catch (_) {
|
||||
url = "https://www.google.com/search?q=${Uri.encodeComponent(content)}";
|
||||
} finally {
|
||||
await afLaunchUrlString(url);
|
||||
}
|
||||
}
|
||||
+1
@@ -51,6 +51,7 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDragHandle: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: MobileURLEditor(
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin {
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
showDragHandle: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
builder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
|
||||
+82
-35
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -13,38 +14,24 @@ import '../../application/cell/bloc/relation_row_search_bloc.dart';
|
||||
class RelationCellEditor extends StatelessWidget {
|
||||
const RelationCellEditor({
|
||||
super.key,
|
||||
required this.databaseId,
|
||||
required this.selectedRowIds,
|
||||
required this.onSelectRow,
|
||||
});
|
||||
|
||||
final String databaseId;
|
||||
final List<String> selectedRowIds;
|
||||
final void Function(String rowId) onSelectRow;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (databaseId.isEmpty) {
|
||||
// no i18n here because UX needs thorough checking.
|
||||
return const Center(
|
||||
child: FlowyText(
|
||||
'''
|
||||
No database has been selected,
|
||||
please select one first in the field editor.
|
||||
''',
|
||||
maxLines: null,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
return BlocBuilder<RelationCellBloc, RelationCellState>(
|
||||
builder: (context, cellState) {
|
||||
if (cellState.relatedDatabaseMeta == null) {
|
||||
return const _RelationCellEditorDatabaseList();
|
||||
}
|
||||
|
||||
return BlocProvider<RelationRowSearchBloc>(
|
||||
create: (context) => RelationRowSearchBloc(
|
||||
databaseId: databaseId,
|
||||
),
|
||||
child: BlocBuilder<RelationCellBloc, RelationCellState>(
|
||||
builder: (context, cellState) {
|
||||
return BlocBuilder<RelationRowSearchBloc, RelationRowSearchState>(
|
||||
return BlocProvider<RelationRowSearchBloc>(
|
||||
create: (context) => RelationRowSearchBloc(
|
||||
databaseId: cellState.relatedDatabaseMeta!.databaseId,
|
||||
),
|
||||
child: BlocBuilder<RelationRowSearchBloc, RelationRowSearchState>(
|
||||
builder: (context, state) {
|
||||
final children = state.filteredRows
|
||||
.map(
|
||||
@@ -63,12 +50,13 @@ please select one first in the field editor.
|
||||
rightIcon: cellState.rows
|
||||
.map((e) => e.rowId)
|
||||
.contains(row.rowId)
|
||||
? FlowySvg(
|
||||
? const FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
color: Theme.of(context).primaryColor,
|
||||
)
|
||||
: null,
|
||||
onTap: () => onSelectRow(row.rowId),
|
||||
onTap: () => context
|
||||
.read<RelationCellBloc>()
|
||||
.add(RelationCellEvent.selectRow(row.rowId)),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -78,7 +66,6 @@ please select one first in the field editor.
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(6.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0) +
|
||||
GridSize.typeOptionContentInsets,
|
||||
@@ -90,15 +77,13 @@ please select one first in the field editor.
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const HSpace(2.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 2,
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
cellState.relatedDatabaseId,
|
||||
child: FlowyText.regular(
|
||||
cellState.relatedDatabaseMeta!.databaseName,
|
||||
fontSize: 11,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -106,10 +91,16 @@ please select one first in the field editor.
|
||||
],
|
||||
),
|
||||
),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: FlowyTextField(
|
||||
hintText: LocaleKeys
|
||||
.grid_relation_rowSearchTextFieldPlaceholder
|
||||
.tr(),
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: Theme.of(context).hintColor),
|
||||
onChanged: (text) => context
|
||||
.read<RelationRowSearchBloc>()
|
||||
.add(RelationRowSearchEvent.updateFilter(text)),
|
||||
@@ -140,6 +131,62 @@ please select one first in the field editor.
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RelationCellEditorDatabaseList extends StatelessWidget {
|
||||
const _RelationCellEditorDatabaseList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => RelationDatabaseListCubit(),
|
||||
child: BlocBuilder<RelationDatabaseListCubit, RelationDatabaseListState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 6, 6, 0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.grid_relation_noDatabaseSelected.tr(),
|
||||
maxLines: null,
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(6),
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
itemCount: state.databaseMetas.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final databaseMeta = state.databaseMetas[index];
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
onTap: () => context.read<RelationCellBloc>().add(
|
||||
RelationCellEvent.selectDatabaseId(
|
||||
databaseMeta.databaseId,
|
||||
),
|
||||
),
|
||||
text: FlowyText.medium(
|
||||
databaseMeta.databaseName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
-13
@@ -205,19 +205,6 @@ class _Title extends StatelessWidget {
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
),
|
||||
child: FlowyIconButton(
|
||||
onPressed: onPressedAddButton,
|
||||
width: 18,
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
+4
-4
@@ -27,11 +27,11 @@ class MobileDatabaseControls extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<GridFilterMenuBloc>(
|
||||
create: (context) => GridFilterMenuBloc(
|
||||
BlocProvider<DatabaseFilterMenuBloc>(
|
||||
create: (context) => DatabaseFilterMenuBloc(
|
||||
viewId: controller.viewId,
|
||||
fieldController: controller.fieldController,
|
||||
)..add(const GridFilterMenuEvent.initial()),
|
||||
)..add(const DatabaseFilterMenuEvent.initial()),
|
||||
),
|
||||
BlocProvider<SortEditorBloc>(
|
||||
create: (context) => SortEditorBloc(
|
||||
@@ -40,7 +40,7 @@ class MobileDatabaseControls extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
|
||||
child: BlocListener<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
|
||||
listenWhen: (p, c) => p.isVisible != c.isVisible,
|
||||
listener: (context, state) => toggleExtension.toggle(),
|
||||
child: ValueListenableBuilder<bool>(
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/util/json_print.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class CollabDocumentAdapter {
|
||||
CollabDocumentAdapter(this.editorState, this.docId);
|
||||
|
||||
final EditorState editorState;
|
||||
final String docId;
|
||||
|
||||
final _service = DocumentService();
|
||||
|
||||
/// Sync version 1
|
||||
///
|
||||
/// Force to reload the document
|
||||
///
|
||||
/// Only use in development
|
||||
Future<EditorState?> syncV1() async {
|
||||
final result = await _service.getDocument(viewId: docId);
|
||||
final document = result.fold((s) => s.toDocument(), (f) => null);
|
||||
if (document == null) {
|
||||
return null;
|
||||
}
|
||||
return EditorState(document: document);
|
||||
}
|
||||
|
||||
/// Sync version 2
|
||||
///
|
||||
/// Translate the [docEvent] from yrs to [Operation]s and apply it to the [editorState]
|
||||
///
|
||||
/// Not fully implemented yet
|
||||
Future<void> syncV2(DocEventPB docEvent) async {
|
||||
prettyPrintJson(docEvent.toProto3Json());
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
for (final event in docEvent.events) {
|
||||
for (final blockEvent in event.event) {
|
||||
switch (blockEvent.command) {
|
||||
case DeltaTypePB.Inserted:
|
||||
break;
|
||||
case DeltaTypePB.Updated:
|
||||
await _syncUpdated(blockEvent, transaction);
|
||||
break;
|
||||
case DeltaTypePB.Removed:
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await editorState.apply(transaction, isRemote: true);
|
||||
}
|
||||
|
||||
/// Sync version 3
|
||||
///
|
||||
/// Diff the local document with the remote document and apply the changes
|
||||
Future<void> syncV3() async {
|
||||
final result = await _service.getDocument(viewId: docId);
|
||||
final document = result.fold((s) => s.toDocument(), (f) => null);
|
||||
if (document == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ops = diffNodes(editorState.document.root, document.root);
|
||||
if (ops.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
for (final op in ops) {
|
||||
transaction.add(op);
|
||||
}
|
||||
await editorState.apply(transaction, isRemote: true);
|
||||
}
|
||||
|
||||
Future<void> _syncUpdated(
|
||||
BlockEventPayloadPB payload,
|
||||
Transaction transaction,
|
||||
) async {
|
||||
assert(payload.command == DeltaTypePB.Updated);
|
||||
|
||||
final path = payload.path;
|
||||
final id = payload.id;
|
||||
final value = jsonDecode(payload.value);
|
||||
|
||||
final nodes = NodeIterator(
|
||||
document: editorState.document,
|
||||
startNode: editorState.document.root,
|
||||
).toList();
|
||||
|
||||
// 1. meta -> text_map = text delta change
|
||||
if (path.isTextDeltaChangeset) {
|
||||
// find the 'text' block and apply the delta
|
||||
// ⚠️ not completed yet.
|
||||
final target = nodes.singleWhereOrNull((n) => n.id == id);
|
||||
if (target != null) {
|
||||
try {
|
||||
final delta = Delta.fromJson(jsonDecode(value));
|
||||
transaction.insertTextDelta(target, 0, delta);
|
||||
} catch (e) {
|
||||
Log.error('Failed to apply delta: $value, error: $e');
|
||||
}
|
||||
}
|
||||
} else if (path.isBlockChangeset) {
|
||||
final target = nodes.singleWhereOrNull((n) => n.id == id);
|
||||
if (target != null) {
|
||||
try {
|
||||
final delta = jsonDecode(value['data'])['delta'];
|
||||
transaction.updateNode(target, {
|
||||
'delta': Delta.fromJson(delta).toJson(),
|
||||
});
|
||||
} catch (e) {
|
||||
Log.error('Failed to update $value, error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on List<String> {
|
||||
bool get isTextDeltaChangeset {
|
||||
return length == 3 && this[0] == 'meta' && this[1] == 'text_map';
|
||||
}
|
||||
|
||||
bool get isBlockChangeset {
|
||||
return length == 2 && this[0] == 'blocks';
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/collab_document_adapter.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
||||
import 'package:appflowy/workspace/application/doc/sync_state_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -47,6 +50,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final DocumentService _documentService = DocumentService();
|
||||
final TrashService _trashService = TrashService();
|
||||
|
||||
late CollabDocumentAdapter _collabDocumentAdapter;
|
||||
|
||||
late final TransactionAdapter _transactionAdapter = TransactionAdapter(
|
||||
documentId: view.id,
|
||||
documentService: _documentService,
|
||||
@@ -78,33 +83,27 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final editorState = await _fetchDocumentState();
|
||||
final result = await _fetchDocumentState();
|
||||
_onViewChanged();
|
||||
_onDocumentChanged();
|
||||
await editorState.fold(
|
||||
final newState = await result.fold(
|
||||
(s) async {
|
||||
final result = await getIt<AuthService>().getUser();
|
||||
final userProfilePB = result.fold(
|
||||
(s) => s,
|
||||
(e) => null,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
error: null,
|
||||
editorState: s,
|
||||
isLoading: false,
|
||||
userProfilePB: userProfilePB,
|
||||
),
|
||||
final userProfilePB =
|
||||
await getIt<AuthService>().getUser().toNullable();
|
||||
return state.copyWith(
|
||||
error: null,
|
||||
editorState: s,
|
||||
isLoading: false,
|
||||
userProfilePB: userProfilePB,
|
||||
);
|
||||
},
|
||||
(f) async => emit(
|
||||
state.copyWith(
|
||||
error: f,
|
||||
editorState: null,
|
||||
isLoading: false,
|
||||
),
|
||||
(f) async => state.copyWith(
|
||||
error: f,
|
||||
editorState: null,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
emit(newState);
|
||||
},
|
||||
moveToTrash: () async {
|
||||
emit(state.copyWith(isDeleted: true));
|
||||
@@ -122,8 +121,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final isDeleted = result.fold((l) => false, (r) => true);
|
||||
emit(state.copyWith(isDeleted: isDeleted));
|
||||
},
|
||||
syncStateChanged: (isSyncing) {
|
||||
emit(state.copyWith(isSyncing: isSyncing));
|
||||
syncStateChanged: (syncState) {
|
||||
emit(state.copyWith(syncState: syncState.value));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -150,7 +149,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
_syncStateListener.start(
|
||||
didReceiveSyncState: (syncState) {
|
||||
if (!isClosed) {
|
||||
add(DocumentEvent.syncStateChanged(syncState.isSyncing));
|
||||
add(DocumentEvent.syncStateChanged(syncState));
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -174,6 +173,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
_collabDocumentAdapter = CollabDocumentAdapter(editorState, view.id);
|
||||
|
||||
// subscribe to the document change from the editor
|
||||
_subscription = editorState.transactionStream.listen((event) async {
|
||||
final time = event.$1;
|
||||
@@ -241,22 +242,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
}
|
||||
}
|
||||
|
||||
void syncDocumentDataPB(DocEventPB docEvent) {
|
||||
// prettyPrintJson(docEvent.toProto3Json());
|
||||
// todo: integrate the document change to the editor
|
||||
// for (final event in docEvent.events) {
|
||||
// for (final blockEvent in event.event) {
|
||||
// switch (blockEvent.command) {
|
||||
// case DeltaTypePB.Inserted:
|
||||
// break;
|
||||
// case DeltaTypePB.Updated:
|
||||
// break;
|
||||
// case DeltaTypePB.Removed:
|
||||
// break;
|
||||
// default:
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
Future<void> syncDocumentDataPB(DocEventPB docEvent) async {
|
||||
if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _collabDocumentAdapter.syncV3();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,17 +258,18 @@ class DocumentEvent with _$DocumentEvent {
|
||||
const factory DocumentEvent.restore() = Restore;
|
||||
const factory DocumentEvent.restorePage() = RestorePage;
|
||||
const factory DocumentEvent.deletePermanently() = DeletePermanently;
|
||||
const factory DocumentEvent.syncStateChanged(bool isSyncing) =
|
||||
syncStateChanged;
|
||||
const factory DocumentEvent.syncStateChanged(
|
||||
final DocumentSyncStatePB syncState,
|
||||
) = syncStateChanged;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentState with _$DocumentState {
|
||||
const factory DocumentState({
|
||||
required bool isDeleted,
|
||||
required bool forceClose,
|
||||
required bool isLoading,
|
||||
required bool isSyncing,
|
||||
required final bool isDeleted,
|
||||
required final bool forceClose,
|
||||
required final bool isLoading,
|
||||
required final DocumentSyncState syncState,
|
||||
bool? isDocumentEmpty,
|
||||
UserProfilePB? userProfilePB,
|
||||
EditorState? editorState,
|
||||
@@ -288,6 +280,6 @@ class DocumentState with _$DocumentState {
|
||||
isDeleted: false,
|
||||
forceClose: false,
|
||||
isLoading: true,
|
||||
isSyncing: false,
|
||||
syncState: DocumentSyncState.Syncing,
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class DocumentService {
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
final canOpen = await openDocument(viewId: view.id);
|
||||
if (canOpen.isSuccess()) {
|
||||
if (canOpen.isSuccess) {
|
||||
return FlowyResult.success(null);
|
||||
}
|
||||
final payload = CreateDocumentPayloadPB()..documentId = view.id;
|
||||
@@ -26,6 +26,14 @@ class DocumentService {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<FlowyResult<DocumentDataPB, FlowyError>> getDocument({
|
||||
required String viewId,
|
||||
}) async {
|
||||
final payload = OpenDocumentPayloadPB()..documentId = viewId;
|
||||
final result = await DocumentEventGetDocumentData(payload).send();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<FlowyResult<BlockPB, FlowyError>> getBlockFromDocument({
|
||||
required DocumentDataPB document,
|
||||
required String blockId,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/doc/sync_state_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'doc_sync_bloc.freezed.dart';
|
||||
|
||||
class DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {
|
||||
DocumentSyncBloc({
|
||||
required this.view,
|
||||
}) : _syncStateListener = DocumentSyncStateListener(id: view.id),
|
||||
super(DocumentSyncBlocState.initial()) {
|
||||
on<DocumentSyncEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final userProfile = await getIt<AuthService>().getUser().then(
|
||||
(result) => result.fold(
|
||||
(l) => l,
|
||||
(r) => null,
|
||||
),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
shouldShowIndicator:
|
||||
userProfile?.authenticator != AuthenticatorPB.Local,
|
||||
),
|
||||
);
|
||||
_syncStateListener.start(
|
||||
didReceiveSyncState: (syncState) {
|
||||
if (!isClosed) {
|
||||
add(DocumentSyncEvent.syncStateChanged(syncState));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final isNetworkConnected = await _connectivity
|
||||
.checkConnectivity()
|
||||
.then((value) => value != ConnectivityResult.none);
|
||||
emit(state.copyWith(isNetworkConnected: isNetworkConnected));
|
||||
|
||||
connectivityStream =
|
||||
_connectivity.onConnectivityChanged.listen((result) {
|
||||
if (!isClosed) {}
|
||||
emit(
|
||||
state.copyWith(
|
||||
isNetworkConnected: result != ConnectivityResult.none,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
syncStateChanged: (syncState) {
|
||||
emit(state.copyWith(syncState: syncState.value));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
final DocumentSyncStateListener _syncStateListener;
|
||||
final _connectivity = Connectivity();
|
||||
|
||||
StreamSubscription? connectivityStream;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await connectivityStream?.cancel();
|
||||
await _syncStateListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentSyncEvent with _$DocumentSyncEvent {
|
||||
const factory DocumentSyncEvent.initial() = Initial;
|
||||
const factory DocumentSyncEvent.syncStateChanged(
|
||||
DocumentSyncStatePB syncState,
|
||||
) = syncStateChanged;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentSyncBlocState with _$DocumentSyncBlocState {
|
||||
const factory DocumentSyncBlocState({
|
||||
required DocumentSyncState syncState,
|
||||
@Default(true) bool isNetworkConnected,
|
||||
@Default(false) bool shouldShowIndicator,
|
||||
}) = _DocumentSyncState;
|
||||
|
||||
factory DocumentSyncBlocState.initial() => const DocumentSyncBlocState(
|
||||
syncState: DocumentSyncState.Syncing,
|
||||
);
|
||||
}
|
||||
+12
-6
@@ -59,11 +59,11 @@ extension DocumentDataPBFromTo on DocumentDataPB {
|
||||
|
||||
// generate the meta
|
||||
final childrenMap = <String, ChildrenPB>{};
|
||||
blocks.forEach((key, value) {
|
||||
final parentId = value.parentId;
|
||||
if (parentId.isNotEmpty) {
|
||||
childrenMap[parentId] ??= ChildrenPB.create();
|
||||
childrenMap[parentId]!.children.add(value.id);
|
||||
blocks.values.where((e) => e.parentId.isNotEmpty).forEach((value) {
|
||||
final childrenId = blocks[value.parentId]?.childrenId;
|
||||
if (childrenId != null) {
|
||||
childrenMap[childrenId] ??= ChildrenPB.create();
|
||||
childrenMap[childrenId]!.children.add(value.id);
|
||||
}
|
||||
});
|
||||
final meta = MetaPB(childrenMap: childrenMap);
|
||||
@@ -101,10 +101,16 @@ extension DocumentDataPBFromTo on DocumentDataPB {
|
||||
children.addAll(childrenIds.map((e) => buildNode(e)).whereNotNull());
|
||||
}
|
||||
|
||||
return block?.toNode(
|
||||
final node = block?.toNode(
|
||||
children: children,
|
||||
meta: meta,
|
||||
);
|
||||
|
||||
for (final element in children) {
|
||||
element.parent = node;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
library document_plugin;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/document_sync_indicator.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
@@ -19,6 +19,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentPluginBuilder extends PluginBuilder {
|
||||
@@ -137,9 +138,22 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
return BlocProvider<ViewInfoBloc>.value(
|
||||
value: bloc,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DocumentShareButton(key: ValueKey(view.id), view: view),
|
||||
const HSpace(4),
|
||||
DocumentShareButton(
|
||||
key: ValueKey('share_button_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
...FeatureFlag.syncDocument.isOn
|
||||
? [
|
||||
const HSpace(20),
|
||||
DocumentSyncIndicator(
|
||||
key: ValueKey('sync_state_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
const HSpace(12),
|
||||
]
|
||||
: [const HSpace(8)],
|
||||
ViewFavoriteButton(
|
||||
key: ValueKey('favorite_button_${view.id}'),
|
||||
view: view,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
@@ -15,24 +14,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
enum EditorNotificationType {
|
||||
undo,
|
||||
redo,
|
||||
}
|
||||
|
||||
class EditorNotification extends Notification {
|
||||
const EditorNotification({
|
||||
required this.type,
|
||||
});
|
||||
|
||||
EditorNotification.undo() : type = EditorNotificationType.undo;
|
||||
EditorNotification.redo() : type = EditorNotificationType.redo;
|
||||
|
||||
final EditorNotificationType type;
|
||||
}
|
||||
|
||||
class DocumentPage extends StatefulWidget {
|
||||
const DocumentPage({
|
||||
super.key,
|
||||
@@ -50,12 +34,23 @@ class DocumentPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DocumentPageState extends State<DocumentPage> {
|
||||
EditorState? editorState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// The appflowy editor use Intl as localization, set the default language as fallback.
|
||||
Intl.defaultLocale = 'en_US';
|
||||
|
||||
EditorNotification.addListener(_onEditorNotification);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
EditorNotification.removeListener(_onEditorNotification);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -75,6 +70,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
}
|
||||
|
||||
final editorState = state.editorState;
|
||||
this.editorState = editorState;
|
||||
final error = state.error;
|
||||
if (error != null || editorState == null) {
|
||||
Log.error(error);
|
||||
@@ -149,20 +145,19 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// Future<void> _exportPage(DocumentDataPB data) async {
|
||||
// final picker = getIt<FilePickerService>();
|
||||
// final dir = await picker.getDirectoryPath();
|
||||
// if (dir == null) {
|
||||
// return;
|
||||
// }
|
||||
// final path = p.join(dir, '${documentBloc.view.name}.json');
|
||||
// const encoder = JsonEncoder.withIndent(' ');
|
||||
// final json = encoder.convert(data.toProto3Json());
|
||||
// await File(path).writeAsString(json.base64.base64);
|
||||
// if (mounted) {
|
||||
// showSnackBarMessage(context, 'Export success to $path');
|
||||
// }
|
||||
// }
|
||||
void _onEditorNotification(EditorNotificationType type) {
|
||||
final editorState = this.editorState;
|
||||
if (editorState == null) {
|
||||
return;
|
||||
}
|
||||
if (type == EditorNotificationType.undo) {
|
||||
undoCommand.execute(editorState);
|
||||
} else if (type == EditorNotificationType.redo) {
|
||||
redoCommand.execute(editorState);
|
||||
} else if (type == EditorNotificationType.exitEditing) {
|
||||
editorState.selection = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _onNotificationAction(
|
||||
BuildContext context,
|
||||
@@ -180,20 +175,3 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentSyncIndicator extends StatelessWidget {
|
||||
const DocumentSyncIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentBloc, DocumentState>(
|
||||
builder: (context, state) {
|
||||
if (state.isSyncing) {
|
||||
return const SizedBox(height: 1, child: LinearProgressIndicator());
|
||||
} else {
|
||||
return const SizedBox(height: 1);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_sync_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentSyncIndicator extends StatelessWidget {
|
||||
const DocumentSyncIndicator({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
DocumentSyncBloc(view: view)..add(const DocumentSyncEvent.initial()),
|
||||
child: BlocBuilder<DocumentSyncBloc, DocumentSyncBlocState>(
|
||||
builder: (context, state) {
|
||||
// don't show indicator if user is local
|
||||
if (!state.shouldShowIndicator) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final Color color;
|
||||
final String hintText;
|
||||
|
||||
if (!state.isNetworkConnected) {
|
||||
color = Colors.grey;
|
||||
hintText = LocaleKeys.newSettings_syncState_noNetworkConnected.tr();
|
||||
} else {
|
||||
switch (state.syncState) {
|
||||
case DocumentSyncState.SyncFinished:
|
||||
color = Colors.green;
|
||||
hintText = LocaleKeys.newSettings_syncState_synced.tr();
|
||||
break;
|
||||
case DocumentSyncState.Syncing:
|
||||
case DocumentSyncState.InitSyncBegin:
|
||||
color = Colors.yellow;
|
||||
hintText = LocaleKeys.newSettings_syncState_syncing.tr();
|
||||
break;
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
return FlowyTooltip(
|
||||
message: hintText,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum EditorNotificationType {
|
||||
none,
|
||||
undo,
|
||||
redo,
|
||||
exitEditing,
|
||||
}
|
||||
|
||||
class EditorNotification {
|
||||
const EditorNotification({
|
||||
required this.type,
|
||||
});
|
||||
|
||||
EditorNotification.undo() : type = EditorNotificationType.undo;
|
||||
EditorNotification.redo() : type = EditorNotificationType.redo;
|
||||
EditorNotification.exitEditing() : type = EditorNotificationType.exitEditing;
|
||||
|
||||
static final PropertyValueNotifier<EditorNotificationType> _notifier =
|
||||
PropertyValueNotifier(
|
||||
EditorNotificationType.none,
|
||||
);
|
||||
|
||||
final EditorNotificationType type;
|
||||
|
||||
void post() {
|
||||
_notifier.value = type;
|
||||
}
|
||||
|
||||
static void addListener(ValueChanged<EditorNotificationType> listener) {
|
||||
_notifier.addListener(() {
|
||||
listener(_notifier.value);
|
||||
});
|
||||
}
|
||||
|
||||
static void removeListener(ValueChanged<EditorNotificationType> listener) {
|
||||
_notifier.removeListener(() {
|
||||
listener(_notifier.value);
|
||||
});
|
||||
}
|
||||
|
||||
static void dispose() {
|
||||
_notifier.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart';
|
||||
@@ -23,6 +20,8 @@ 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/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
@@ -198,6 +197,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
_initEditorL10n();
|
||||
_initializeShortcuts();
|
||||
appFlowyEditorAutoScrollEdgeOffset = 220;
|
||||
indentableBlockTypes.add(ToggleListBlockKeys.type);
|
||||
convertibleBlockTypes.add(ToggleListBlockKeys.type);
|
||||
slashMenuItems = _customSlashMenuItems();
|
||||
@@ -297,19 +297,12 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
if (PlatformExtension.isMobile) {
|
||||
return AppFlowyMobileToolbar(
|
||||
toolbarHeight: 46.0,
|
||||
toolbarHeight: 42.0,
|
||||
editorState: editorState,
|
||||
toolbarItems: [
|
||||
undoToolbarItem,
|
||||
redoToolbarItem,
|
||||
addBlockToolbarItem,
|
||||
todoListToolbarItem,
|
||||
aaToolbarItem,
|
||||
boldToolbarItem,
|
||||
italicToolbarItem,
|
||||
underlineToolbarItem,
|
||||
colorToolbarItem,
|
||||
],
|
||||
toolbarItemsBuilder: (selection) => buildMobileToolbarItems(
|
||||
editorState,
|
||||
selection,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -317,19 +310,12 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
editorState: editorState,
|
||||
editorScrollController: editorScrollController,
|
||||
toolbarBuilder: (context, anchor, closeToolbar) {
|
||||
return AdaptiveTextSelectionToolbar.editable(
|
||||
clipboardStatus: ClipboardStatus.pasteable,
|
||||
onCopy: () {
|
||||
customCopyCommand.execute(editorState);
|
||||
closeToolbar();
|
||||
},
|
||||
onCut: () => customCutCommand.execute(editorState),
|
||||
onPaste: () => customPasteCommand.execute(editorState),
|
||||
onSelectAll: () => selectAllCommand.execute(editorState),
|
||||
onLiveTextInput: null,
|
||||
onLookUp: null,
|
||||
onSearchWeb: null,
|
||||
onShare: null,
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
buttonItems: buildMobileFloatingToolbarItems(
|
||||
editorState,
|
||||
anchor,
|
||||
closeToolbar,
|
||||
),
|
||||
anchors: TextSelectionToolbarAnchors(
|
||||
primaryAnchor: anchor,
|
||||
),
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ extension PasteNodes on EditorState {
|
||||
}
|
||||
final transaction = this.transaction;
|
||||
final insertedDelta = insertedNode.delta;
|
||||
// if the node is empty, replace it with the inserted node.
|
||||
if (delta.isEmpty) {
|
||||
// if the node is empty and its type is paragprah, replace it with the inserted node.
|
||||
if (delta.isEmpty && node.type == ParagraphBlockKeys.type) {
|
||||
transaction.insertNode(
|
||||
selection.end.path.next,
|
||||
insertedNode,
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
List<ContextMenuButtonItem> buildMobileFloatingToolbarItems(
|
||||
EditorState editorState,
|
||||
Offset offset,
|
||||
Function closeToolbar,
|
||||
) {
|
||||
// copy, paste, select, select all, cut
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return [];
|
||||
}
|
||||
final toolbarItems = <ContextMenuButtonItem>[];
|
||||
|
||||
if (!selection.isCollapsed) {
|
||||
toolbarItems.add(
|
||||
ContextMenuButtonItem(
|
||||
label: LocaleKeys.editor_copy.tr(),
|
||||
onPressed: () {
|
||||
copyCommand.execute(editorState);
|
||||
closeToolbar();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
toolbarItems.add(
|
||||
ContextMenuButtonItem(
|
||||
label: LocaleKeys.editor_paste.tr(),
|
||||
onPressed: () {
|
||||
pasteCommand.execute(editorState);
|
||||
closeToolbar();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (!selection.isCollapsed) {
|
||||
toolbarItems.add(
|
||||
ContextMenuButtonItem(
|
||||
label: LocaleKeys.editor_cut.tr(),
|
||||
onPressed: () {
|
||||
cutCommand.execute(editorState);
|
||||
closeToolbar();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
toolbarItems.add(
|
||||
ContextMenuButtonItem(
|
||||
label: LocaleKeys.editor_select.tr(),
|
||||
onPressed: () {
|
||||
editorState.selectWord(offset);
|
||||
closeToolbar();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
toolbarItems.add(
|
||||
ContextMenuButtonItem(
|
||||
label: LocaleKeys.editor_selectAll.tr(),
|
||||
onPressed: () {
|
||||
selectAllCommand.execute(editorState);
|
||||
closeToolbar();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return toolbarItems;
|
||||
}
|
||||
|
||||
extension on EditorState {
|
||||
void selectWord(Offset offset) {
|
||||
final node = service.selectionService.getNodeInOffset(offset);
|
||||
final selection = node?.selectable?.getWordBoundaryInOffset(offset);
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
updateSelectionWithReason(selection);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension SelectionColor on EditorState {
|
||||
String? getSelectionColor(String key) {
|
||||
final selection = this.selection;
|
||||
if (selection == null) {
|
||||
return null;
|
||||
}
|
||||
String? color = toggledStyle[key];
|
||||
if (color == null) {
|
||||
if (selection.isCollapsed && selection.startIndex != 0) {
|
||||
color = getDeltaAttributeValueInSelection<String>(
|
||||
key,
|
||||
selection.copyWith(
|
||||
start: selection.start.copyWith(
|
||||
offset: selection.startIndex - 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
color = getDeltaAttributeValueInSelection<String>(
|
||||
key,
|
||||
);
|
||||
}
|
||||
}
|
||||
return color;
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -20,9 +20,9 @@ class AlignItems extends StatelessWidget {
|
||||
|
||||
final EditorState editorState;
|
||||
final List<(String, FlowySvgData)> _alignMenuItems = [
|
||||
(_left, FlowySvgs.m_aa_align_left_s),
|
||||
(_center, FlowySvgs.m_aa_align_center_s),
|
||||
(_right, FlowySvgs.m_aa_align_right_s),
|
||||
(_left, FlowySvgs.m_aa_align_left_m),
|
||||
(_center, FlowySvgs.m_aa_align_center_m),
|
||||
(_right, FlowySvgs.m_aa_align_right_m),
|
||||
];
|
||||
|
||||
@override
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -14,10 +14,10 @@ class BIUSItems extends StatelessWidget {
|
||||
final EditorState editorState;
|
||||
|
||||
final List<(FlowySvgData, String)> _bius = [
|
||||
(FlowySvgs.m_aa_bold_s, AppFlowyRichTextKeys.bold),
|
||||
(FlowySvgs.m_aa_italic_s, AppFlowyRichTextKeys.italic),
|
||||
(FlowySvgs.m_aa_underline_s, AppFlowyRichTextKeys.underline),
|
||||
(FlowySvgs.m_aa_strike_s, AppFlowyRichTextKeys.strikethrough),
|
||||
(FlowySvgs.m_toolbar_bold_m, AppFlowyRichTextKeys.bold),
|
||||
(FlowySvgs.m_toolbar_italic_m, AppFlowyRichTextKeys.italic),
|
||||
(FlowySvgs.m_toolbar_underline_m, AppFlowyRichTextKeys.underline),
|
||||
(FlowySvgs.m_toolbar_strike_m, AppFlowyRichTextKeys.strikethrough),
|
||||
];
|
||||
|
||||
@override
|
||||
+8
-8
@@ -2,9 +2,9 @@ import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -23,9 +23,9 @@ class BlockItems extends StatelessWidget {
|
||||
final AppFlowyMobileToolbarWidgetService service;
|
||||
|
||||
final List<(FlowySvgData, String)> _blockItems = [
|
||||
(FlowySvgs.m_aa_bulleted_list_s, BulletedListBlockKeys.type),
|
||||
(FlowySvgs.m_aa_numbered_list_s, NumberedListBlockKeys.type),
|
||||
(FlowySvgs.m_aa_quote_s, QuoteBlockKeys.type),
|
||||
(FlowySvgs.m_toolbar_bulleted_list_m, BulletedListBlockKeys.type),
|
||||
(FlowySvgs.m_toolbar_numbered_list_m, NumberedListBlockKeys.type),
|
||||
(FlowySvgs.m_aa_quote_m, QuoteBlockKeys.type),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -82,7 +82,7 @@ class BlockItems extends StatelessWidget {
|
||||
Widget _buildLinkItem(BuildContext context) {
|
||||
final theme = ToolbarColorExtension.of(context);
|
||||
final items = [
|
||||
(AppFlowyRichTextKeys.code, FlowySvgs.m_aa_code_s),
|
||||
(AppFlowyRichTextKeys.code, FlowySvgs.m_aa_code_m),
|
||||
// (InlineMathEquationKeys.formula, FlowySvgs.m_aa_math_s),
|
||||
];
|
||||
return PopupMenu(
|
||||
@@ -119,7 +119,7 @@ class BlockItems extends StatelessWidget {
|
||||
showDownArrow: true,
|
||||
onTap: _onLinkItemTap,
|
||||
backgroundColor: theme.toolbarMenuItemBackgroundColor,
|
||||
icon: FlowySvgs.m_aa_link_s,
|
||||
icon: FlowySvgs.m_toolbar_link_m,
|
||||
isSelected: false,
|
||||
iconPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14.0,
|
||||
+4
-11
@@ -5,27 +5,20 @@ import 'package:flutter/material.dart';
|
||||
class CloseKeyboardOrMenuButton extends StatelessWidget {
|
||||
const CloseKeyboardOrMenuButton({
|
||||
super.key,
|
||||
required this.showingMenu,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final bool showingMenu;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 62,
|
||||
height: 46,
|
||||
height: 42,
|
||||
child: FlowyButton(
|
||||
margin: showingMenu ? const EdgeInsets.only(right: 0.5) : null,
|
||||
text: showingMenu
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_toolbar_show_keyboard_s,
|
||||
)
|
||||
: const FlowySvg(
|
||||
FlowySvgs.m_toolbar_hide_keyboard_s,
|
||||
),
|
||||
text: const FlowySvg(
|
||||
FlowySvgs.m_toolbar_keyboard_m,
|
||||
),
|
||||
onTap: onPressed,
|
||||
),
|
||||
);
|
||||
+12
-36
@@ -1,8 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_list.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -20,7 +21,10 @@ class ColorItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ToolbarColorExtension.of(context);
|
||||
final selectedBackgroundColor = _getBackgroundColor(context);
|
||||
final String? selectedTextColor =
|
||||
editorState.getSelectionColor(AppFlowyRichTextKeys.textColor);
|
||||
final String? selectedBackgroundColor =
|
||||
editorState.getSelectionColor(AppFlowyRichTextKeys.backgroundColor);
|
||||
|
||||
return MobileToolbarMenuItemWrapper(
|
||||
size: const Size(82, 52),
|
||||
@@ -43,10 +47,11 @@ class ColorItem extends StatelessWidget {
|
||||
selection: editorState.selection!,
|
||||
);
|
||||
},
|
||||
icon: FlowySvgs.m_aa_color_s,
|
||||
backgroundColor:
|
||||
selectedBackgroundColor ?? theme.toolbarMenuItemBackgroundColor,
|
||||
selectedBackgroundColor: selectedBackgroundColor,
|
||||
icon: FlowySvgs.m_aa_font_color_m,
|
||||
iconColor: selectedTextColor?.tryToColor(),
|
||||
backgroundColor: selectedBackgroundColor?.tryToColor() ??
|
||||
theme.toolbarMenuItemBackgroundColor,
|
||||
selectedBackgroundColor: selectedBackgroundColor?.tryToColor(),
|
||||
isSelected: selectedBackgroundColor != null,
|
||||
showRightArrow: true,
|
||||
iconPadding: const EdgeInsets.only(
|
||||
@@ -56,33 +61,4 @@ class ColorItem extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color? _getBackgroundColor(BuildContext context) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return null;
|
||||
}
|
||||
String? backgroundColor =
|
||||
editorState.toggledStyle[AppFlowyRichTextKeys.backgroundColor];
|
||||
if (backgroundColor == null) {
|
||||
if (selection.isCollapsed && selection.startIndex != 0) {
|
||||
backgroundColor = editorState.getDeltaAttributeValueInSelection<String>(
|
||||
AppFlowyRichTextKeys.backgroundColor,
|
||||
selection.copyWith(
|
||||
start: selection.start.copyWith(
|
||||
offset: selection.startIndex - 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
backgroundColor = editorState.getDeltaAttributeValueInSelection<String>(
|
||||
AppFlowyRichTextKeys.backgroundColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (backgroundColor != null && int.tryParse(backgroundColor) != null) {
|
||||
return Color(int.parse(backgroundColor));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+15
-35
@@ -1,7 +1,8 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -9,6 +10,8 @@ import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _count = 6;
|
||||
|
||||
Future<void> showTextColorAndBackgroundColorPicker(
|
||||
BuildContext context, {
|
||||
required EditorState editorState,
|
||||
@@ -26,7 +29,7 @@ Future<void> showTextColorAndBackgroundColorPicker(
|
||||
backgroundColor: theme.toolbarMenuBackgroundColor,
|
||||
elevation: 20,
|
||||
title: LocaleKeys.grid_selectOption_colorPanelTitle.tr(),
|
||||
padding: const EdgeInsets.fromLTRB(18, 4, 18, 8),
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 10, 8),
|
||||
builder: (context) {
|
||||
return _TextColorAndBackgroundColor(
|
||||
editorState: editorState,
|
||||
@@ -63,9 +66,10 @@ class _TextColorAndBackgroundColorState
|
||||
extends State<_TextColorAndBackgroundColor> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String? selectedTextColor = _getColor(AppFlowyRichTextKeys.textColor);
|
||||
final String? selectedBackgroundColor =
|
||||
_getColor(AppFlowyRichTextKeys.backgroundColor);
|
||||
final String? selectedTextColor =
|
||||
widget.editorState.getSelectionColor(AppFlowyRichTextKeys.textColor);
|
||||
final String? selectedBackgroundColor = widget.editorState
|
||||
.getSelectionColor(AppFlowyRichTextKeys.backgroundColor);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -79,6 +83,7 @@ class _TextColorAndBackgroundColorState
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
const VSpace(6.0),
|
||||
_TextColors(
|
||||
selectedColor: selectedTextColor?.tryToColor(),
|
||||
onSelectedColor: (textColor) async {
|
||||
@@ -115,6 +120,7 @@ class _TextColorAndBackgroundColorState
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
const VSpace(6.0),
|
||||
_BackgroundColors(
|
||||
selectedColor: selectedBackgroundColor?.tryToColor(),
|
||||
onSelectedColor: (backgroundColor) async {
|
||||
@@ -145,28 +151,6 @@ class _TextColorAndBackgroundColorState
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? _getColor(String key) {
|
||||
final selection = widget.selection;
|
||||
String? color = widget.editorState.toggledStyle[key];
|
||||
if (color == null) {
|
||||
if (selection.isCollapsed && selection.startIndex != 0) {
|
||||
color = widget.editorState.getDeltaAttributeValueInSelection<String>(
|
||||
key,
|
||||
selection.copyWith(
|
||||
start: selection.start.copyWith(
|
||||
offset: selection.startIndex - 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
color = widget.editorState.getDeltaAttributeValueInSelection<String>(
|
||||
key,
|
||||
);
|
||||
}
|
||||
}
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
class _BackgroundColors extends StatelessWidget {
|
||||
@@ -202,7 +186,7 @@ class _BackgroundColors extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 6,
|
||||
crossAxisCount: _count,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: colors.mapIndexed(
|
||||
@@ -236,9 +220,7 @@ class _BackgroundColorItem extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(
|
||||
6.0,
|
||||
),
|
||||
margin: const EdgeInsets.all(6.0),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: Corners.s12Border,
|
||||
@@ -283,7 +265,7 @@ class _TextColors extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 6,
|
||||
crossAxisCount: _count,
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -317,9 +299,7 @@ class _TextColorItem extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(
|
||||
6.0,
|
||||
),
|
||||
margin: const EdgeInsets.all(6.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: Corners.s12Border,
|
||||
border: Border.all(
|
||||
+2
-3
@@ -1,13 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/util/google_font_family_extension.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
+4
-4
@@ -19,25 +19,25 @@ class HeadingsAndTextItems extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_HeadingOrTextItem(
|
||||
icon: FlowySvgs.m_aa_h1_s,
|
||||
icon: FlowySvgs.m_aa_h1_m,
|
||||
blockType: HeadingBlockKeys.type,
|
||||
editorState: editorState,
|
||||
level: 1,
|
||||
),
|
||||
_HeadingOrTextItem(
|
||||
icon: FlowySvgs.m_aa_h2_s,
|
||||
icon: FlowySvgs.m_aa_h2_m,
|
||||
blockType: HeadingBlockKeys.type,
|
||||
editorState: editorState,
|
||||
level: 2,
|
||||
),
|
||||
_HeadingOrTextItem(
|
||||
icon: FlowySvgs.m_aa_h3_s,
|
||||
icon: FlowySvgs.m_aa_h3_m,
|
||||
blockType: HeadingBlockKeys.type,
|
||||
editorState: editorState,
|
||||
level: 3,
|
||||
),
|
||||
_HeadingOrTextItem(
|
||||
icon: FlowySvgs.m_aa_text_s,
|
||||
icon: FlowySvgs.m_aa_paragraph_m,
|
||||
blockType: ParagraphBlockKeys.type,
|
||||
editorState: editorState,
|
||||
),
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -22,7 +22,7 @@ class IndentAndOutdentItems extends StatelessWidget {
|
||||
children: [
|
||||
MobileToolbarMenuItemWrapper(
|
||||
size: const Size(95, 52),
|
||||
icon: FlowySvgs.m_aa_outdent_s,
|
||||
icon: FlowySvgs.m_aa_outdent_m,
|
||||
enable: isOutdentable(editorState),
|
||||
isSelected: false,
|
||||
enableTopRightRadius: false,
|
||||
@@ -37,7 +37,7 @@ class IndentAndOutdentItems extends StatelessWidget {
|
||||
const ScaledVerticalDivider(),
|
||||
MobileToolbarMenuItemWrapper(
|
||||
size: const Size(95, 52),
|
||||
icon: FlowySvgs.m_aa_indent_s,
|
||||
icon: FlowySvgs.m_aa_indent_m,
|
||||
enable: isIndentable(editorState),
|
||||
isSelected: false,
|
||||
enableTopLeftRadius: false,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Loaded 100 of 502 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user