Compare commits

...
Author SHA1 Message Date
Nathan.fooo 529b5e5080 refactor: Model select (#7875)
* refactor: model select

* refactor: add test

* fix: add source

* fix: add source

* chore: notify all unset source

* chore: fix test
2025-05-02 15:49:31 +08:00
Lucas.Xu 4e2723f917 chore: remove sentry 2025-05-01 17:43:18 +08:00
Lucas.Xu aebae48c14 chore: update i18n 2025-05-01 17:32:12 +08:00
Nathan.foooandLucas.Xu 7750f2257f fix: anon user can not locate its data (#7873)
* fix: anon user can not locate its data

* chore: add test

* chore: bump version 0.9.1

* chore: remove self-hosted ios ci

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-05-01 15:06:56 +08:00
Lucas e6e23936f5 fix: appimage build (#7870) 2025-04-30 20:35:10 +08:00
Lucas 8441a8c7f0 feat: remove reminder in the same event (#7869) 2025-04-30 19:34:43 +08:00
Morn 86872c462f chore: update CHANGELOG.md (#7868) 2025-04-30 17:36:56 +08:00
Lucas 9389034bf0 chore: bump editor version e56746a (#7866)
* chore: bump editor version e56746a

* fix: use old workspaces if the get workspaces api failed
2025-04-30 17:18:51 +08:00
Lucas 63244e0b3c fix: paste issue with the nested image (#7857)
* chore: replace delete button style

* chore: replace textfield in settings page

* fix: icon text align issue in database

* fix: member count is 0

* fix: invite member issue on mobile

* fix: checklist item align issue on Windows

* fix: integration test

* fix: support converting paragraph to markdown

* chore: update editor version

* fix: delete command in simple table cell

* feat: use new password error code
2025-04-30 11:45:48 +08:00
Morn 64dff44ce1 fix: revert some date picker code and fix notification issue (#7861)
* fix: revert some date picker code and fix notification issue

* fix: UI overflow issue

* chore: update reminder remove logic
2025-04-30 11:27:57 +08:00
Richard Shiue 8b1eb49155 chore: import new prompts and add featured category (#7859)
* chore: mark prompts as featured

* chore: implement is featured list

* chore: implement categories

* chore: remove learning category
2025-04-30 09:49:13 +08:00
Richard Shiue 4a4ee79c3f fix: adjust chat message layout (#7860)
* fix: user chat message bubble should have flex

* chore: adjust spacing between messages
2025-04-29 20:42:19 +08:00
Richard Shiue 22111cfa53 fix: text color for local ai model picker (#7855) 2025-04-29 18:07:25 +08:00
Morn d28987d16d fix: some mobile search UI issues (#7845)
* fix: some mobile search UI issues

* fix: some icon colors

* fix: show 'Add Link' while creating a link on mobile

* fix: add tapping event for mobile embed link

* fix: bookmark can not open the non-http link

* fix: improve the behavior of links on mobile

* fix: switch workspace will refresh the reminders

* chore: remove unused import

* fix: test error
2025-04-29 12:52:28 +08:00
Lucas 63aa5112ba fix: integration test (#7852) 2025-04-29 11:20:42 +08:00
Morn 4f3eefbefc chore: remove unuseful import (#7849) 2025-04-29 09:58:21 +08:00
MornandLucas.Xu 9353863439 fix: some lr issues (#7848)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-04-28 22:13:13 +08:00
Lucas fb33e4d5c3 fix: unable to expand sidebar on Windows (#7847)
* fix: unable to collapse sidebar

* fix: unable to expand workspace menu
2025-04-28 22:01:37 +08:00
Lucas 807aa74509 chore: update i18n (#7846)
* chore: update i18n

* chore: update i18n

* chore: update i18n

* chore: only show one copied toast

* chore: add upgrade dialog in invite link

* chore: update dialog style

* fix: flutter tests
2025-04-28 20:47:08 +08:00
Lucas 168b29abe3 Revert "chore: try to fix the mobile build" (#7844)
This reverts commit 3f06f6415a.
2025-04-28 13:25:05 +08:00
Nathan 2463107c6c chore: fix ios build 2025-04-28 13:18:18 +08:00
Richard Shiue 2b03c34df1 fix: local and remote returning different data (#7843) 2025-04-28 13:17:16 +08:00
Morn 66fac524c1 fix: some notification center launch review issues (#7825)
* fix: some notification center UI issues

* fix: add number to add dot on mobile

* chore: revamp number red dot

* fix: apply tokens for notification center

* fix: change some token value

* chore: change notification at to icon

* fix: revamp reminder bloc

* fix: test errors

* chore: lint issue

* fix: unit test errors
2025-04-28 13:16:52 +08:00
Lucas 2661ad27ec feat: improve password error message (#7842)
* chore: update i18n

* chore: bump client api version

* chore: try to fix the mobile build

* chore: enable iOS CI
2025-04-28 13:08:28 +08:00
Morn 15bfdfb550 fix: some mobile search LR issues (#7836) 2025-04-28 11:16:02 +08:00
Morn 1f54946a0e fix: mobile link LR issues (#7837)
* fix: mobile link LR issues

* fix: change mobile toolbar link icon
2025-04-28 11:14:23 +08:00
Nathan.fooo f4d7d1381d Merge pull request #7840 from AppFlowy-IO/fix_dropdown_menu
chore: remove radius on dropdown menu
2025-04-28 10:34:54 +08:00
Lucas ae5f06ad1a feat: support 'forgot password', 'setup password' and 'change password' on mobile (#7841)
* feat: forgot password

* fix: warning

* feat: move verify token logic to sign in bloc

* feat: set new page page

* feat: integrate reset password api

* chore: i18n

* chore: upgrade iOS podfile

* feat: support setup password and change password on mobile

* chore: remove unused files

* chore: i18n

* fix: flutter analyze
2025-04-28 10:25:07 +08:00
Richard Shiue ecb1c07917 chore: new design for custom prompt (#7833)
* chore: new design

* chore: improve text selection bg

* fix: remove listener
2025-04-28 10:03:06 +08:00
Richard Shiue cc982b81ee fix: custom prompt launch review issues (#7829)
* fix: text overflow in category items

* fix: don't pass invalid characters

* fix: prompt title typo

* fix: examples should also rcontain initial instructions

* fix: selection not visible

* fix: chat page input border gets cut off

* fix: remove hover and stretch buttons
2025-04-28 09:04:29 +08:00
Nathan 90bbaea2c2 chore: remove radius on dropdown menu 2025-04-27 19:11:05 +08:00
Nathan.fooo ec5eb4e337 Merge pull request #7834 from AppFlowy-IO/support_switch_local_models
Support switch local models
2025-04-26 10:43:59 +08:00
Nathan 28a4303f0c Merge branch 'main' into support_switch_local_models 2025-04-26 10:43:32 +08:00
Nathan f374ca1574 chore: update 2025-04-26 10:40:33 +08:00
Nathan 549e8aee03 chore: clippy 2025-04-26 10:04:03 +08:00
Nathan 3bc0cc7b43 chore: rename 2025-04-26 00:56:45 +08:00
Nathan 90000add22 chore: update ui 2025-04-25 23:41:35 +08:00
Nathan 86e6845302 chore: update ui 2025-04-25 23:29:22 +08:00
Lucas 670023b8da fix: add open workspace assertion (#7824)
* fix: add fix workspace assertion

* fix: macos build

* feat: add new token

* feat: change min width of AFButton to 76.0

* chore: update translations

* feat: add verifying button

* feat: set barrierDismissible as false

* chore: install libcurl4-openssl-dev on Linux

* fix: flutter analyze

* chore: bump cloud version to 0.9.45

* fix: ci tests

* fix: home bloc test

* fix: integration test

* fix: integration test

* fix: integration test
2025-04-25 21:40:21 +08:00
Nathan 7dd8d06c85 chore: display all local models 2025-04-25 16:53:32 +08:00
Nathan 2c6253576f chore: fix test 2025-04-25 14:33:09 +08:00
Nathan.fooo 7f13790485 Merge pull request #7828 from AppFlowy-IO/fix_user_rename
fix: user name stay no change when reopen the setting menu after renaming
2025-04-25 14:02:22 +08:00
Nathan 3bd34226c3 chore: refresh user profile when open workspace 2025-04-25 14:01:06 +08:00
Nathan d893b89e4d fix: clippy 2025-04-25 13:13:08 +08:00
Nathan.fooo d0b3a0dda3 Merge pull request #7660 from AppFlowy-IO/ai_writer_test
Ai writer test
2025-04-25 11:18:51 +08:00
Nathan 4d2380a954 fix: test 2025-04-25 10:35:09 +08:00
Nathan a673b12514 fix: test 2025-04-25 00:16:04 +08:00
Nathan 14fc72bac6 fix: test 2025-04-24 23:53:43 +08:00
Nathan f0875d6b6e fix: user rename 2025-04-24 23:29:20 +08:00
Nathan.fooo f822743c1b Merge pull request #7826 from AppFlowy-IO/workspace_type2
chore: replace auth type with workspace type
2025-04-24 20:58:53 +08:00
Nathan 36dacdbdf2 chore: fix test 2025-04-24 20:55:06 +08:00
Nathan 8485a67357 chore: clippy 2025-04-24 20:08:25 +08:00
Nathan da47c11070 chore: replace auth type with workspace type 2025-04-24 20:01:41 +08:00
Nathan.fooo c35609b1c3 Merge pull request #7823 from AppFlowy-IO/use_user_auth_type
chore: use user auth type
2025-04-24 16:32:23 +08:00
Nathan dbda1972d1 chore: update client api 2025-04-24 16:25:42 +08:00
Nathan c9ca6c6cc8 chore: update template 2025-04-24 15:54:17 +08:00
Nathan 9e5a6df52c chore: update template 2025-04-24 15:45:38 +08:00
Nathan 76b9cbd446 Merge branch 'main' into use_user_auth_type 2025-04-24 13:39:54 +08:00
Nathan 718c5ad16f chore: use auth type instead of workspace auth type 2025-04-24 13:38:25 +08:00
Lucas 48d6a71d96 refactor: deeplink handler (#7802)
* refactor: deeplink handler

* feat: add invitation deeplink handler

* test: add deeplink tests

* feat: add user_id in the invitation callback

* feat: add expire login deeplink handler

* feat: add loading indicator in continue with password page

* feat: replace user_id with email

* fix: invitation deeplink test
2025-04-24 13:20:05 +08:00
Lucas 6a887fdca9 feat: support invite member by link on mobile (#7811)
* fix: invitation link issues

* fix: cargo fmt

* feat: support invite member by link on mobile

* feat: implement new settings page design on mobile

* feat: add leave workspace button

* fix: flutter analyze

* fix: bloc error

* feat: add addMembers page
2025-04-24 10:28:18 +08:00
Nathan 851869644b Merge branch 'main' into ai_writer_test 2025-04-24 10:25:04 +08:00
Richard Shiue 780ba51d87 chore: remove sample response (#7821) 2025-04-24 10:13:38 +08:00
Nathan 6da7c17a9e chore: upgrade cloud version 2025-04-02 22:37:31 +08:00
Nathan 13d8b1ed37 chore: fix test 2025-04-02 18:03:03 +08:00
Nathan 0d0c6fff32 chore: update test 2025-04-01 13:32:28 +08:00
Lucas.Xu e4187a570c Merge branch 'main' into ai_writer_test 2025-04-01 09:27:16 +08:00
Richard Shiue 01dbcf7697 chore: don't try to force update selection 2025-03-31 22:56:15 +08:00
Richard Shiue 2ec68c9269 chore: merge remote-tracking branch 'upstream/main' into this one 2025-03-31 22:54:26 +08:00
Nathan 73e3cbdc18 chore: remove env test 2025-03-31 21:53:56 +08:00
Nathan 5c99572e93 chore: rename test 2025-03-31 21:00:14 +08:00
Nathan 72fe1d7c47 chore: add integration test 2025-03-31 16:19:56 +08:00
Nathan 89647ae683 chore: logs for windows 2025-03-31 00:15:59 +08:00
304 changed files with 9691 additions and 3646 deletions

No files matched your search

+1 -1
View File
@@ -65,7 +65,7 @@ runs:
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
;;
Windows)
vcpkg integrate install
@@ -52,7 +52,7 @@ runs:
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager libcurl4-openssl-dev
shell: bash
- name: Enable Flutter Desktop
+13 -13
View File
@@ -28,7 +28,7 @@ env:
FLUTTER_VERSION: "3.27.4"
RUST_TOOLCHAIN: "1.81.0"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.54-amd64
CLOUD_VERSION: 0.9.37-amd64
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -40,7 +40,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -74,7 +74,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [windows-latest]
os: [ windows-latest ]
include:
- os: windows-latest
flutter_profile: development-windows-x86
@@ -101,7 +101,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [macos-latest]
os: [ macos-latest ]
include:
- os: macos-latest
flutter_profile: development-mac-x86_64
@@ -123,12 +123,12 @@ jobs:
flutter_profile: ${{ matrix.flutter_profile }}
unit_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -174,7 +174,7 @@ jobs:
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
fi
shell: bash
@@ -217,11 +217,11 @@ jobs:
shell: bash
cloud_integration_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -308,7 +308,7 @@ jobs:
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
shell: bash
- name: Enable Flutter Desktop
@@ -340,13 +340,13 @@ jobs:
shell: bash
integration_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
os: [ ubuntu-latest ]
test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
+21 -42
View File
@@ -26,28 +26,7 @@ concurrency:
cancel-in-progress: true
jobs:
build-self-hosted:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Build AppFlowy
working-directory: frontend
run: |
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
cargo make --profile development-ios-arm64-sim code_generation
- uses: futureware-tech/simulator-action@v3
id: simulator-action
with:
model: "iPhone 15"
shutdown_after_job: false
integration-tests:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: macos-latest
steps:
@@ -94,26 +73,26 @@ jobs:
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
cargo make --profile development-ios-arm64-sim code_generation
- uses: futureware-tech/simulator-action@v3
id: simulator-action
with:
model: "iPhone 15"
shutdown_after_job: false
# - uses: futureware-tech/simulator-action@v3
# id: simulator-action
# with:
# model: "iPhone 15"
# shutdown_after_job: false
- name: Run AppFlowy on simulator
working-directory: frontend/appflowy_flutter
run: |
flutter run -d ${{ steps.simulator-action.outputs.udid }} &
pid=$!
sleep 500
kill $pid
continue-on-error: true
# - name: Run AppFlowy on simulator
# working-directory: frontend/appflowy_flutter
# run: |
# flutter run -d ${{ steps.simulator-action.outputs.udid }} &
# pid=$!
# sleep 500
# kill $pid
# continue-on-error: true
# Integration tests
- name: Run integration tests
working-directory: frontend/appflowy_flutter
# The integration tests are flaky and sometimes fail with "Connection timed out":
# Don't block the CI. If the tests fail, the CI will still pass.
# Instead, we're using Code Magic to re-run the tests to check if they pass.
continue-on-error: true
run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
# # Integration tests
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# # The integration tests are flaky and sometimes fail with "Connection timed out":
# # Don't block the CI. If the tests fail, the CI will still pass.
# # Instead, we're using Code Magic to re-run the tests to check if they pass.
# continue-on-error: true
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
+2 -1
View File
@@ -366,9 +366,10 @@ jobs:
run: |
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo apt-get update
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev libcurl4-openssl-dev
sudo apt-get install keybinder-3.0
sudo apt-get install -y alien libnotify-dev
sudo apt install libfuse2
source $HOME/.cargo/env
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
+43
View File
@@ -1,4 +1,47 @@
# Release Notes
## Version 0.9.1 - 01/05/2025
### Desktop
#### New Features
- Added AppFlowy Prompt Library to AI Chat and Document's Ask AI
- Revamped the desktop in-app notification center
- Supported login with password, as well as forgot and change password options
- Supported copying link to invite members
- Improved the Settings' Members tab with new metadata: member avatar and joined time
#### Bug Fixes
- Fixed data loss when using anonymous local
- Fixed crash when trying to delete an emoji
- Fixed Windows scaling issue
- Correctly displayed mention text by decoding web content
### Mobile
#### New Features
- Supported workspace search
- Improved UX for links in documents
- Supported changing password in Mobile Settings
- Added support for inviting members via links
#### Bug Fixes
- Correctly displayed mention text by decoding web content
## Version 0.9.0 - 30/04/2025
### Desktop
#### New Features
- Added AppFlowy Prompt Library to AI Chat and Document's Ask AI
- Revamped the desktop in-app notification center
- Supported login with password, as well as forgot and change password options
- Supported copying link to invite members
- Improved the Settings' Members tab with new metadata: member avatar and joined time
#### Bug Fixes
- Fixed crash when trying to delete an emoji
- Fixed Windows scaling issue
- Correctly displayed mention text by decoding web content
### Mobile
#### New Features
- Supported workspace search
- Improved UX for links in documents
- Supported changing password in Mobile Settings
- Added support for inviting members via links
#### Bug Fixes
- Correctly displayed mention text by decoding web content
## Version 0.8.9 - 16/04/2025
### Desktop
#### New Features
+1 -1
View File
@@ -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.9.0"
APPFLOWY_VERSION = "0.9.1"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
File diff suppressed because it is too large. Load diff
@@ -12,24 +12,38 @@ import 'uncategorized/uncategorized_test_runner.dart'
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
Future<void> main() async {
preset_af_cloud_env_test.main();
// don't remove this test, it can prevent the test from failing.
{
preset_af_cloud_env_test.main();
data_migration_test_runner.main();
data_migration_test_runner.main();
// uncategorized
uncategorized_test_runner.main();
// uncategorized
uncategorized_test_runner.main();
// workspace
workspace_test_runner.main();
// document
document_test_runner.main();
// workspace
workspace_test_runner.main();
}
// sidebar
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
// don't remove this test, it can prevent the test from failing.
{
preset_af_cloud_env_test.main();
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
}
// database
database_test_runner.main();
// don't remove this test, it can prevent the test from failing.
{
preset_af_cloud_env_test.main();
database_test_runner.main();
}
// document
// don't remove this test, it can prevent the test from failing.
{
preset_af_cloud_env_test.main();
document_test_runner.main();
}
}
@@ -1,12 +1,19 @@
import 'package:appflowy/ai/service/ai_entities.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
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:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/ai_test_op.dart';
import '../../../shared/constants.dart';
import '../../../shared/mock/mock_ai.dart';
import '../../../shared/util.dart';
void main() {
@@ -17,6 +24,7 @@ void main() {
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
aiRepositoryBuilder: () => MockAIRepository(),
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
@@ -43,5 +51,184 @@ void main() {
// expect the ai writer block is not in the document
expect(find.byType(AiWriterBlockComponent), findsNothing);
});
testWidgets('Improve writing', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
await tester.editor.tapLineOfEditorAt(0);
// insert a paragraph
final text = 'I have an apple';
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(text);
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: text.length),
),
);
await tester.pumpAndSettle();
await tester.tapButton(find.byType(ImproveWritingButton));
final editorState = tester.editor.getCurrentEditorState();
final document = editorState.document;
expect(document.root.children.length, 3);
expect(document.root.children[1].type, ParagraphBlockKeys.type);
expect(
document.root.children[1].delta!.toPlainText(),
'I have an apple and a banana',
);
});
testWidgets('fix grammar', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
await tester.editor.tapLineOfEditorAt(0);
// insert a paragraph
final text = 'We didn’t had enough money';
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(text);
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: text.length),
),
);
await tester.pumpAndSettle();
await tester.selectAIWriter(AiWriterCommand.fixSpellingAndGrammar);
final editorState = tester.editor.getCurrentEditorState();
final document = editorState.document;
expect(document.root.children.length, 3);
expect(document.root.children[1].type, ParagraphBlockKeys.type);
expect(
document.root.children[1].delta!.toPlainText(),
'We didn’t have enough money',
);
});
testWidgets('ask ai', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudDevelop,
aiRepositoryBuilder: () => MockAIRepository(
validator: _CompletionHistoryValidator(),
),
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
await tester.editor.tapLineOfEditorAt(0);
// insert a paragraph
final text = 'What is TPU?';
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(text);
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: text.length),
),
);
await tester.pumpAndSettle();
await tester.selectAIWriter(AiWriterCommand.userQuestion);
await tester.enterTextInPromptTextField("Explain the concept of TPU");
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
// await tester.selectModel("GPT-4o-mini");
await tester.enterTextInPromptTextField("How about GPU?");
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
});
});
}
class _CompletionHistoryValidator extends StreamCompletionValidator {
static const _expectedResponses = {
1: "What is TPU?",
3: [
"What is TPU?",
"Explain the concept of TPU",
"TPU is a tensor processing unit that is designed to accelerate machine",
],
5: [
"What is TPU?",
"Explain the concept of TPU",
"TPU is a tensor processing unit that is designed to accelerate machine",
"How about GPU?",
"GPU is a graphics processing unit that is designed to accelerate machine learning tasks.",
],
};
@override
bool validate(
String text,
String? objectId,
CompletionTypePB completionType,
PredefinedFormat? format,
List<String> sourceIds,
List<AiWriterRecord> history,
) {
assert(completionType == CompletionTypePB.UserQuestion);
if (history.isEmpty) return false;
final expectedMessages = _expectedResponses[history.length];
if (expectedMessages == null) return false;
if (expectedMessages is String) {
_assertMessage(history[0].content, expectedMessages);
return true;
} else if (expectedMessages is List<String>) {
for (var i = 0; i < expectedMessages.length; i++) {
_assertMessage(history[i].content, expectedMessages[i]);
}
return true;
}
return false;
}
void _assertMessage(String actual, String expected) {
assert(
actual.trim() == expected,
"expected '$expected', but got '${actual.trim()}'",
);
}
}
@@ -8,11 +8,13 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Empty', () {
testWidgets('set appflowy cloud', (tester) async {
group('Preset cloud env', () {
testWidgets('use self-hosted cloud', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.pumpAndSettle();
});
});
}
@@ -39,8 +39,8 @@ void main() {
);
await tester.tapButton(find.byType(AccountSignInOutButton));
tester.expectToSeeText(LocaleKeys.button_ok.tr());
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
tester.expectToSeeText(LocaleKeys.button_yes.tr());
await tester.tapButtonWithName(LocaleKeys.button_yes.tr());
// Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 5));
@@ -80,8 +80,8 @@ void main() {
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
expect(find.byType(AppFlowyEditorPage), findsNothing);
expect(find.text('Blank page'), findsOne);
expect(find.byType(AppFlowyEditorPage), findsOneWidget);
expect(find.text('Blank page'), findsNothing);
});
});
}
@@ -237,11 +237,11 @@ void main() {
await tester.tapButton(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
// set date to be fifteenth of the next month
// set date to be fifteenth of the last month
await tester.tap(
find.descendant(
of: find.byType(DesktopAppFlowyDatePicker),
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
matching: find.byFlowySvg(FlowySvgs.arrow_left_s),
),
);
await tester.pumpAndSettle();
@@ -275,9 +275,9 @@ void main() {
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final now = DateTime.now();
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
final fifteenthOfLastMonth = DateTime(now.year, now.month - 1, 15);
final formattedDate =
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
dateTimeSettings.dateFormat.formatDate(fifteenthOfLastMonth, false);
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
@@ -378,11 +378,11 @@ void main() {
await tester.tapButton(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
// set date to be fifteenth of the next month
// set date to be fifteenth of the last month
await tester.tap(
find.descendant(
of: find.byType(DesktopAppFlowyDatePicker),
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
matching: find.byFlowySvg(FlowySvgs.arrow_left_s),
),
);
await tester.pumpAndSettle();
@@ -416,9 +416,9 @@ void main() {
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final now = DateTime.now();
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
final fifteenthOfLastMonth = DateTime(now.year, now.month - 1, 15);
final formattedDate =
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
dateTimeSettings.dateFormat.formatDate(fifteenthOfLastMonth, false);
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
@@ -1,13 +1,8 @@
import 'dart:io';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -91,23 +86,24 @@ void main() {
// }
// });
testWidgets('reset to default location', (tester) async {
await tester.initializeAppFlowy();
// Disable this test because it failed after executing.
// testWidgets('reset to default location', (tester) async {
// await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// await tester.tapAnonymousSignInButton();
// home and readme document
await tester.expectToSeeHomePageWithGetStartedPage();
// // home and readme document
// await tester.expectToSeeHomePageWithGetStartedPage();
// open settings and restore the location
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.manageData);
await tester.restoreLocation();
// // open settings and restore the location
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.manageData);
// await tester.restoreLocation();
expect(
await appFlowyApplicationDataDirectory().then((value) => value.path),
await getIt<ApplicationDataStorage>().getPath(),
);
});
// expect(
// await appFlowyApplicationDataDirectory().then((value) => value.path),
// await getIt<ApplicationDataStorage>().getPath(),
// );
// });
});
}
@@ -128,7 +128,12 @@ void main() {
/// remove the link
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));
final node = editorState.getNodeAtPath([0]);
expect(node?.delta?.toJson(), [{'insert': testText}]);
expect(
node?.delta?.toJson(),
[
{'insert': testText},
],
);
});
});
}
@@ -36,7 +36,7 @@ void main() {
expect(find.byType(MobileSearchRecentList), findsNothing);
expect(find.byType(MobileSearchResultList), findsOneWidget);
expect(
find.text(LocaleKeys.search_noResultForSearching.tr(args: [query])),
find.text(LocaleKeys.search_noResultForSearching.tr()),
findsOneWidget,
);
@@ -0,0 +1,41 @@
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
extension AppFlowyAITest on WidgetTester {
Future<void> selectAIWriter(AiWriterCommand command) async {
await tapButton(find.byType(AiWriterToolbarActionList));
await tapButton(find.text(command.i18n));
await pumpAndSettle();
}
Future<void> selectModel(String modelName) async {
await tapButton(find.byType(SelectModelMenu));
await tapButton(find.text(modelName));
await pumpAndSettle();
}
Future<void> enterTextInPromptTextField(String text) async {
// Wait for the text field to be visible
await pumpAndSettle();
// Find the ExtendedTextField widget
final textField = find.descendant(
of: find.byType(PromptInputTextField),
matching: find.byType(TextField),
);
expect(textField, findsOneWidget, reason: 'ExtendedTextField not found');
final widget = element(textField).widget as TextField;
expect(widget.enabled, isTrue, reason: 'TextField is not enabled');
await tap(textField);
testTextInput.enterText(text);
await pumpAndSettle(const Duration(milliseconds: 300));
}
}
@@ -28,8 +28,8 @@ extension AppFlowyAuthTest on WidgetTester {
await tapButton(find.byType(AccountSignInOutButton));
expectToSeeText(LocaleKeys.button_ok.tr());
await tapButtonWithName(LocaleKeys.button_ok.tr());
expectToSeeText(LocaleKeys.button_yes.tr());
await tapButtonWithName(LocaleKeys.button_yes.tr());
}
Future<void> tapSignInAsGuest() async {
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/ai/service/appflowy_ai_service.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/cloud_env_test.dart';
import 'package:appflowy/startup/entry_point.dart';
@@ -20,6 +21,8 @@ import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:universal_platform/universal_platform.dart';
import 'mock/mock_ai.dart';
class FlowyTestContext {
FlowyTestContext({required this.applicationDataDirectory});
@@ -33,13 +36,14 @@ extension AppFlowyTestBase on WidgetTester {
// use to specify the application data directory, if not specified, a temporary directory will be used.
String? dataDirectory,
Size windowSize = const Size(1600, 1200),
AuthenticatorType? cloudType,
String? email,
AuthenticatorType? cloudType,
AIRepository Function()? aiRepositoryBuilder,
}) async {
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
// Set the window size
await binding.setSurfaceSize(windowSize);
}
//cloudType = AuthenticatorType.appflowyCloudDevelop;
mockHotKeyManagerHandlers();
final applicationDataDirectory = dataDirectory ??
@@ -50,43 +54,12 @@ extension AppFlowyTestBase on WidgetTester {
await FlowyRunner.run(
AppFlowyApplication(),
IntegrationMode.integrationTest,
rustEnvsBuilder: () {
final rustEnvs = <String, String>{};
if (cloudType != null) {
switch (cloudType) {
case AuthenticatorType.local:
break;
case AuthenticatorType.appflowyCloudSelfHost:
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
break;
default:
throw Exception("not supported");
}
}
return rustEnvs;
},
didInitGetItCallback: () {
return Future(
() async {
if (cloudType != null) {
switch (cloudType) {
case AuthenticatorType.local:
await useLocalServer();
break;
case AuthenticatorType.appflowyCloudSelfHost:
await useTestSelfHostedAppFlowyCloud();
getIt.unregister<AuthService>();
getIt.registerFactory<AuthService>(
() => AppFlowyCloudMockAuthService(email: email),
);
default:
throw Exception("not supported");
}
}
},
);
},
rustEnvsBuilder: () => _buildRustEnvs(cloudType),
didInitGetItCallback: () => _initializeCloudServices(
cloudType: cloudType,
email: email,
aiRepositoryBuilder: aiRepositoryBuilder,
),
);
await waitUntilSignInPageShow();
@@ -95,6 +68,76 @@ extension AppFlowyTestBase on WidgetTester {
);
}
Map<String, String> _buildRustEnvs(AuthenticatorType? cloudType) {
final rustEnvs = <String, String>{};
if (cloudType != null) {
switch (cloudType) {
case AuthenticatorType.local:
break;
case AuthenticatorType.appflowyCloudSelfHost:
case AuthenticatorType.appflowyCloudDevelop:
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
break;
default:
throw Exception("Unsupported cloud type: $cloudType");
}
}
return rustEnvs;
}
Future<void> _initializeCloudServices({
required AuthenticatorType? cloudType,
String? email,
AIRepository Function()? aiRepositoryBuilder,
}) async {
if (cloudType == null) return;
switch (cloudType) {
case AuthenticatorType.local:
await useLocalServer();
break;
case AuthenticatorType.appflowyCloudSelfHost:
await _setupAppFlowyCloud(
useLocal: false,
email: email,
aiRepositoryBuilder: aiRepositoryBuilder,
);
break;
case AuthenticatorType.appflowyCloudDevelop:
await _setupAppFlowyCloud(
useLocal: integrationMode().isDevelop,
email: email,
aiRepositoryBuilder: aiRepositoryBuilder,
);
break;
default:
throw Exception("Unsupported cloud type: $cloudType");
}
}
Future<void> _setupAppFlowyCloud({
required bool useLocal,
String? email,
AIRepository Function()? aiRepositoryBuilder,
}) async {
if (useLocal) {
await useAppFlowyCloudDevelop("http://localhost");
} else {
await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl);
}
getIt.unregister<AuthService>();
getIt.unregister<AIRepository>();
getIt.registerFactory<AuthService>(
() => AppFlowyCloudMockAuthService(email: email),
);
getIt.registerFactory<AIRepository>(
aiRepositoryBuilder ?? () => MockAIRepository(),
);
}
void mockHotKeyManagerHandlers() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
@@ -275,10 +318,6 @@ extension AppFlowyFinderTestBase on CommonFinders {
}
}
Future<void> useTestSelfHostedAppFlowyCloud() async {
await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
}
Future<String> mockApplicationDataStorage({
// use to append after the application data directory
String? pathExtension,
@@ -0,0 +1,128 @@
import 'dart:async';
import 'package:appflowy/ai/service/ai_entities.dart';
import 'package:appflowy/ai/service/appflowy_ai_service.dart';
import 'package:appflowy/ai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
import 'package:mocktail/mocktail.dart';
final _mockAiMap = <CompletionTypePB, Map<String, List<String>>>{
CompletionTypePB.ImproveWriting: {
"I have an apple": [
"I",
"have",
"an",
"apple",
"and",
"a",
"banana",
],
},
CompletionTypePB.SpellingAndGrammar: {
"We didn’t had enough money": [
"We",
"didn’t",
"have",
"enough",
"money",
],
},
CompletionTypePB.UserQuestion: {
"Explain the concept of TPU": [
"TPU",
"is",
"a",
"tensor",
"processing",
"unit",
"that",
"is",
"designed",
"to",
"accelerate",
"machine",
],
"How about GPU?": [
"GPU",
"is",
"a",
"graphics",
"processing",
"unit",
"that",
"is",
"designed",
"to",
"accelerate",
"machine",
"learning",
"tasks",
],
},
};
abstract class StreamCompletionValidator {
bool validate(
String text,
String? objectId,
CompletionTypePB completionType,
PredefinedFormat? format,
List<String> sourceIds,
List<AiWriterRecord> history,
);
}
class MockCompletionStream extends Mock implements CompletionStream {}
class MockAIRepository extends Mock implements AppFlowyAIService {
MockAIRepository({this.validator});
StreamCompletionValidator? validator;
@override
Future<(String, CompletionStream)?> streamCompletion({
String? objectId,
required String text,
PredefinedFormat? format,
List<String> sourceIds = const [],
List<AiWriterRecord> history = const [],
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) processMessage,
required Future<void> Function(String text) processAssistMessage,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
required void Function(LocalAIStreamingState state)
onLocalAIStreamingStateChange,
}) async {
if (validator != null) {
if (!validator!.validate(
text,
objectId,
completionType,
format,
sourceIds,
history,
)) {
throw Exception('Invalid completion');
}
}
final stream = MockCompletionStream();
unawaited(
Future(() async {
await onStart();
final lines = _mockAiMap[completionType]?[text.trim()];
if (lines == null) {
throw Exception('No mock ai found for $text and $completionType');
}
for (final line in lines) {
await processMessage('$line ');
}
await onEnd();
}),
);
return ('mock_id', stream);
}
}
@@ -9,7 +9,6 @@ import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -86,7 +85,7 @@ extension AppFlowySettings on WidgetTester {
final userNameFinder = find.descendant(
of: find.byType(AccountUserProfile),
matching: find.byType(FlowyTextField),
matching: find.byType(TextField),
);
await enterText(userNameFinder, name);
await pumpAndSettle();
+30 -30
View File
@@ -8,35 +8,35 @@ PODS:
- ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.4):
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.4)
- DKImagePickerController/PhotoGallery (4.3.4):
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.4)
- DKPhotoGallery (0.0.17):
- DKPhotoGallery/Core (= 0.0.17)
- DKPhotoGallery/Model (= 0.0.17)
- DKPhotoGallery/Preview (= 0.0.17)
- DKPhotoGallery/Resource (= 0.0.17)
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.17):
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.17):
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.17):
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.17):
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
@@ -65,17 +65,17 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- ReachabilitySwift (5.0.0)
- ReachabilitySwift (5.2.4)
- saver_gallery (0.0.1):
- Flutter
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2)
- Sentry/HybridSDK (8.35.1)
- sentry_flutter (8.8.0):
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- Sentry/HybridSDK (8.46.0)
- sentry_flutter (8.14.2):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.35.1)
- Sentry/HybridSDK (= 8.46.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -86,8 +86,8 @@ PODS:
- FlutterMacOS
- super_native_extensions (0.0.1):
- Flutter
- SwiftyGif (5.4.3)
- Toast (4.0.0)
- SwiftyGif (5.4.5)
- Toast (4.1.1)
- url_launcher_ios (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
@@ -185,8 +185,8 @@ SPEC CHECKSUMS:
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
@@ -199,17 +199,17 @@ SPEC CHECKSUMS:
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
@@ -122,8 +122,6 @@ enum AiPromptCategory {
business,
@JsonValue("marketing")
marketing,
@JsonValue("learning")
learning,
@JsonValue("travel")
travel,
@JsonValue("contentSeo")
@@ -145,7 +143,17 @@ enum AiPromptCategory {
@JsonValue("caseStudies")
caseStudies,
@JsonValue("salesCopy")
salesCopy;
salesCopy,
@JsonValue("education")
education,
@JsonValue("work")
work,
@JsonValue("podcastProduction")
podcastProduction,
@JsonValue("copyWriting")
copyWriting,
@JsonValue("customerSuccess")
customerSuccess;
String get i18n {
return switch (this) {
@@ -155,7 +163,6 @@ enum AiPromptCategory {
healthAndFitness => LocaleKeys.ai_customPrompt_healthAndFitness.tr(),
business => LocaleKeys.ai_customPrompt_business.tr(),
marketing => LocaleKeys.ai_customPrompt_marketing.tr(),
learning => LocaleKeys.ai_customPrompt_learning.tr(),
travel => LocaleKeys.ai_customPrompt_travel.tr(),
contentSeo => LocaleKeys.ai_customPrompt_contentSeo.tr(),
emailMarketing => LocaleKeys.ai_customPrompt_emailMarketing.tr(),
@@ -167,6 +174,11 @@ enum AiPromptCategory {
strategy => LocaleKeys.ai_customPrompt_strategy.tr(),
caseStudies => LocaleKeys.ai_customPrompt_caseStudies.tr(),
salesCopy => LocaleKeys.ai_customPrompt_salesCopy.tr(),
education => LocaleKeys.ai_customPrompt_education.tr(),
work => LocaleKeys.ai_customPrompt_work.tr(),
podcastProduction => LocaleKeys.ai_customPrompt_podcastProduction.tr(),
copyWriting => LocaleKeys.ai_customPrompt_copyWriting.tr(),
customerSuccess => LocaleKeys.ai_customPrompt_customerSuccess.tr(),
};
}
}
@@ -179,7 +191,6 @@ class AiPrompt extends Equatable {
required this.content,
required this.category,
required this.example,
required this.sampleResponse,
required this.isFeatured,
});
@@ -191,14 +202,16 @@ class AiPrompt extends Equatable {
final String id;
final String name;
final String content;
@JsonKey(
unknownEnumValue: AiPromptCategory.other,
defaultValue: AiPromptCategory.other,
)
final AiPromptCategory category;
@JsonKey(defaultValue: "")
final String example;
@JsonKey(defaultValue: "")
final String sampleResponse;
@JsonKey(defaultValue: false)
final bool isFeatured;
@override
List<Object?> get props => [id, name, content, category, sampleResponse];
List<Object?> get props => [id, name, content, category, example, isFeatured];
}
@@ -7,7 +7,6 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:protobuf/protobuf.dart';
import 'package:universal_platform/universal_platform.dart';
typedef OnModelStateChangedCallback = void Function(AIModelState state);
@@ -52,25 +51,29 @@ class AIModelStateNotifier {
final String objectId;
final LocalAIStateListener? _localAIListener;
final AIModelSwitchListener _aiModelSwitchListener;
LocalAIPB? _localAIState;
AvailableModelsPB? _availableModels;
// callbacks
LocalAIPB? _localAIState;
ModelSelectionPB? _modelSelection;
AIModelState _currentState = _defaultState();
List<AIModelPB> _availableModels = [];
AIModelPB? _selectedModel;
final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];
final List<OnAvailableModelsChangedCallback>
_availableModelsChangedCallbacks = [];
/// Starts platform-specific listeners
void _startListening() {
if (UniversalPlatform.isDesktop) {
_localAIListener?.start(
stateCallback: (state) async {
_localAIState = state;
_notifyStateChanged();
_updateAll();
if (state.state == RunningStatePB.Running ||
state.state == RunningStatePB.Stopped) {
await _loadAvailableModels();
_notifyAvailableModelsChanged();
await _loadModelSelection();
_updateAll();
}
},
);
@@ -78,25 +81,25 @@ class AIModelStateNotifier {
_aiModelSwitchListener.start(
onUpdateSelectedModel: (model) async {
final updatedModels = _availableModels?.deepCopy()
?..selectedModel = model;
_availableModels = updatedModels;
_notifyAvailableModelsChanged();
_selectedModel = model;
_updateAll();
if (model.isLocal && UniversalPlatform.isDesktop) {
await _loadLocalAiState();
await _loadLocalState();
_updateAll();
}
_notifyStateChanged();
},
);
}
void _init() async {
await Future.wait([_loadLocalAiState(), _loadAvailableModels()]);
_notifyStateChanged();
_notifyAvailableModelsChanged();
Future<void> _init() async {
await Future.wait([
if (UniversalPlatform.isDesktop) _loadLocalState(),
_loadModelSelection(),
]);
_updateAll();
}
/// Register callbacks for state or available-models changes
void addListener({
OnModelStateChangedCallback? onStateChanged,
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
@@ -109,6 +112,7 @@ class AIModelStateNotifier {
}
}
/// Remove previously registered callbacks
void removeListener({
OnModelStateChangedCallback? onStateChanged,
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
@@ -128,116 +132,88 @@ class AIModelStateNotifier {
await _aiModelSwitchListener.stop();
}
AIModelState getState() {
if (UniversalPlatform.isMobile) {
return AIModelState(
type: AiType.cloud,
hintText: LocaleKeys.chat_inputMessageHint.tr(),
tooltip: null,
isEditable: true,
localAIEnabled: false,
);
/// Returns current AIModelState
AIModelState getState() => _currentState;
/// Returns available models and the selected model
(List<AIModelPB>, AIModelPB?) getModelSelection() =>
(_availableModels, _selectedModel);
void _updateAll() {
_currentState = _computeState();
for (final cb in _stateChangedCallbacks) {
cb(_currentState);
}
final availableModels = _availableModels;
final localAiState = _localAIState;
if (availableModels == null) {
return AIModelState(
type: AiType.cloud,
hintText: LocaleKeys.chat_inputMessageHint.tr(),
isEditable: true,
tooltip: null,
localAIEnabled: false,
);
}
if (localAiState == null) {
return AIModelState(
type: AiType.cloud,
hintText: LocaleKeys.chat_inputMessageHint.tr(),
tooltip: null,
isEditable: true,
localAIEnabled: false,
);
for (final cb in _availableModelsChangedCallbacks) {
cb(_availableModels, _selectedModel);
}
}
if (!availableModels.selectedModel.isLocal) {
return AIModelState(
type: AiType.cloud,
hintText: LocaleKeys.chat_inputMessageHint.tr(),
tooltip: null,
isEditable: true,
localAIEnabled: false,
);
}
final editable = localAiState.state == RunningStatePB.Running;
final tooltip = localAiState.enabled
? (editable
? null
: LocaleKeys.settings_aiPage_keys_localAINotReadyTextFieldPrompt
.tr())
: LocaleKeys.settings_aiPage_keys_localAIDisabledTextFieldPrompt.tr();
final hintText = localAiState.enabled
? (editable
? LocaleKeys.chat_inputLocalAIMessageHint.tr()
: LocaleKeys.settings_aiPage_keys_localAIInitializing.tr())
: LocaleKeys.settings_aiPage_keys_localAIDisabled.tr();
return AIModelState(
type: AiType.local,
hintText: hintText,
tooltip: tooltip,
isEditable: editable,
localAIEnabled: localAiState.enabled,
Future<void> _loadModelSelection() async {
await AIEventGetSourceModelSelection(
ModelSourcePB(source: objectId),
).send().fold(
(ms) {
_modelSelection = ms;
_availableModels = ms.models;
_selectedModel = ms.selectedModel;
},
(e) => Log.error("Failed to fetch models: \$e"),
);
}
(List<AIModelPB>, AIModelPB?) getAvailableModels() {
final availableModels = _availableModels;
if (availableModels == null) {
return ([], null);
}
return (availableModels.models, availableModels.selectedModel);
}
void _notifyAvailableModelsChanged() {
final (models, selectedModel) = getAvailableModels();
for (final callback in _availableModelsChangedCallbacks) {
callback(models, selectedModel);
}
}
void _notifyStateChanged() {
final state = getState();
for (final callback in _stateChangedCallbacks) {
callback(state);
}
}
Future<void> _loadAvailableModels() {
final payload = AvailableModelsQueryPB(source: objectId);
return AIEventGetAvailableModels(payload).send().fold(
(models) => _availableModels = models,
(err) => Log.error("Failed to get available models: $err"),
Future<void> _loadLocalState() async {
await AIEventGetLocalAIState().send().fold(
(s) => _localAIState = s,
(e) => Log.error("Failed to fetch local AI state: \$e"),
);
}
Future<void> _loadLocalAiState() {
return AIEventGetLocalAIState().send().fold(
(localAIState) => _localAIState = localAIState,
(error) => Log.error("Failed to get local AI state: $error"),
);
static AIModelState _defaultState() => AIModelState(
type: AiType.cloud,
hintText: LocaleKeys.chat_inputMessageHint.tr(),
tooltip: null,
isEditable: true,
localAIEnabled: false,
);
/// Core logic computing the state from local and selection data
AIModelState _computeState() {
if (UniversalPlatform.isMobile) return _defaultState();
if (_modelSelection == null || _localAIState == null) {
return _defaultState();
}
if (!_selectedModel!.isLocal) {
return _defaultState();
}
final enabled = _localAIState!.enabled;
final running = _localAIState!.state == RunningStatePB.Running;
final hintKey = enabled
? (running
? LocaleKeys.chat_inputLocalAIMessageHint
: LocaleKeys.settings_aiPage_keys_localAIInitializing)
: LocaleKeys.settings_aiPage_keys_localAIDisabled;
final tooltipKey = enabled
? (running
? null
: LocaleKeys.settings_aiPage_keys_localAINotReadyTextFieldPrompt)
: LocaleKeys.settings_aiPage_keys_localAIDisabledTextFieldPrompt;
return AIModelState(
type: AiType.local,
hintText: hintKey.tr(),
tooltip: tooltipKey?.tr(),
isEditable: running,
localAIEnabled: enabled,
);
}
}
extension AiModelExtension on AIModelPB {
bool get isDefault {
return name == "Auto";
}
String get i18n {
return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
}
extension AIModelPBExtension on AIModelPB {
bool get isDefault => name == 'Auto';
String get i18n =>
isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
}
@@ -29,12 +29,14 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
void _loadPrompts() async {
availablePrompts.addAll(await _aiService.getBuiltInPrompts());
final visiblePrompts = _getFilteredPrompts(availablePrompts);
final featuredPrompts =
availablePrompts.where((prompt) => prompt.isFeatured);
final visiblePrompts = _getFilteredPrompts(featuredPrompts);
emit(
AiPromptSelectorState.ready(
visiblePrompts: visiblePrompts,
showCustomPrompts: false,
isFeaturedCategorySelected: false,
isFeaturedCategorySelected: true,
selectedPromptId: visiblePrompts.firstOrNull?.id,
selectedCategory: null,
favoritePrompts: [],
@@ -45,12 +47,13 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
void selectFeaturedCategory() {
state.maybeMap(
ready: (readyState) {
final prompts = availablePrompts.where((prompt) => prompt.isFeatured);
final visiblePrompts = _getFilteredPrompts(prompts);
emit(
readyState.copyWith(
// TODO(RS): Add logic to filter prompts based on the featured category
// visiblePrompts: prompts,
visiblePrompts: visiblePrompts,
isFeaturedCategorySelected: true,
// selectedPromptId: prompts.firstOrNull?.id,
selectedPromptId: visiblePrompts.firstOrNull?.id,
),
);
},
@@ -136,22 +139,13 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
);
}
AiPrompt? get selectedPrompt {
return state.maybeMap(
ready: (readyState) {
return readyState.visiblePrompts.firstWhereOrNull(
(prompt) => prompt.id == readyState.selectedPromptId,
);
},
orElse: () => null,
);
}
void _filterTextChanged() {
state.maybeMap(
ready: (readyState) {
final unfilteredPrompts = readyState.selectedCategory == null
? availablePrompts
? readyState.isFeaturedCategorySelected
? availablePrompts.where((prompt) => prompt.isFeatured)
: availablePrompts
: availablePrompts.where(
(prompt) => prompt.category == readyState.selectedCategory,
);
@@ -23,7 +23,7 @@ enum LocalAIStreamingState {
}
abstract class AIRepository {
Future<void> streamCompletion({
Future<(String, CompletionStream)?> streamCompletion({
String? objectId,
required String text,
PredefinedFormat? format,
@@ -45,8 +45,6 @@ abstract class AIRepository {
}
class AppFlowyAIService implements AIRepository {
final List<AiPrompt> _builtInPrompts = [];
@override
Future<(String, CompletionStream)?> streamCompletion({
String? objectId,
@@ -98,10 +96,6 @@ class AppFlowyAIService implements AIRepository {
@override
Future<List<AiPrompt>> getBuiltInPrompts() async {
if (_builtInPrompts.isNotEmpty) {
return _builtInPrompts;
}
final prompts = <AiPrompt>[];
try {
@@ -120,8 +114,6 @@ class AppFlowyAIService implements AIRepository {
Log.error(e);
}
_builtInPrompts.addAll(prompts);
return prompts;
}
@@ -83,7 +83,7 @@ class SelectModelState with _$SelectModelState {
}) = _SelectModelState;
factory SelectModelState.initial(AIModelStateNotifier notifier) {
final (models, selectedModel) = notifier.getAvailableModels();
final (models, selectedModel) = notifier.getModelSelection();
return SelectModelState(
models: models,
selectedModel: selectedModel,
@@ -5,7 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AiPromptCategoryList extends StatelessWidget {
class AiPromptCategoryList extends StatefulWidget {
const AiPromptCategoryList({
super.key,
required this.padding,
@@ -13,19 +13,26 @@ class AiPromptCategoryList extends StatelessWidget {
final EdgeInsetsGeometry padding;
@override
State<AiPromptCategoryList> createState() => _AiPromptCategoryListState();
}
class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
bool isSearching = false;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Padding(
padding: padding,
padding: EdgeInsets.all(
theme.spacing.l,
),
child: TextFieldTapRegion(
groupId: "ai_prompt_category_list",
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: theme.spacing.s,
children: [
_buildSearchField(context),
_buildFeaturedCategory(context),
const AFDivider(),
Expanded(
@@ -37,7 +44,6 @@ class AiPromptCategoryList extends StatelessWidget {
_buildCategoryItem(context, AiPromptCategory.writing),
_buildCategoryItem(context, AiPromptCategory.business),
_buildCategoryItem(context, AiPromptCategory.marketing),
_buildCategoryItem(context, AiPromptCategory.learning),
_buildCategoryItem(
context,
AiPromptCategory.healthAndFitness,
@@ -53,6 +59,15 @@ class AiPromptCategoryList extends StatelessWidget {
_buildCategoryItem(context, AiPromptCategory.strategy),
_buildCategoryItem(context, AiPromptCategory.caseStudies),
_buildCategoryItem(context, AiPromptCategory.salesCopy),
_buildCategoryItem(context, AiPromptCategory.education),
_buildCategoryItem(context, AiPromptCategory.work),
_buildCategoryItem(
context,
AiPromptCategory.podcastProduction,
),
_buildCategoryItem(context, AiPromptCategory.copyWriting),
_buildCategoryItem(context, AiPromptCategory.customerSuccess),
_buildCategoryItem(context, AiPromptCategory.other),
],
),
),
@@ -62,16 +77,6 @@ class AiPromptCategoryList extends StatelessWidget {
);
}
Widget _buildSearchField(BuildContext context) {
return AFTextField(
groupId: "ai_prompt_category_list",
hintText: "Search",
size: AFTextFieldSize.m,
controller: context.read<AiPromptSelectorCubit>().filterTextController,
autoFocus: true,
);
}
Widget _buildCategoryItem(
BuildContext context,
AiPromptCategory? category,
@@ -98,15 +103,12 @@ class AiPromptCategoryList extends StatelessWidget {
}
},
builder: (context, isHovering, disabled) {
return Row(
children: [
Text(
LocaleKeys.ai_customPrompt_featured.tr(),
style: AppFlowyTheme.of(context).textStyle.body.standard(
color: theme.textColorScheme.primary,
),
),
],
return Text(
LocaleKeys.ai_customPrompt_featured.tr(),
style: AppFlowyTheme.of(context).textStyle.body.standard(
color: theme.textColorScheme.primary,
),
overflow: TextOverflow.ellipsis,
);
},
borderRadius: theme.borderRadius.m,
@@ -152,15 +154,12 @@ class AiPromptCategoryItem extends StatelessWidget {
return AFBaseButton(
onTap: onSelect,
builder: (context, isHovering, disabled) {
return Row(
children: [
Text(
category?.i18n ?? LocaleKeys.ai_customPrompt_all.tr(),
style: AppFlowyTheme.of(context).textStyle.body.standard(
color: theme.textColorScheme.primary,
),
),
],
return Text(
category?.i18n ?? LocaleKeys.ai_customPrompt_all.tr(),
style: AppFlowyTheme.of(context).textStyle.body.standard(
color: theme.textColorScheme.primary,
),
overflow: TextOverflow.ellipsis,
);
},
borderRadius: theme.borderRadius.m,
@@ -88,12 +88,7 @@ class AiPromptModal extends StatelessWidget {
),
Expanded(
flex: 2,
child: AiPromptVisibleList(
padding: EdgeInsets.symmetric(
vertical: theme.spacing.l,
horizontal: theme.spacing.l,
),
),
child: AiPromptVisibleList(),
),
Expanded(
flex: 3,
@@ -115,10 +110,6 @@ class AiPromptModal extends StatelessWidget {
}
return AiPromptPreview(
prompt: selectedPrompt,
padding: EdgeInsets.symmetric(
vertical: theme.spacing.l,
horizontal: theme.spacing.l,
),
);
},
),
@@ -131,20 +122,6 @@ class AiPromptModal extends StatelessWidget {
),
),
),
AFModalFooter(
trailing: [
AFFilledTextButton.primary(
text: LocaleKeys.ai_customPrompt_usePrompt.tr(),
onTap: () {
final selectedPrompt =
context.read<AiPromptSelectorCubit>().selectedPrompt;
if (selectedPrompt != null) {
Navigator.of(context).pop(selectedPrompt);
}
},
),
],
),
],
),
);
@@ -10,71 +10,83 @@ class AiPromptPreview extends StatelessWidget {
const AiPromptPreview({
super.key,
required this.prompt,
required this.padding,
});
final AiPrompt prompt;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return SelectionArea(
child: ListView(
padding: padding,
child: Column(
children: [
SelectionContainer.disabled(
child: Text(
prompt.name,
style: theme.textStyle.headline.standard(
color: theme.textColorScheme.primary,
Padding(
padding: EdgeInsets.only(
top: theme.spacing.l,
left: theme.spacing.l,
right: theme.spacing.l,
),
child: SelectionContainer.disabled(
child: Row(
children: [
Expanded(
child: Text(
prompt.name,
style: theme.textStyle.headline.standard(
color: theme.textColorScheme.primary,
),
),
),
HSpace(theme.spacing.s),
AFFilledTextButton.primary(
text: LocaleKeys.ai_customPrompt_usePrompt.tr(),
onTap: () {
Navigator.of(context).pop(prompt);
},
),
],
),
),
),
VSpace(theme.spacing.xs),
SelectionContainer.disabled(
child: Text(
LocaleKeys.ai_customPrompt_prompt.tr(),
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.primary,
Expanded(
child: ListView(
padding: EdgeInsets.all(
theme.spacing.l,
),
children: [
SelectionContainer.disabled(
child: Text(
LocaleKeys.ai_customPrompt_prompt.tr(),
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.primary,
),
),
),
VSpace(theme.spacing.xs),
_PromptContent(
prompt: prompt,
),
VSpace(theme.spacing.xl),
if (prompt.example.isNotEmpty) ...[
SelectionContainer.disabled(
child: Text(
LocaleKeys.ai_customPrompt_example.tr(),
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.primary,
),
),
),
VSpace(theme.spacing.xs),
_PromptExample(
prompt: prompt,
),
VSpace(theme.spacing.xl),
],
],
),
),
VSpace(theme.spacing.xs),
_PromptContent(
prompt: prompt,
),
VSpace(theme.spacing.xl),
if (prompt.example.isNotEmpty) ...[
SelectionContainer.disabled(
child: Text(
LocaleKeys.ai_customPrompt_example.tr(),
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.primary,
),
),
),
VSpace(theme.spacing.xs),
_PromptExample(
prompt: prompt,
),
VSpace(theme.spacing.xl),
],
if (prompt.sampleResponse.isNotEmpty) ...[
SelectionContainer.disabled(
child: Text(
LocaleKeys.ai_customPrompt_sampleOutput.tr(),
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.primary,
),
),
),
VSpace(theme.spacing.xs),
_PromptSampleOutput(
prompt: prompt,
),
],
],
),
);
@@ -175,27 +187,3 @@ class _PromptExample extends StatelessWidget {
);
}
}
class _PromptSampleOutput extends StatelessWidget {
const _PromptSampleOutput({
required this.prompt,
});
final AiPrompt prompt;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Container(
padding: EdgeInsets.all(theme.spacing.l),
decoration: BoxDecoration(
color: theme.fillColorScheme.quaternary,
borderRadius: BorderRadius.circular(theme.borderRadius.m),
),
child: AIMarkdownText(
markdown: prompt.sampleResponse,
),
);
}
}
@@ -1,9 +1,14 @@
import 'dart:async';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:diffutil_dart/diffutil.dart';
import 'package:flutter/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
const Duration _listItemAnimationDuration = Duration(milliseconds: 150);
@@ -11,11 +16,8 @@ const Duration _listItemAnimationDuration = Duration(milliseconds: 150);
class AiPromptVisibleList extends StatefulWidget {
const AiPromptVisibleList({
super.key,
required this.padding,
});
final EdgeInsetsGeometry padding;
@override
State<AiPromptVisibleList> createState() => _AiPromptVisibleListState();
}
@@ -25,18 +27,26 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
final scrollController = ScrollController();
final List<AiPrompt> oldList = [];
late AiPromptSelectorCubit cubit;
late bool filterIsEmpty;
@override
void initState() {
super.initState();
final prompts = context.read<AiPromptSelectorCubit>().state.maybeMap(
ready: (value) => value.visiblePrompts,
orElse: () => <AiPrompt>[],
);
cubit = context.read<AiPromptSelectorCubit>();
final textController = cubit.filterTextController;
final prompts = cubit.state.maybeMap(
ready: (value) => value.visiblePrompts,
orElse: () => <AiPrompt>[],
);
filterIsEmpty = textController.text.isEmpty;
textController.addListener(handleFilterTextChanged);
oldList.addAll(prompts);
}
@override
void dispose() {
cubit.filterTextController.removeListener(handleFilterTextChanged);
scrollController.dispose();
super.dispose();
}
@@ -52,37 +62,85 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
);
handleVisiblePromptListChanged(list);
},
child: AnimatedList(
controller: scrollController,
padding: widget.padding,
key: listKey,
initialItemCount: oldList.length,
itemBuilder: (context, index, animation) {
final cubit = context.read<AiPromptSelectorCubit>();
child: Column(
children: [
VSpace(spacing.l),
Padding(
padding: EdgeInsets.symmetric(horizontal: spacing.l),
child: buildSearchField(context),
),
VSpace(spacing.s),
Expanded(
child: AnimatedList(
controller: scrollController,
padding: EdgeInsets.all(spacing.l),
key: listKey,
initialItemCount: oldList.length,
itemBuilder: (context, index, animation) {
final cubit = context.read<AiPromptSelectorCubit>();
return cubit.state.maybeMap(
ready: (state) {
final prompt = state.visiblePrompts[index];
return Padding(
padding: EdgeInsets.only(
top: index == 0 ? 0 : spacing.s,
bottom:
index == state.visiblePrompts.length - 1 ? 0 : spacing.s,
),
child: _AiPromptListItem(
animation: animation,
prompt: prompt,
isSelected: state.selectedPromptId == prompt.id,
),
);
},
orElse: () => const SizedBox.shrink(),
);
},
return cubit.state.maybeMap(
ready: (state) {
final prompt = state.visiblePrompts[index];
return Padding(
padding: EdgeInsets.only(
top: index == 0 ? 0 : spacing.s,
bottom: index == state.visiblePrompts.length - 1
? 0
: spacing.s,
),
child: _AiPromptListItem(
animation: animation,
prompt: prompt,
isSelected: state.selectedPromptId == prompt.id,
),
);
},
orElse: () => const SizedBox.shrink(),
);
},
),
),
],
),
);
}
Widget buildSearchField(BuildContext context) {
final theme = AppFlowyTheme.of(context);
final iconSize = 20.0;
return AFTextField(
groupId: "ai_prompt_category_list",
hintText: "Search",
controller: context.read<AiPromptSelectorCubit>().filterTextController,
autoFocus: true,
suffixIconConstraints: BoxConstraints.tightFor(
width: iconSize + theme.spacing.m,
height: iconSize,
),
suffixIconBuilder: filterIsEmpty
? null
: (context, isObscured) => TextFieldTapRegion(
groupId: "ai_prompt_category_list",
child: Padding(
padding: EdgeInsets.only(right: theme.spacing.m),
child: GestureDetector(
onTap: () => context
.read<AiPromptSelectorCubit>()
.filterTextController
.clear(),
child: FlowySvg(
FlowySvgs.toast_close_s,
color: theme.textColorScheme.secondary,
size: const Size.square(20),
),
),
),
),
);
}
void handleVisiblePromptListChanged(
List<AiPrompt> newList,
) {
@@ -122,6 +180,12 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
..clear()
..addAll(newList);
}
void handleFilterTextChanged() {
setState(() {
filterIsEmpty = cubit.filterTextController.text.isEmpty;
});
}
}
class _AiPromptListItem extends StatefulWidget {
@@ -146,6 +210,7 @@ class _AiPromptListItemState extends State<_AiPromptListItem> {
@override
void dispose() {
timer?.cancel();
timer = null;
super.dispose();
}
@@ -159,6 +224,9 @@ class _AiPromptListItemState extends State<_AiPromptListItem> {
curve: Curves.easeIn,
);
final surfacePrimaryHover =
Theme.of(context).isLightMode ? Color(0xFFF8FAFF) : Color(0xFF3C3F4E);
return FadeTransition(
opacity: curvedAnimation,
child: SizeTransition(
@@ -167,74 +235,103 @@ class _AiPromptListItemState extends State<_AiPromptListItem> {
onEnter: (_) {
setState(() {
isHovering = true;
timer = Timer(
const Duration(milliseconds: 300),
() {
if (isHovering) {
cubit.selectPrompt(widget.prompt.id);
}
},
);
timer = Timer(const Duration(milliseconds: 300), () {
if (mounted) {
cubit.selectPrompt(widget.prompt.id);
}
});
});
},
onExit: (_) {
setState(() {
isHovering = false;
timer?.cancel();
timer = null;
});
},
child: GestureDetector(
onTap: () {
cubit.selectPrompt(widget.prompt.id);
},
onDoubleTap: () {
Navigator.of(context).pop(widget.prompt);
},
child: Container(
padding: EdgeInsets.all(theme.spacing.m),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(theme.borderRadius.m),
border: Border.all(
color: widget.isSelected
? isHovering
? theme.borderColorScheme.themeThickHover
: theme.borderColorScheme.themeThick
: isHovering
? theme.borderColorScheme.greyTertiaryHover
: theme.borderColorScheme.greyTertiary,
),
color: theme.surfaceColorScheme.primary,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
child: Stack(
children: [
Container(
padding: EdgeInsets.all(theme.spacing.m),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(theme.borderRadius.m),
border: Border.all(
color: widget.isSelected
? isHovering
? theme.borderColorScheme.themeThickHover
: theme.borderColorScheme.themeThick
: isHovering
? theme.borderColorScheme.greyTertiaryHover
: theme.borderColorScheme.greyTertiary,
),
color: theme.surfaceColorScheme.primary,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
widget.prompt.name,
maxLines: 1,
style: theme.textStyle.body.standard(
color: theme.textColorScheme.primary,
Row(
children: [
Expanded(
child: Text(
widget.prompt.name,
maxLines: 1,
style: theme.textStyle.body.standard(
color: theme.textColorScheme.primary,
),
overflow: TextOverflow.ellipsis,
softWrap: true,
),
),
overflow: TextOverflow.ellipsis,
softWrap: true,
],
),
Text(
widget.prompt.content,
maxLines: 2,
style: theme.textStyle.caption.standard(
color: theme.textColorScheme.secondary,
),
overflow: TextOverflow.ellipsis,
softWrap: true,
),
],
),
Text(
widget.prompt.content,
maxLines: 2,
style: theme.textStyle.caption.standard(
color: theme.textColorScheme.secondary,
),
if (isHovering)
Positioned(
top: theme.spacing.s,
right: theme.spacing.s,
child: DecoratedBox(
decoration: BoxDecoration(boxShadow: theme.shadow.small),
child: AFBaseButton(
onTap: () {
Navigator.of(context).pop(widget.prompt);
},
builder: (context, isHovering, disabled) {
return Text(
LocaleKeys.ai_customPrompt_usePrompt.tr(),
style: theme.textStyle.body.standard(
color: theme.textColorScheme.primary,
),
);
},
backgroundColor: (context, isHovering, disabled) {
if (isHovering) {
return surfacePrimaryHover;
}
return theme.surfaceColorScheme.primary;
},
padding: EdgeInsets.symmetric(
vertical: theme.spacing.s,
horizontal: theme.spacing.m,
),
borderRadius: theme.borderRadius.m,
),
),
overflow: TextOverflow.ellipsis,
softWrap: true,
),
],
),
],
),
),
),
@@ -260,11 +260,12 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
if (widget.isStreaming) {
return;
}
final trimmedText = inputControlCubit.formatIntputText(
widget.textController.text.trim(),
);
String userInput = widget.textController.text.trim();
userInput = inputControlCubit.formatIntputText(userInput);
userInput = AiPromptInputTextEditingController.restore(userInput);
widget.textController.clear();
if (trimmedText.isEmpty) {
if (userInput.isEmpty) {
return;
}
@@ -276,7 +277,7 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
final predefinedFormat = bloc.state.predefinedFormat;
widget.onSubmitted(
trimmedText,
userInput,
showPredefinedFormats ? predefinedFormat : null,
metadata,
);
@@ -57,7 +57,8 @@ class AiPromptInputTextEditingController extends TextEditingController {
text: restore(string),
style: theme.textStyle.body.standard().copyWith(
color: theme.textColorScheme.purple,
backgroundColor: theme.fillColorScheme.purpleLight,
backgroundColor:
theme.fillColorScheme.purpleThick.withAlpha(51),
),
),
);
@@ -90,38 +90,40 @@ class SelectModelPopoverContent extends StatelessWidget {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (localModels.isNotEmpty) ...[
_ModelSectionHeader(
title: LocaleKeys.chat_switchModel_localModel.tr(),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (localModels.isNotEmpty) ...[
_ModelSectionHeader(
title: LocaleKeys.chat_switchModel_localModel.tr(),
),
const VSpace(4.0),
],
...localModels.map(
(model) => _ModelItem(
model: model,
isSelected: model == selectedModel,
onTap: () => onSelectModel?.call(model),
),
),
if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[
const VSpace(8.0),
_ModelSectionHeader(
title: LocaleKeys.chat_switchModel_cloudModel.tr(),
),
const VSpace(4.0),
],
...cloudModels.map(
(model) => _ModelItem(
model: model,
isSelected: model == selectedModel,
onTap: () => onSelectModel?.call(model),
),
),
const VSpace(4.0),
],
...localModels.map(
(model) => _ModelItem(
model: model,
isSelected: model == selectedModel,
onTap: () => onSelectModel?.call(model),
),
),
if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[
const VSpace(8.0),
_ModelSectionHeader(
title: LocaleKeys.chat_switchModel_cloudModel.tr(),
),
const VSpace(4.0),
],
...cloudModels.map(
(model) => _ModelItem(
model: model,
isSelected: model == selectedModel,
onTap: () => onSelectModel?.call(model),
),
),
],
),
),
);
}
@@ -215,45 +217,41 @@ class _CurrentModelButton extends StatelessWidget {
behavior: HitTestBehavior.opaque,
child: SizedBox(
height: DesktopAIPromptSizes.actionBarButtonSize,
child: AnimatedSize(
duration: const Duration(milliseconds: 50),
curve: Curves.easeInOut,
alignment: AlignmentDirectional.centerStart,
child: FlowyHover(
style: const HoverStyle(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Padding(
padding: const EdgeInsetsDirectional.all(4.0),
child: Row(
children: [
Padding(
// TODO: remove this after change icon to 20px
padding: EdgeInsets.all(2),
child: FlowySvg(
FlowySvgs.ai_sparks_s,
color: Theme.of(context).hintColor,
size: Size.square(16),
),
),
if (model != null && !model!.isDefault)
Padding(
padding: EdgeInsetsDirectional.only(end: 2.0),
child: FlowyText(
model!.i18n,
fontSize: 12,
figmaLineHeight: 16,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
FlowySvg(
FlowySvgs.ai_source_drop_down_s,
child: FlowyHover(
style: const HoverStyle(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Padding(
padding: const EdgeInsetsDirectional.all(4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
// TODO: remove this after change icon to 20px
padding: EdgeInsets.all(2),
child: FlowySvg(
FlowySvgs.ai_sparks_s,
color: Theme.of(context).hintColor,
size: const Size.square(8),
size: Size.square(16),
),
],
),
),
if (model != null && !model!.isDefault)
Padding(
padding: EdgeInsetsDirectional.only(end: 2.0),
child: FlowyText(
model!.i18n,
fontSize: 12,
figmaLineHeight: 16,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
FlowySvg(
FlowySvgs.ai_source_drop_down_s,
color: Theme.of(context).hintColor,
size: const Size.square(8),
),
],
),
),
),
@@ -54,7 +54,7 @@ class _PromptInputMobileSelectSourcesButtonState
Widget build(BuildContext context) {
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
builder: (context, state) {
final userProfile = context.read<UserWorkspaceBloc>().userProfile;
final userProfile = context.read<UserWorkspaceBloc>().state.userProfile;
final workspaceId = state.currentWorkspace?.workspaceId ?? '';
return MultiBlocProvider(
providers: [
@@ -60,7 +60,7 @@ class _PromptInputDesktopSelectSourcesButtonState
@override
Widget build(BuildContext context) {
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final userProfile = userWorkspaceBloc.userProfile;
final userProfile = userWorkspaceBloc.state.userProfile;
final workspaceId =
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
@@ -39,8 +39,11 @@ Future<bool> afLaunchUri(
);
}
// on Linux, add http scheme to the url if it is not present
if (UniversalPlatform.isLinux && !isURL(url, {'require_protocol': true})) {
// on Linux or Android or Windows, add http scheme to the url if it is not present
if ((UniversalPlatform.isLinux ||
UniversalPlatform.isAndroid ||
UniversalPlatform.isWindows) &&
!isURL(url, {'require_protocol': true})) {
uri = Uri.parse('https://$url');
}
+6 -1
View File
@@ -167,11 +167,16 @@ Future<void> useBaseWebDomain(String? url) async {
);
}
Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
Future<void> useSelfHostedAppFlowyCloud(String url) async {
await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);
await _setAppFlowyCloudUrl(url);
}
Future<void> useAppFlowyCloudDevelop(String url) async {
await _setAuthenticatorType(AuthenticatorType.appflowyCloudDevelop);
await _setAppFlowyCloudUrl(url);
}
Future<void> useAppFlowyBetaCloudWithURL(
String url,
AuthenticatorType authenticatorType,
@@ -30,7 +30,7 @@ class NotificationReminderBloc
add(const NotificationReminderEvent.reset());
},
reset: () async {
final createdAt = await _getCreatedAt(
final scheduledAt = await _getScheduledAt(
reminder,
dateFormat,
timeFormat,
@@ -40,23 +40,24 @@ class NotificationReminderBloc
if (view == null) {
emit(
NotificationReminderState(
createdAt: createdAt,
scheduledAt: scheduledAt,
pageTitle: '',
reminderContent: '',
isLocked: false,
status: NotificationReminderStatus.error,
),
);
return;
}
final layout = view!.layout;
final layout = view.layout;
if (layout.isDocumentView) {
final node = await _getContent(reminder);
if (node != null) {
emit(
NotificationReminderState(
createdAt: createdAt,
scheduledAt: scheduledAt,
pageTitle: view.nameOrDefault,
isLocked: view.isLocked,
view: view,
@@ -70,7 +71,7 @@ class NotificationReminderBloc
} else if (layout.isDatabaseView) {
emit(
NotificationReminderState(
createdAt: createdAt,
scheduledAt: scheduledAt,
pageTitle: view.nameOrDefault,
isLocked: view.isLocked,
view: view,
@@ -88,20 +89,16 @@ class NotificationReminderBloc
late final UserDateFormatPB dateFormat;
late final UserTimeFormatPB timeFormat;
Future<String> _getCreatedAt(
Future<String> _getScheduledAt(
ReminderPB reminder,
UserDateFormatPB dateFormat,
UserTimeFormatPB timeFormat,
) async {
final rCreatedAt = reminder.createdAt;
final createdAt = rCreatedAt != null
? _formatTimestamp(
rCreatedAt,
timeFormat: timeFormat,
dateFormate: dateFormat,
)
: '';
return createdAt;
return _formatTimestamp(
reminder.scheduledAt.toInt() * 1000,
timeFormat: timeFormat,
dateFormate: dateFormat,
);
}
Future<ViewPB?> _getView(ReminderPB reminder) async {
@@ -203,7 +200,7 @@ class NotificationReminderState with _$NotificationReminderState {
const NotificationReminderState._();
const factory NotificationReminderState({
required String createdAt,
required String scheduledAt,
required String pageTitle,
required String reminderContent,
required bool isLocked,
@@ -216,7 +213,7 @@ class NotificationReminderState with _$NotificationReminderState {
factory NotificationReminderState.initial() =>
const NotificationReminderState(
createdAt: '',
scheduledAt: '',
pageTitle: '',
reminderContent: '',
isLocked: false,
@@ -53,6 +53,7 @@ class _MobileBottomSheetEditLinkWidgetState
ViewPB? currentView;
bool showErrorText = false;
bool showRemoveLink = false;
String title = LocaleKeys.editor_editLink.tr();
AppFlowyThemeData get theme => AppFlowyTheme.of(context);
@@ -73,6 +74,7 @@ class _MobileBottomSheetEditLinkWidgetState
)..searchRecentViews();
if (linkInfo.link.isEmpty) {
isShowingSearchResult = true;
title = LocaleKeys.toolbar_addLink.tr();
} else {
showRemoveLink = true;
textFocusNode.requestFocus();
@@ -104,7 +106,7 @@ class _MobileBottomSheetEditLinkWidgetState
child: Column(
children: [
BottomSheetHeader(
title: LocaleKeys.editor_editLink.tr(),
title: title,
onClose: () => context.pop(),
confirmButton: FlowyTextButton(
LocaleKeys.button_done.tr(),
@@ -163,6 +165,7 @@ class _MobileBottomSheetEditLinkWidgetState
decoration: LinkStyle.buildLinkTextFieldInputDecoration(
LocaleKeys.document_toolbar_linkNameHint.tr(),
contentPadding: EdgeInsets.all(14),
radius: 12,
context,
),
),
@@ -186,6 +189,11 @@ class _MobileBottomSheetEditLinkWidgetState
autofocus: true,
context: context,
contentPadding: EdgeInsets.all(14),
textStyle: TextStyle(
fontSize: 16,
height: 20 / 16,
fontWeight: FontWeight.w400,
),
),
VSpace(6),
searchTextField.buildResultContainer(
@@ -267,6 +275,7 @@ class _MobileBottomSheetEditLinkWidgetState
height: 48,
decoration: buildBorderDecoration(),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: showSearchResult,
child: Padding(
padding: EdgeInsets.all(12),
@@ -291,6 +300,7 @@ class _MobileBottomSheetEditLinkWidgetState
Widget buildRemoveLink() {
if (!showRemoveLink) return SizedBox.shrink();
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
widget.onRemoveLink(linkInfo);
context.pop();
@@ -203,7 +203,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
final userProfile = context.read<MobileViewPageBloc>().state.userProfilePB;
// the publish feature is only available for AppFlowy Cloud
if (userProfile == null ||
userProfile.workspaceAuthType != AuthTypePB.Server) {
userProfile.workspaceType != WorkspaceTypePB.ServerW) {
return [];
}
@@ -1,5 +1,6 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -235,6 +236,7 @@ class BottomSheetHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: SizedBox(
@@ -265,11 +267,11 @@ class BottomSheetHeader extends StatelessWidget {
Align(
child: Container(
constraints: const BoxConstraints(maxWidth: 250),
child: FlowyText(
child: Text(
title,
fontSize: 17.0,
fontWeight: FontWeight.w500,
overflow: TextOverflow.ellipsis,
style: theme.textStyle.heading4.prominent(
color: theme.textColorScheme.primary,
),
),
),
),
@@ -27,7 +27,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:sentry/sentry.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key});
@@ -65,14 +64,6 @@ class MobileHomeScreen extends StatelessWidget {
return const WorkspaceFailedScreen();
}
Sentry.configureScope(
(scope) => scope.setUser(
SentryUser(
id: userProfile.id.toString(),
),
),
);
return Scaffold(
body: SafeArea(
bottom: false,
@@ -194,7 +194,7 @@ class _MobileWorkspace extends StatelessWidget {
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.openWorkspace(
workspace.workspaceId,
workspace.workspaceAuthType,
workspace.workspaceType,
),
);
},
@@ -85,7 +85,8 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
PersonalInfoSettingGroup(
userProfile: userProfile,
),
const WorkspaceSettingGroup(),
if (state.userProfile.userAuthType == AuthTypePB.Server)
const WorkspaceSettingGroup(),
const AppearanceSettingGroup(),
const LanguageSettingGroup(),
if (Env.enableCustomCloud) const CloudSettingGroup(),
@@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget {
text: LocaleKeys.settings_popupMenuItem_settings.tr(),
),
// only show the member items in cloud mode
if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[
if (userProfile.workspaceType == WorkspaceTypePB.ServerW) ...[
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _MobileSettingsPopupMenuItem.members,
@@ -200,7 +200,9 @@ class MobileViewPage extends StatelessWidget {
const WidgetSpan(child: HSpace(8.0)),
],
TextSpan(
text: name,
text: name.orDefault(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
),
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.w600,
@@ -164,7 +164,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
return const MobileRecentSpace();
case MobileSpaceTabType.spaces:
final showAIFloatingButton =
widget.userProfile.workspaceAuthType == AuthTypePB.Server;
widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;
return Stack(
children: [
MobileHomeSpace(userProfile: widget.userProfile),
@@ -123,7 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget {
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.createWorkspace(
name,
AuthTypePB.Server,
WorkspaceTypePB.ServerW,
),
);
},
@@ -6,10 +6,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
import 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';
import 'package:appflowy/shared/red_dot.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/presentation/notifications/number_red_dot.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -175,23 +175,28 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
final hasUnreads = state.reminders.any(
(reminder) => !reminder.isRead,
);
return Stack(
children: [
isActive
? const FlowySvg(
FlowySvgs.m_home_active_notification_m,
blendMode: null,
)
: const FlowySvg(
FlowySvgs.m_home_notification_m,
),
if (hasUnreads)
const Positioned(
top: 2,
right: 4,
child: NotificationRedDot(),
return SizedBox(
width: 40,
height: 40,
child: Stack(
children: [
Center(
child: isActive
? const FlowySvg(
FlowySvgs.m_home_active_notification_m,
blendMode: null,
)
: const FlowySvg(
FlowySvgs.m_home_notification_m,
),
),
],
if (hasUnreads)
const Align(
alignment: Alignment.topRight,
child: NumberedRedDot.mobile(),
),
],
),
);
},
),
@@ -126,7 +126,6 @@ class _NotificationScreenContent extends StatelessWidget {
onAction: _onAction,
onReadChanged: _onReadChanged,
actionBar: InboxActionBar(
hasUnreads: state.hasUnreads,
showUnreadsOnly: filterState.showUnreadsOnly,
),
),
@@ -101,9 +101,7 @@ class _InnerNotificationItem extends StatelessWidget {
NotificationCheckIcon(
isSelected: mSelectedNotificationIds.value.contains(reminder.id),
),
const HSpace(3.0),
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
const HSpace(3.0),
const HSpace(12.0),
NotificationIcon(reminder: reminder),
const HSpace(12.0),
Expanded(
@@ -48,14 +48,19 @@ class NotificationItem extends StatelessWidget {
return const SizedBox.shrink();
}
final child = Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: _SlidableNotificationItem(
tabType: tabType,
reminder: reminder,
child: _InnerNotificationItem(
final child = GestureDetector(
onLongPress: () {
context.onMoreAction();
},
child: Padding(
padding: const EdgeInsets.all(8),
child: _SlidableNotificationItem(
tabType: tabType,
reminder: reminder,
child: _InnerNotificationItem(
tabType: tabType,
reminder: reminder,
),
),
),
);
@@ -102,14 +107,11 @@ class _InnerNotificationItem extends StatelessWidget {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const HSpace(8.0),
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
const HSpace(4.0),
const HSpace(10.0),
NotificationIcon(reminder: reminder),
const HSpace(12.0),
Expanded(
child: NotificationContent(reminder: reminder),
),
Expanded(child: NotificationContent(reminder: reminder)),
const HSpace(6.0),
],
);
}
@@ -10,11 +10,11 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_ui/appflowy_ui.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:universal_platform/universal_platform.dart';
const _kNotificationIconHeight = 36.0;
@@ -22,12 +22,15 @@ class NotificationIcon extends StatelessWidget {
const NotificationIcon({
super.key,
required this.reminder,
this.atSize = 12,
});
final ReminderPB reminder;
final double atSize;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return SizedBox(
width: 42,
height: 36,
@@ -45,13 +48,13 @@ class NotificationIcon extends StatelessWidget {
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).cardColor,
color: theme.fillColorScheme.primary,
),
child: Center(
child: FlowyText(
'@',
fontSize: 12,
figmaLineHeight: 14,
child: FlowySvg(
FlowySvgs.notification_icon_at_s,
size: Size.square(atSize),
color: theme.iconColorScheme.primary,
),
),
),
@@ -88,14 +91,15 @@ class UnreadRedDot extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const SizedBox(
final theme = AppFlowyTheme.of(context);
return SizedBox(
height: _kNotificationIconHeight,
child: Center(
child: SizedBox.square(
dimension: 6.0,
dimension: 7.0,
child: DecoratedBox(
decoration: ShapeDecoration(
color: Color(0xFFFF6331),
color: theme.borderColorScheme.errorThick,
shape: OvalBorder(),
),
),
@@ -118,6 +122,8 @@ class NotificationContent extends StatefulWidget {
}
class _NotificationContentState extends State<NotificationContent> {
AppFlowyThemeData get theme => AppFlowyTheme.of(context);
@override
void didUpdateWidget(covariant NotificationContent oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -141,14 +147,11 @@ class _NotificationContentState extends State<NotificationContent> {
mainAxisSize: MainAxisSize.min,
children: [
// title & time
_buildHeader(state.createdAt, !widget.reminder.isRead),
_buildHeader(state.scheduledAt, !widget.reminder.isRead),
// page name
_buildPageName(context, state.isLocked, state.pageTitle),
// content
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: _buildContent(view, nodes: state.nodes),
),
_buildContent(view, nodes: state.nodes),
],
);
},
@@ -175,6 +178,8 @@ class _NotificationContentState extends State<NotificationContent> {
fontSize: 14,
figmaLineHeight: 22,
color: context.notificationItemTextColor,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
);
}
@@ -191,6 +196,7 @@ class _NotificationContentState extends State<NotificationContent> {
LocaleKeys.settings_notifications_titles_reminder.tr(),
fontSize: 14,
figmaLineHeight: 20,
color: theme.textColorScheme.primary,
),
Spacer(),
if (createAt.isNotEmpty)
@@ -198,7 +204,7 @@ class _NotificationContentState extends State<NotificationContent> {
createAt,
fontSize: 12,
figmaLineHeight: 18,
color: context.notificationItemTextColor,
color: theme.textColorScheme.secondary,
),
if (unread) ...[
HSpace(4),
@@ -225,19 +231,25 @@ class _NotificationContentState extends State<NotificationContent> {
LocaleKeys.notificationHub_mentionedYou.tr(),
fontSize: 12,
figmaLineHeight: 18,
color: context.notificationItemTextColor,
color: theme.textColorScheme.secondary,
),
const NotificationEllipse(),
if (isLocked)
Padding(
padding: EdgeInsets.only(right: 5),
child: FlowySvg(FlowySvgs.notification_lock_s),
child: FlowySvg(
FlowySvgs.notification_lock_s,
color: theme.iconColorScheme.secondary,
),
),
Flexible(
child: FlowyText.regular(
pageTitle,
fontSize: 12,
figmaLineHeight: 18,
color: theme.textColorScheme.secondary,
overflow: TextOverflow.ellipsis,
),
FlowyText.regular(
pageTitle,
fontSize: 12,
figmaLineHeight: 18,
color: context.notificationItemTextColor,
),
],
),
@@ -254,7 +266,7 @@ class NotificationEllipse extends StatelessWidget {
return Container(
width: 2.50,
height: 2.50,
margin: const EdgeInsets.symmetric(horizontal: 5.0),
margin: const EdgeInsets.symmetric(horizontal: 6.0),
decoration: ShapeDecoration(
color: context.notificationItemTextColor,
shape: const OvalBorder(),
@@ -310,11 +322,7 @@ class NotificationDocumentContent extends StatelessWidget {
styleCustomizer: styleCustomizer,
// the editor is not editable in the chat
editable: false,
customHeadingPadding: UniversalPlatform.isDesktop
? EdgeInsets.zero
: EdgeInsets.symmetric(
vertical: EditorStyleCustomizer.nodeHorizontalPadding,
),
customPadding: (node) => EdgeInsets.zero,
);
return IgnorePointer(
@@ -76,41 +76,43 @@ enum NotificationPaneActionType {
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
onPressed: (context) {
final reminderBloc = context.read<ReminderBloc>();
final notificationReminderBloc =
context.read<NotificationReminderBloc>();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: reminderBloc),
BlocProvider.value(value: notificationReminderBloc),
],
child: _NotificationMoreActions(
onClickMultipleChoice: () {
Future.delayed(const Duration(milliseconds: 250), () {
bottomNavigationBarType.value =
BottomNavigationBarActionType
.notificationMultiSelect;
});
},
),
);
},
);
},
onPressed: (context) => context.onMoreAction(),
);
}
}
}
extension NotificationPaneActionExtension on BuildContext {
void onMoreAction() {
final reminderBloc = read<ReminderBloc>();
final notificationReminderBloc = read<NotificationReminderBloc>();
showMobileBottomSheet(
this,
showDragHandle: true,
showDivider: false,
useRootNavigator: true,
backgroundColor: Theme.of(this).colorScheme.surface,
builder: (_) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: reminderBloc),
BlocProvider.value(value: notificationReminderBloc),
],
child: _NotificationMoreActions(
onClickMultipleChoice: () {
Future.delayed(const Duration(milliseconds: 250), () {
bottomNavigationBarType.value =
BottomNavigationBarActionType.notificationMultiSelect;
});
},
),
);
},
);
}
}
class _NotificationMoreActions extends StatelessWidget {
const _NotificationMoreActions({
required this.onClickMultipleChoice,
@@ -1,4 +1,3 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';
@@ -23,12 +22,18 @@ class MobileSearchResultCell extends StatelessWidget {
final theme = AppFlowyTheme.of(context),
textColor = theme.textColorScheme.primary;
final commandPaletteState = context.read<CommandPaletteBloc>().state;
final displayName = item.displayName.isEmpty
? LocaleKeys.menuAppHeader_defaultNewPageName.tr()
: item.displayName;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildIcon(),
SizedBox.square(
dimension: 24,
child: Center(child: buildIcon(theme)),
),
HSpace(12),
Flexible(
child: Column(
@@ -38,7 +43,7 @@ class MobileSearchResultCell extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: buildHighLightSpan(
content: item.displayName,
content: displayName,
normal: theme.textStyle.heading4.standard(color: textColor),
highlight: theme.textStyle.heading4
.standard(color: textColor)
@@ -48,7 +53,7 @@ class MobileSearchResultCell extends StatelessWidget {
),
),
buildPath(commandPaletteState, theme),
buildSummary(theme),
...buildSummary(theme),
],
),
),
@@ -57,76 +62,57 @@ class MobileSearchResultCell extends StatelessWidget {
);
}
Widget buildIcon() {
Widget buildIcon(AppFlowyThemeData theme) {
final icon = item.icon;
if (icon.ty == ResultIconTypePB.Emoji) {
return icon.getIcon(size: 16.0, lineHeight: 20 / 16) ?? SizedBox.shrink();
} else {
return icon.getIcon(size: 20) ?? SizedBox.shrink();
} else {
return icon.getIcon(size: 20, iconColor: theme.iconColorScheme.primary) ??
SizedBox.shrink();
}
}
Widget buildPath(CommandPaletteState state, AppFlowyThemeData theme) {
bool isInTrash = false;
for (final view in state.trash) {
if (view.id == item.id) {
isInTrash = true;
break;
}
}
if (isInTrash) {
return Row(
children: [
const FlowySvg(FlowySvgs.trash_s, size: Size.square(20)),
const HSpace(4.0),
Text(
'${LocaleKeys.trash_text.tr()} / ${item.displayName}',
return BlocProvider(
create: (context) => ViewAncestorBloc(item.id),
child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(
builder: (context, state) {
final ancestors = state.ancestor.ancestors;
List<String> displayPath = ancestors.map((e) => e.name).toList();
if (ancestors.length > 2) {
displayPath = [ancestors.first.name, '...', ancestors.last.name];
}
return Text(
displayPath.join(' / '),
style: theme.textStyle.body
.standard(color: theme.textColorScheme.secondary),
),
],
);
} else {
return BlocProvider(
create: (context) => ViewAncestorBloc(item.id),
child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(
builder: (context, state) {
final ancestors = state.ancestor.ancestors;
List<String> displayPath = ancestors.map((e) => e.name).toList();
if (ancestors.length > 2) {
displayPath = [ancestors.first.name, '...', ancestors.last.name];
}
return Text(
displayPath.join(' / '),
style: theme.textStyle.body
.standard(color: theme.textColorScheme.secondary),
);
},
),
);
}
}
Widget buildSummary(AppFlowyThemeData theme) {
if (item.content.isEmpty) {
return const SizedBox.shrink();
}
return RichText(
maxLines: 3,
overflow: TextOverflow.ellipsis,
text: buildHighLightSpan(
content: item.content,
normal: theme.textStyle.heading4
.standard(color: theme.textColorScheme.secondary),
highlight: theme.textStyle.heading4
.standard(color: theme.textColorScheme.primary)
.copyWith(
backgroundColor: theme.fillColorScheme.themeSelect,
),
);
},
),
);
}
List<Widget> buildSummary(AppFlowyThemeData theme) {
if (item.content.isEmpty) return [];
return [
VSpace(theme.spacing.m),
RichText(
maxLines: 3,
overflow: TextOverflow.ellipsis,
text: buildHighLightSpan(
content: item.content,
normal: theme.textStyle.heading4
.standard(color: theme.textColorScheme.secondary),
highlight: theme.textStyle.heading4
.standard(color: theme.textColorScheme.primary)
.copyWith(
backgroundColor: theme.fillColorScheme.themeSelect,
),
),
),
];
}
TextSpan buildHighLightSpan({
required String content,
required TextStyle normal,
@@ -10,7 +10,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'mobile_search_ask_ai_entrance.dart';
import 'mobile_search_result.dart';
@@ -54,23 +53,11 @@ class MobileSearchScreen extends StatelessWidget {
return const WorkspaceFailedScreen();
}
Sentry.configureScope(
(scope) => scope.setUser(
SentryUser(
id: userProfile.id.toString(),
),
),
);
return Scaffold(
body: SafeArea(
bottom: false,
child: Provider.value(
value: userProfile,
child: MobileSearchPage(
userProfile: userProfile,
workspaceLatestPB: latest,
),
),
return Provider.value(
value: userProfile,
child: MobileSearchPage(
userProfile: userProfile,
workspaceLatestPB: latest,
),
);
},
@@ -78,7 +65,7 @@ class MobileSearchScreen extends StatelessWidget {
}
}
class MobileSearchPage extends StatelessWidget {
class MobileSearchPage extends StatefulWidget {
const MobileSearchPage({
super.key,
required this.userProfile,
@@ -88,31 +75,62 @@ class MobileSearchPage extends StatelessWidget {
final UserProfilePB userProfile;
final WorkspaceLatestPB workspaceLatestPB;
@override
State<MobileSearchPage> createState() => _MobileSearchPageState();
}
class _MobileSearchPageState extends State<MobileSearchPage> {
bool get enableShowAISearch =>
userProfile.workspaceAuthType == AuthTypePB.Server;
widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;
final focusNode = FocusNode();
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CommandPaletteBloc, CommandPaletteState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MobileSearchTextfield(
hintText: enableShowAISearch
? LocaleKeys.search_searchOrAskAI.tr()
: LocaleKeys.search_label.tr(),
query: state.query ?? '',
onChanged: (value) => context
.read<CommandPaletteBloc>()
.add(CommandPaletteEvent.searchChanged(search: value)),
return SafeArea(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MobileSearchTextfield(
focusNode: focusNode,
hintText: enableShowAISearch
? LocaleKeys.search_searchOrAskAI.tr()
: LocaleKeys.search_label.tr(),
query: state.query ?? '',
onChanged: (value) =>
context.read<CommandPaletteBloc>().add(
CommandPaletteEvent.searchChanged(search: value),
),
),
if (enableShowAISearch)
MobileSearchAskAiEntrance(query: state.query),
Flexible(
child: NotificationListener(
child: MobileSearchResult(),
onNotification: (t) {
if (t is ScrollUpdateNotification) {
if (focusNode.hasFocus) {
focusNode.unfocus();
}
}
return true;
},
),
),
],
),
if (enableShowAISearch)
MobileSearchAskAiEntrance(query: state.query),
Flexible(child: SafeArea(child: MobileSearchResult())),
],
),
),
);
},
@@ -6,6 +6,7 @@ import 'package:appflowy/workspace/application/command_palette/command_palette_b
import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
@@ -36,13 +37,18 @@ class MobileSearchRecentList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
final commandPaletteState = context.read<CommandPaletteBloc>().state;
final trashIdSet = commandPaletteState.trash.map((e) => e.id).toSet();
return BlocProvider(
create: (context) =>
RecentViewsBloc()..add(const RecentViewsEvent.initial()),
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) {
final List<ViewPB> recentViews =
state.views.map((e) => e.item).toSet().toList();
final List<ViewPB> recentViews = state.views
.map((e) => e.item)
.where((e) => !trashIdSet.contains(e.id))
.toList();
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -115,7 +121,7 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
children: [
const VSpace(16),
Text(
LocaleKeys.search_bestMatch.tr(),
LocaleKeys.commandPalette_bestMatches.tr(),
style: theme.textStyle.body
.enhanced(color: theme.textColorScheme.secondary),
),
@@ -130,6 +136,10 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
.fold((s) => s, (s) => null);
if (view != null && context.mounted) {
await _goToView(context, view);
} else {
Log.error(
'tapping search result, view not found: ${item.id}',
);
}
},
child: MobileSearchResultCell(
@@ -161,12 +171,13 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
),
const VSpace(12),
Text(
LocaleKeys.search_noResultForSearching.tr(args: [query]),
LocaleKeys.search_noResultForSearching.tr(),
style: theme.textStyle.body.enhanced(color: textColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
textAlign: TextAlign.center,
LocaleKeys.search_noResultForSearchingHint.tr(),
style: theme.textStyle.caption.standard(color: textColor),
),
@@ -11,11 +11,13 @@ class MobileSearchTextfield extends StatefulWidget {
this.onChanged,
required this.hintText,
required this.query,
required this.focusNode,
});
final String hintText;
final String query;
final ValueChanged<String>? onChanged;
final FocusNode focusNode;
@override
State<MobileSearchTextfield> createState() => _MobileSearchTextfieldState();
@@ -23,18 +25,15 @@ class MobileSearchTextfield extends StatefulWidget {
class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
late final TextEditingController controller;
late final FocusNode focusNode;
final ValueNotifier<bool> hasFocusValueNotifier = ValueNotifier(true);
FocusNode get focusNode => widget.focusNode;
@override
void initState() {
super.initState();
controller = TextEditingController(text: widget.query);
focusNode = FocusNode();
focusNode.addListener(() {
if (!mounted) return;
hasFocusValueNotifier.value = focusNode.hasFocus;
});
focusNode.addListener(onFocusChanged);
controller.addListener(() {
if (!mounted) return;
widget.onChanged?.call(controller.text);
@@ -46,7 +45,7 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
@override
void dispose() {
controller.dispose();
focusNode.dispose();
focusNode.removeListener(onFocusChanged);
hasFocusValueNotifier.dispose();
bottomNavigationBarItemType.removeListener(onBackOrLeave);
super.dispose();
@@ -56,7 +55,7 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return SizedBox(
height: 40,
height: 42,
child: ValueListenableBuilder(
valueListenable: controller,
builder: (context, _, __) {
@@ -85,12 +84,17 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
if (!hasFocus || !hasText) return SizedBox.shrink();
return GestureDetector(
onTap: () => focusNode.unfocus(),
child: Padding(
behavior: HitTestBehavior.opaque,
child: Container(
height: 42,
padding: EdgeInsets.only(left: 8),
child: Text(
LocaleKeys.button_cancel.tr(),
style: theme.textStyle.body
.standard(color: theme.textColorScheme.action),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
LocaleKeys.button_cancel.tr(),
style: theme.textStyle.body
.standard(color: theme.textColorScheme.action),
),
),
),
);
@@ -157,11 +161,17 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
);
}
void onFocusChanged() {
if (!mounted) return;
hasFocusValueNotifier.value = focusNode.hasFocus;
}
void onBackOrLeave() {
final label = bottomNavigationBarItemType.value;
if (label == BottomNavigationBarItemType.search.label) {
focusNode.requestFocus();
} else {
focusNode.unfocus();
controller.clear();
}
}
@@ -2,6 +2,7 @@ import 'dart:math';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -328,6 +329,7 @@ class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
}
Widget _buildNoResultsWidget(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
@@ -350,7 +352,10 @@ class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
child: Center(
child: Text(
LocaleKeys.inlineActions_noResults.tr(),
style: TextStyle(fontSize: 18.0, color: Color(0x801F2225)),
style: TextStyle(
fontSize: 18.0,
color: theme.textColorScheme.primary,
),
textAlign: TextAlign.center,
),
),
@@ -1,9 +1,9 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.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';
@@ -22,23 +22,23 @@ class AboutSettingGroup extends StatelessWidget {
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_mobile_privacyPolicy.tr(),
trailing: const Icon(
Icons.chevron_right,
trailing: MobileSettingTrailing(
text: '',
),
onTap: () => afLaunchUrlString('https://appflowy.com/privacy'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
trailing: const Icon(
Icons.chevron_right,
trailing: MobileSettingTrailing(
text: '',
),
onTap: () => afLaunchUrlString('https://appflowy.com/terms'),
),
if (kDebugMode)
MobileSettingItem(
name: 'Feature Flags',
trailing: const Icon(
Icons.chevron_right,
trailing: MobileSettingTrailing(
text: '',
),
onTap: () {
context.push(FeatureFlagScreen.routeName);
@@ -46,9 +46,10 @@ class AboutSettingGroup extends StatelessWidget {
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_version.tr(),
trailing: FlowyText(
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
color: Theme.of(context).colorScheme.onSurface,
trailing: MobileSettingTrailing(
text:
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
showArrow: false,
),
),
],
@@ -2,11 +2,11 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_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/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -23,7 +23,6 @@ class AiSettingsGroup extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocProvider(
create: (context) => SettingsAIBloc(
userProfile,
@@ -36,21 +35,8 @@ class AiSettingsGroup extends StatelessWidget {
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_aiPage_keys_llmModelType.tr(),
trailing: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText(
state.availableModels?.selectedModel.name ?? "",
color: theme.colorScheme.onSurface,
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.chevron_right),
],
),
trailing: MobileSettingTrailing(
text: state.availableModels?.selectedModel.name ?? "",
),
onTap: () => _onLLMModelTypeTap(context, state),
),
@@ -1,10 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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';
@@ -17,20 +17,12 @@ class RTLSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textDirection =
context.watch<AppearanceSettingsCubit>().state.textDirection;
return MobileSettingItem(
name: LocaleKeys.settings_appearance_textDirection_label.tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
_textDirectionLabelText(textDirection),
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),
],
trailing: MobileSettingTrailing(
text: _textDirectionLabelText(textDirection),
),
onTap: () {
showMobileBottomSheet(
@@ -1,11 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:scaled_app/scaled_app.dart';
@@ -42,18 +42,10 @@ class _DisplaySizeSettingState extends State<DisplaySizeSetting> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return MobileSettingItem(
name: LocaleKeys.settings_appearance_displaySize.tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
scaleFactor.toStringAsFixed(2),
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),
],
trailing: MobileSettingTrailing(
text: scaleFactor.toStringAsFixed(1),
),
onTap: () {
showMobileBottomSheet(
@@ -1,11 +1,11 @@
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/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/util/theme_mode_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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';
@@ -18,19 +18,11 @@ class ThemeSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final themeMode = context.watch<AppearanceSettingsCubit>().state.themeMode;
return MobileSettingItem(
name: LocaleKeys.settings_appearance_themeMode_label.tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
themeMode.labelText,
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),
],
trailing: MobileSettingTrailing(
text: themeMode.labelText,
),
onTap: () {
showMobileBottomSheet(
@@ -1,11 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
class CloudSettingGroup extends StatelessWidget {
const CloudSettingGroup({
@@ -15,19 +15,23 @@ class CloudSettingGroup extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) => MobileSettingGroup(
groupTitle: LocaleKeys.settings_menu_cloudSettings.tr(),
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_menu_cloudAppFlowy.tr(),
trailing: const Icon(
Icons.chevron_right,
future: getAuthenticatorType(),
builder: (context, snapshot) {
final cloudType = snapshot.data ?? AuthenticatorType.appflowyCloud;
final name = titleFromCloudType(cloudType);
return MobileSettingGroup(
groupTitle: 'Cloud settings',
settingItemList: [
MobileSettingItem(
name: 'Cloud server',
trailing: MobileSettingTrailing(
text: name,
),
onTap: () => context.push(AppFlowyCloudPage.routeName),
),
onTap: () => context.push(AppFlowyCloudPage.routeName),
),
],
),
],
);
},
);
}
}
@@ -2,11 +2,11 @@ import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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';
@@ -20,21 +20,12 @@ class FontSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final selectedFont = context.watch<AppearanceSettingsCubit>().state.font;
final name = selectedFont.fontFamilyDisplayName;
return MobileSettingItem(
name: LocaleKeys.settings_appearance_fontFamily_label.tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
lineHeight: 1.0,
name,
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),
],
trailing: MobileSettingTrailing(
text: name,
),
onTap: () async {
final newFont = await context.push<String>(FontPickerScreen.routeName);
@@ -1,9 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/language.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';
@@ -28,22 +28,13 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
return state.locale;
},
builder: (context, locale) {
final theme = Theme.of(context);
return MobileSettingGroup(
groupTitle: LocaleKeys.settings_menu_language.tr(),
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_menu_language.tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
lineHeight: 1.0,
languageFromLocale(locale),
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),
],
trailing: MobileSettingTrailing(
text: languageFromLocale(locale),
),
onTap: () async {
final newLocale =
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -64,12 +65,11 @@ class _EditUsernameBottomSheetState extends State<EditUsernameBottomSheet> {
),
),
const VSpace(16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: submitUserName,
child: Text(LocaleKeys.button_update.tr()),
),
AFFilledTextButton.primary(
text: LocaleKeys.button_update.tr(),
onTap: submitUserName,
size: AFButtonSize.l,
alignment: Alignment.center,
),
],
);
@@ -1,10 +1,16 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/password/password_bloc.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -21,11 +27,19 @@ class PersonalInfoSettingGroup extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocProvider<SettingsUserViewBloc>(
create: (context) => getIt<SettingsUserViewBloc>(
param1: userProfile,
)..add(const SettingsUserEvent.initial()),
return MultiBlocProvider(
providers: [
BlocProvider<SettingsUserViewBloc>(
create: (context) => getIt<SettingsUserViewBloc>(
param1: userProfile,
)..add(const SettingsUserEvent.initial()),
),
BlocProvider(
create: (context) => PasswordBloc(userProfile)
..add(PasswordEvent.init())
..add(PasswordEvent.checkHasPassword()),
),
],
child: BlocSelector<SettingsUserViewBloc, SettingsUserState, String>(
selector: (state) => state.userProfile.name,
builder: (context, userName) {
@@ -33,16 +47,10 @@ class PersonalInfoSettingGroup extends StatelessWidget {
groupTitle: LocaleKeys.settings_accountPage_title.tr(),
settingItemList: [
MobileSettingItem(
name: userName,
subtitle: isAuthEnabled && userProfile.email.isNotEmpty
? Text(
userProfile.email,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
)
: null,
trailing: const Icon(Icons.chevron_right),
name: 'User name',
trailing: MobileSettingTrailing(
text: userName,
),
onTap: () {
showMobileBottomSheet(
context,
@@ -64,10 +72,111 @@ class PersonalInfoSettingGroup extends StatelessWidget {
);
},
),
...userProfile.userAuthType == AuthTypePB.Server
? [
_buildEmailItem(context, userProfile),
_buildPasswordItem(context, userProfile),
]
: [
_buildLoginItem(context, userProfile),
],
],
);
},
),
);
}
Widget _buildEmailItem(BuildContext context, UserProfilePB userProfile) {
final theme = AppFlowyTheme.of(context);
return MobileSettingItem(
name: LocaleKeys.settings_accountPage_email_title.tr(),
trailing: Text(
userProfile.email,
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.secondary,
),
),
);
}
Widget _buildPasswordItem(BuildContext context, UserProfilePB userProfile) {
return BlocBuilder<PasswordBloc, PasswordState>(
builder: (context, state) {
final hasPassword = state.hasPassword;
final title = hasPassword
? LocaleKeys.newSettings_myAccount_password_changePassword.tr()
: LocaleKeys.newSettings_myAccount_password_setupPassword.tr();
final passwordBloc = context.read<PasswordBloc>();
return MobileSettingItem(
name: LocaleKeys.newSettings_myAccount_password_title.tr(),
trailing: MobileSettingTrailing(
text: '',
),
onTap: () {
showMobileBottomSheet(
context,
showHeader: true,
title: title,
showCloseButton: true,
showDragHandle: true,
showDivider: false,
padding: const EdgeInsets.symmetric(horizontal: 16),
builder: (_) {
Widget child;
if (hasPassword) {
child = ChangePasswordDialogContent(
userProfile: userProfile,
showTitle: false,
showCloseAndSaveButton: false,
showSaveButton: true,
padding: EdgeInsets.zero,
);
} else {
child = SetupPasswordDialogContent(
userProfile: userProfile,
showTitle: false,
showCloseAndSaveButton: false,
showSaveButton: true,
padding: EdgeInsets.zero,
);
}
return BlocProvider.value(
value: passwordBloc,
child: child,
);
},
);
},
);
},
);
}
Widget _buildLoginItem(BuildContext context, UserProfilePB userProfile) {
final theme = AppFlowyTheme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
LocaleKeys.signIn_youAreInLocalMode.tr(),
style: theme.textStyle.body.standard(
color: theme.textColorScheme.secondary,
),
),
VSpace(theme.spacing.m),
AFOutlinedTextButton.normal(
text: LocaleKeys.signIn_loginToAppFlowyCloud.tr(),
size: AFButtonSize.l,
alignment: Alignment.center,
onTap: () async {
// logout and restart the app
await getIt<AuthService>().signOut();
await runAppFlowy();
},
),
VSpace(theme.spacing.m),
],
);
}
}
@@ -87,7 +87,7 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
case SelfHostUrlBottomSheetType.shareDomain:
await useBaseWebDomain(url);
case SelfHostUrlBottomSheetType.cloudURL:
await useSelfHostedAppFlowyCloudWithURL(url);
await useSelfHostedAppFlowyCloud(url);
}
await runAppFlowy();
},
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/shared/appflowy_cache_manager.dart';
import 'package:appflowy/startup/startup.dart';
@@ -30,15 +31,15 @@ class SupportSettingGroup extends StatelessWidget {
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_mobile_joinDiscord.tr(),
trailing: const Icon(
Icons.chevron_right,
trailing: MobileSettingTrailing(
text: '',
),
onTap: () => afLaunchUrlString('https://discord.gg/JucBXeU2FE'),
),
MobileSettingItem(
name: LocaleKeys.workspace_errorActions_reportIssue.tr(),
trailing: const Icon(
Icons.chevron_right,
trailing: MobileSettingTrailing(
text: '',
),
onTap: () {
showMobileBottomSheet(
@@ -57,8 +58,8 @@ class SupportSettingGroup extends StatelessWidget {
),
MobileSettingItem(
name: LocaleKeys.settings_files_clearCache.tr(),
trailing: const Icon(
Icons.chevron_right,
trailing: MobileSettingTrailing(
text: '',
),
onTap: () async {
await showFlowyMobileConfirmDialog(
@@ -9,6 +9,7 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
import 'package:appflowy/workspace/presentation/settings/pages/account/account_deletion.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -26,11 +27,12 @@ class UserSessionSettingGroup extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Column(
children: [
// third party sign in buttons
if (showThirdPartyLogin) _buildThirdPartySignInButtons(context),
const VSpace(8.0),
VSpace(theme.spacing.xxl),
// logout button
MobileLogoutButton(
@@ -40,14 +42,20 @@ class UserSessionSettingGroup extends StatelessWidget {
// delete account button
// only show the delete account button in cloud mode
if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[
const VSpace(16.0),
MobileLogoutButton(
if (userProfile.userAuthType == AuthTypePB.Server) ...[
VSpace(theme.spacing.xxl),
AFOutlinedTextButton.destructive(
alignment: Alignment.center,
text: LocaleKeys.button_deleteAccount.tr(),
textColor: Theme.of(context).colorScheme.error,
onPressed: () => _showDeleteAccountDialog(context),
textStyle: theme.textStyle.body.standard(
color: theme.textColorScheme.error,
),
onTap: () => _showDeleteAccountDialog(context),
size: AFButtonSize.l,
),
],
VSpace(theme.spacing.xxl),
],
);
}
@@ -1,3 +1,4 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -15,16 +16,22 @@ class MobileSettingGroup extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(4.0),
FlowyText.semibold(
VSpace(theme.spacing.s),
Text(
groupTitle,
style: theme.textStyle.heading4.enhanced(
color: theme.textColorScheme.primary,
),
),
const VSpace(4.0),
VSpace(theme.spacing.s),
...settingItemList,
showDivider ? const Divider() : const SizedBox.shrink(),
showDivider
? AFDivider(spacing: theme.spacing.m)
: const SizedBox.shrink(),
],
);
}
@@ -1,3 +1,4 @@
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -26,17 +27,18 @@ class MobileSettingItem extends StatelessWidget {
return Padding(
padding: padding,
child: ListTile(
title: title ?? _buildDefaultTitle(name),
title: title ?? _buildDefaultTitle(context, name),
subtitle: subtitle,
trailing: trailing,
onTap: onTap,
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0),
contentPadding: EdgeInsets.zero,
),
);
}
Widget _buildDefaultTitle(String? name) {
Widget _buildDefaultTitle(BuildContext context, String? name) {
final theme = AppFlowyTheme.of(context);
return Row(
children: [
if (leadingIcon != null) ...[
@@ -44,9 +46,11 @@ class MobileSettingItem extends StatelessWidget {
const HSpace(8),
],
Expanded(
child: FlowyText.medium(
child: Text(
name ?? '',
fontSize: 14.0,
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.primary,
),
overflow: TextOverflow.ellipsis,
),
),
@@ -0,0 +1,42 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class MobileSettingTrailing extends StatelessWidget {
const MobileSettingTrailing({
super.key,
required this.text,
this.showArrow = true,
});
final String text;
final bool showArrow;
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
text,
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.secondary,
),
overflow: TextOverflow.ellipsis,
),
),
if (showArrow) ...[
const HSpace(8),
FlowySvg(
FlowySvgs.toolbar_arrow_right_m,
size: Size.square(24),
color: theme.iconColorScheme.tertiary,
),
],
],
);
}
}
@@ -0,0 +1,324 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/m_invite_member_by_email.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:appflowy_ui/appflowy_ui.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 'member_list.dart';
class AddMembersScreen extends StatelessWidget {
const AddMembersScreen({
super.key,
});
static const routeName = '/add_member';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FlowyAppBar(
titleText: 'Add members',
),
body: const _InviteMemberPage(),
resizeToAvoidBottomInset: false,
);
}
}
class _InviteMemberPage extends StatefulWidget {
const _InviteMemberPage();
@override
State<_InviteMemberPage> createState() => _InviteMemberPageState();
}
class _InviteMemberPageState extends State<_InviteMemberPage> {
final emailController = TextEditingController();
late final Future<UserProfilePB?> userProfile;
bool exceededLimit = false;
@override
void initState() {
super.initState();
userProfile = UserBackendService.getCurrentUserProfile().fold(
(s) => s,
(f) => null,
);
}
@override
void dispose() {
emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return FutureBuilder(
future: userProfile,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox.shrink();
}
if (snapshot.hasError || snapshot.data == null) {
return _buildError(context);
}
final userProfile = snapshot.data!;
return BlocProvider<WorkspaceMemberBloc>(
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
..add(const WorkspaceMemberEvent.initial())
..add(const WorkspaceMemberEvent.getInviteCode()),
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
listener: _onListener,
builder: (context, state) {
return Column(
children: [
if (state.myRole.isOwner) ...[
Container(
width: double.infinity,
padding: EdgeInsets.all(theme.spacing.xl),
child: const MInviteMemberByEmail(),
),
VSpace(theme.spacing.m),
],
if (state.members.isNotEmpty) ...[
const AFDivider(),
VSpace(theme.spacing.xl),
MobileMemberList(
members: state.members,
userProfile: userProfile,
myRole: state.myRole,
),
],
if (state.myRole.isMember) ...[
Spacer(),
const _LeaveWorkspaceButton(),
],
const VSpace(48),
],
);
},
),
);
},
);
}
Widget _buildError(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 48.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText.medium(
LocaleKeys.settings_appearance_members_workspaceMembersError.tr(),
fontSize: 18.0,
textAlign: TextAlign.center,
),
const VSpace(8.0),
FlowyText.regular(
LocaleKeys
.settings_appearance_members_workspaceMembersErrorDescription
.tr(),
fontSize: 17.0,
maxLines: 10,
textAlign: TextAlign.center,
lineHeight: 1.3,
color: Theme.of(context).hintColor,
),
],
),
),
);
}
void _onListener(BuildContext context, WorkspaceMemberState state) {
final actionResult = state.actionResult;
if (actionResult == null) {
return;
}
final actionType = actionResult.actionType;
final result = actionResult.result;
// only show the result dialog when the action is WorkspaceMemberActionType.add
if (actionType == WorkspaceMemberActionType.addByEmail) {
result.fold(
(s) {
showToastNotification(
message:
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
);
},
(f) {
Log.error('add workspace member failed: $f');
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
? LocaleKeys
.settings_appearance_members_inviteFailedMemberLimitMobile
.tr()
: LocaleKeys.settings_appearance_members_failedToAddMember.tr();
setState(() {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
type: ToastificationType.error,
message: message,
);
},
);
} else if (actionType == WorkspaceMemberActionType.inviteByEmail) {
result.fold(
(s) {
showToastNotification(
message:
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
);
},
(f) {
Log.error('invite workspace member failed: $f');
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
? LocaleKeys
.settings_appearance_members_inviteFailedMemberLimitMobile
.tr()
: LocaleKeys.settings_appearance_members_failedToInviteMember
.tr();
setState(() {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
type: ToastificationType.error,
message: message,
);
},
);
} else if (actionType == WorkspaceMemberActionType.removeByEmail) {
result.fold(
(s) {
showToastNotification(
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceSuccess
.tr(),
);
},
(f) {
showToastNotification(
type: ToastificationType.error,
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceFailed
.tr(),
);
},
);
} else if (actionType == WorkspaceMemberActionType.generateInviteLink) {
result.fold(
(s) async {
showToastNotification(
message: LocaleKeys
.settings_appearance_members_generatedLinkSuccessfully
.tr(),
);
// copy the invite link to the clipboard
final inviteLink = state.inviteLink;
if (inviteLink != null) {
await getIt<ClipboardService>().setPlainText(inviteLink);
showToastNotification(
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
);
}
},
(f) {
Log.error('generate invite link failed: $f');
showToastNotification(
type: ToastificationType.error,
message:
LocaleKeys.settings_appearance_members_generatedLinkFailed.tr(),
);
},
);
} else if (actionType == WorkspaceMemberActionType.resetInviteLink) {
result.fold(
(s) async {
showToastNotification(
message: LocaleKeys
.settings_appearance_members_resetLinkSuccessfully
.tr(),
);
// copy the invite link to the clipboard
final inviteLink = state.inviteLink;
if (inviteLink != null) {
await getIt<ClipboardService>().setPlainText(inviteLink);
showToastNotification(
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
);
}
},
(f) {
Log.error('generate invite link failed: $f');
showToastNotification(
type: ToastificationType.error,
message:
LocaleKeys.settings_appearance_members_resetLinkFailed.tr(),
);
},
);
}
}
}
class _LeaveWorkspaceButton extends StatelessWidget {
const _LeaveWorkspaceButton();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AFOutlinedTextButton.destructive(
alignment: Alignment.center,
text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
onTap: () => _leaveWorkspace(context),
size: AFButtonSize.l,
),
);
}
void _leaveWorkspace(BuildContext context) {
showFlowyCupertinoConfirmDialog(
title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),
leftButton: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
),
rightButton: FlowyText(
LocaleKeys.button_confirm.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
onRightButtonPressed: (buttonContext) async {},
);
}
}
@@ -0,0 +1,357 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:appflowy_ui/appflowy_ui.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:string_validator/string_validator.dart';
import 'member_list.dart';
ValueNotifier<int> mobileLeaveWorkspaceNotifier = ValueNotifier(0);
class InviteMembersScreen extends StatelessWidget {
const InviteMembersScreen({
super.key,
});
static const routeName = '/invite_member';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FlowyAppBar(
titleText: LocaleKeys.settings_appearance_members_label.tr(),
),
body: const _InviteMemberPage(),
resizeToAvoidBottomInset: false,
);
}
}
class _InviteMemberPage extends StatefulWidget {
const _InviteMemberPage();
@override
State<_InviteMemberPage> createState() => _InviteMemberPageState();
}
class _InviteMemberPageState extends State<_InviteMemberPage> {
final emailController = TextEditingController();
late final Future<UserProfilePB?> userProfile;
bool exceededLimit = false;
@override
void initState() {
super.initState();
userProfile = UserBackendService.getCurrentUserProfile().fold(
(s) => s,
(f) => null,
);
}
@override
void dispose() {
emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return FutureBuilder(
future: userProfile,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox.shrink();
}
if (snapshot.hasError || snapshot.data == null) {
return _buildError(context);
}
final userProfile = snapshot.data!;
return BlocProvider<WorkspaceMemberBloc>(
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
..add(const WorkspaceMemberEvent.initial())
..add(const WorkspaceMemberEvent.getInviteCode()),
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
listener: _onListener,
builder: (context, state) {
return Column(
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (state.myRole.isOwner) ...[
Padding(
padding: EdgeInsets.all(theme.spacing.xl),
child: _buildInviteMemberArea(context),
),
const VSpace(16),
],
if (state.members.isNotEmpty) ...[
const AFDivider(),
VSpace(theme.spacing.xl),
MobileMemberList(
members: state.members,
userProfile: userProfile,
myRole: state.myRole,
),
],
],
),
),
if (state.myRole.isMember) const _LeaveWorkspaceButton(),
const VSpace(48),
],
);
},
),
);
},
);
}
Widget _buildInviteMemberArea(BuildContext context) {
return Column(
children: [
TextFormField(
autofocus: true,
controller: emailController,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(),
),
),
const VSpace(16),
if (exceededLimit) ...[
FlowyText.regular(
LocaleKeys.settings_appearance_members_inviteFailedMemberLimitMobile
.tr(),
fontSize: 14.0,
maxLines: 3,
color: Theme.of(context).colorScheme.error,
),
const VSpace(16),
],
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _inviteMember(context),
child: Text(
LocaleKeys.settings_appearance_members_sendInvite.tr(),
),
),
),
],
);
}
Widget _buildError(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 48.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText.medium(
LocaleKeys.settings_appearance_members_workspaceMembersError.tr(),
fontSize: 18.0,
textAlign: TextAlign.center,
),
const VSpace(8.0),
FlowyText.regular(
LocaleKeys
.settings_appearance_members_workspaceMembersErrorDescription
.tr(),
fontSize: 17.0,
maxLines: 10,
textAlign: TextAlign.center,
lineHeight: 1.3,
color: Theme.of(context).hintColor,
),
],
),
),
);
}
void _onListener(BuildContext context, WorkspaceMemberState state) {
final actionResult = state.actionResult;
if (actionResult == null) {
return;
}
final actionType = actionResult.actionType;
final result = actionResult.result;
// get keyboard height
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
// only show the result dialog when the action is WorkspaceMemberActionType.add
if (actionType == WorkspaceMemberActionType.addByEmail) {
result.fold(
(s) {
showToastNotification(
message:
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
bottomPadding: keyboardHeight,
);
},
(f) {
Log.error('add workspace member failed: $f');
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
? LocaleKeys
.settings_appearance_members_inviteFailedMemberLimitMobile
.tr()
: LocaleKeys.settings_appearance_members_failedToAddMember.tr();
setState(() {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
type: ToastificationType.error,
bottomPadding: keyboardHeight,
message: message,
);
},
);
} else if (actionType == WorkspaceMemberActionType.inviteByEmail) {
result.fold(
(s) {
showToastNotification(
message:
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
bottomPadding: keyboardHeight,
);
},
(f) {
Log.error('invite workspace member failed: $f');
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
? LocaleKeys
.settings_appearance_members_inviteFailedMemberLimitMobile
.tr()
: LocaleKeys.settings_appearance_members_failedToInviteMember
.tr();
setState(() {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
type: ToastificationType.error,
message: message,
bottomPadding: keyboardHeight,
);
},
);
} else if (actionType == WorkspaceMemberActionType.removeByEmail) {
result.fold(
(s) {
showToastNotification(
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceSuccess
.tr(),
bottomPadding: keyboardHeight,
);
},
(f) {
showToastNotification(
type: ToastificationType.error,
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceFailed
.tr(),
bottomPadding: keyboardHeight,
);
},
);
}
}
void _inviteMember(BuildContext context) {
final email = emailController.text;
if (!isEmail(email)) {
showToastNotification(
type: ToastificationType.error,
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
return;
}
context
.read<WorkspaceMemberBloc>()
.add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email));
// clear the email field after inviting
emailController.clear();
}
}
class _LeaveWorkspaceButton extends StatelessWidget {
const _LeaveWorkspaceButton();
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Theme.of(context).colorScheme.error,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(
color: Theme.of(context).colorScheme.error,
width: 0.5,
),
),
),
onPressed: () => _leaveWorkspace(context),
child: FlowyText(
LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
fontSize: 14.0,
color: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.w500,
),
),
);
}
void _leaveWorkspace(BuildContext context) {
showFlowyCupertinoConfirmDialog(
title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),
leftButton: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
),
rightButton: FlowyText(
LocaleKeys.button_confirm.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
onRightButtonPressed: (buttonContext) async {
// try to use popUntil with a specific route name but failed
// so use pop twice as a workaround
Navigator.of(buttonContext).pop();
Navigator.of(context).pop();
Navigator.of(context).pop();
mobileLeaveWorkspaceNotifier.value =
mobileLeaveWorkspaceNotifier.value + 1;
},
);
}
}
@@ -1,19 +1,25 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/setting/workspace/add_members_screen.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/m_invite_member_by_link.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:appflowy_ui/appflowy_ui.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:string_validator/string_validator.dart';
import 'package:go_router/go_router.dart';
import 'member_list.dart';
@@ -31,11 +37,26 @@ class InviteMembersScreen extends StatelessWidget {
return Scaffold(
appBar: FlowyAppBar(
titleText: LocaleKeys.settings_appearance_members_label.tr(),
actions: [
_buildAddMemberButton(context),
],
),
body: const _InviteMemberPage(),
resizeToAvoidBottomInset: false,
);
}
Widget _buildAddMemberButton(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 20),
child: GestureDetector(
onTap: () {
context.push(AddMembersScreen.routeName);
},
child: FlowySvg(FlowySvgs.add_thin_s),
),
);
}
}
class _InviteMemberPage extends StatefulWidget {
@@ -67,6 +88,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
return FutureBuilder(
future: userProfile,
builder: (context, snapshot) {
@@ -81,37 +103,38 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
return BlocProvider<WorkspaceMemberBloc>(
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
..add(const WorkspaceMemberEvent.initial()),
..add(const WorkspaceMemberEvent.initial())
..add(const WorkspaceMemberEvent.getInviteCode()),
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
listener: _onListener,
builder: (context, state) {
return Column(
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (state.myRole.isOwner) ...[
Padding(
padding: const EdgeInsets.all(16.0),
child: _buildInviteMemberArea(context),
),
const VSpace(16),
],
if (state.members.isNotEmpty) ...[
const VSpace(8),
MobileMemberList(
members: state.members,
userProfile: userProfile,
myRole: state.myRole,
),
],
],
),
),
if (state.myRole.isMember) const _LeaveWorkspaceButton(),
const VSpace(48),
],
return SingleChildScrollView(
child: Column(
children: [
if (state.myRole.isOwner) ...[
Container(
width: double.infinity,
padding: EdgeInsets.all(theme.spacing.xl),
child: const MInviteMemberByLink(),
),
VSpace(theme.spacing.m),
],
if (state.members.isNotEmpty) ...[
const AFDivider(),
VSpace(theme.spacing.xl),
MobileMemberList(
members: state.members,
userProfile: userProfile,
myRole: state.myRole,
),
],
if (state.myRole.isMember) ...[
Spacer(),
const _LeaveWorkspaceButton(),
],
const VSpace(48),
],
),
);
},
),
@@ -120,41 +143,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
);
}
Widget _buildInviteMemberArea(BuildContext context) {
return Column(
children: [
TextFormField(
autofocus: true,
controller: emailController,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(),
),
),
const VSpace(16),
if (exceededLimit) ...[
FlowyText.regular(
LocaleKeys.settings_appearance_members_inviteFailedMemberLimitMobile
.tr(),
fontSize: 14.0,
maxLines: 3,
color: Theme.of(context).colorScheme.error,
),
const VSpace(16),
],
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _inviteMember(context),
child: Text(
LocaleKeys.settings_appearance_members_sendInvite.tr(),
),
),
),
],
);
}
Widget _buildError(BuildContext context) {
return Center(
child: Padding(
@@ -193,9 +181,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
final actionType = actionResult.actionType;
final result = actionResult.result;
// get keyboard height
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
// only show the result dialog when the action is WorkspaceMemberActionType.add
if (actionType == WorkspaceMemberActionType.addByEmail) {
result.fold(
@@ -203,7 +188,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
showToastNotification(
message:
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
bottomPadding: keyboardHeight,
);
},
(f) {
@@ -218,7 +202,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
});
showToastNotification(
type: ToastificationType.error,
bottomPadding: keyboardHeight,
message: message,
);
},
@@ -229,7 +212,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
showToastNotification(
message:
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
bottomPadding: keyboardHeight,
);
},
(f) {
@@ -243,10 +225,10 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
setState(() {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
type: ToastificationType.error,
message: message,
bottomPadding: keyboardHeight,
);
},
);
@@ -257,7 +239,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceSuccess
.tr(),
bottomPadding: keyboardHeight,
);
},
(f) {
@@ -266,28 +247,65 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceFailed
.tr(),
bottomPadding: keyboardHeight,
);
},
);
} else if (actionType == WorkspaceMemberActionType.generateInviteLink) {
result.fold(
(s) {
showToastNotification(
message: LocaleKeys
.settings_appearance_members_generatedLinkSuccessfully
.tr(),
);
// copy the invite link to the clipboard
final inviteLink = state.inviteLink;
if (inviteLink != null) {
getIt<ClipboardService>().setPlainText(inviteLink);
showToastNotification(
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
);
}
},
(f) {
Log.error('generate invite link failed: $f');
showToastNotification(
type: ToastificationType.error,
message:
LocaleKeys.settings_appearance_members_generatedLinkFailed.tr(),
);
},
);
} else if (actionType == WorkspaceMemberActionType.resetInviteLink) {
result.fold(
(s) {
showToastNotification(
message: LocaleKeys
.settings_appearance_members_resetLinkSuccessfully
.tr(),
);
// copy the invite link to the clipboard
final inviteLink = state.inviteLink;
if (inviteLink != null) {
getIt<ClipboardService>().setPlainText(inviteLink);
showToastNotification(
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
);
}
},
(f) {
Log.error('generate invite link failed: $f');
showToastNotification(
type: ToastificationType.error,
message:
LocaleKeys.settings_appearance_members_resetLinkFailed.tr(),
);
},
);
}
}
void _inviteMember(BuildContext context) {
final email = emailController.text;
if (!isEmail(email)) {
showToastNotification(
type: ToastificationType.error,
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
return;
}
context
.read<WorkspaceMemberBloc>()
.add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email));
// clear the email field after inviting
emailController.clear();
}
}
class _LeaveWorkspaceButton extends StatelessWidget {
@@ -295,28 +313,13 @@ class _LeaveWorkspaceButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Theme.of(context).colorScheme.error,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(
color: Theme.of(context).colorScheme.error,
width: 0.5,
),
),
),
onPressed: () => _leaveWorkspace(context),
child: FlowyText(
LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
fontSize: 14.0,
color: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.w500,
),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AFOutlinedTextButton.destructive(
alignment: Alignment.center,
text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
onTap: () => _leaveWorkspace(context),
size: AFButtonSize.l,
),
);
}
@@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -27,31 +28,34 @@ class MobileMemberList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SlidableAutoCloseBehavior(
child: SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
separatorBuilder: () => const FlowyDivider(
padding: EdgeInsets.symmetric(horizontal: 16.0),
final theme = AppFlowyTheme.of(context);
return SingleChildScrollView(
child: SlidableAutoCloseBehavior(
child: SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
separatorBuilder: () => SizedBox.shrink(),
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text(
'Joined',
style: theme.textStyle.heading4.enhanced(
color: theme.textColorScheme.primary,
),
),
),
...members.map(
(member) => _MemberItem(
member: member,
myRole: myRole,
userProfile: userProfile,
),
),
],
),
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: FlowyText.semibold(
LocaleKeys.settings_appearance_members_label.tr(),
fontSize: 16.0,
),
),
...members.map(
(member) => _MemberItem(
member: member,
myRole: myRole,
userProfile: userProfile,
),
),
],
),
);
}
@@ -70,8 +74,8 @@ class _MemberItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
final canDelete = myRole.canDelete && member.email != userProfile.email;
final textColor = member.role.isOwner ? Theme.of(context).hintColor : null;
Widget child;
@@ -79,17 +83,19 @@ class _MemberItem extends StatelessWidget {
child = Row(
children: [
Expanded(
child: FlowyText.medium(
child: Text(
member.name,
color: textColor,
fontSize: 15.0,
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.primary,
),
),
),
Expanded(
child: FlowyText.medium(
child: Text(
member.role.description,
color: textColor,
fontSize: 15.0,
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.secondary,
),
textAlign: TextAlign.end,
),
),
@@ -99,18 +105,19 @@ class _MemberItem extends StatelessWidget {
child = Row(
children: [
Expanded(
child: FlowyText.medium(
child: Text(
member.name,
color: textColor,
fontSize: 15.0,
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.primary,
),
overflow: TextOverflow.ellipsis,
),
),
const HSpace(36.0),
FlowyText.medium(
Text(
member.role.description,
color: textColor,
fontSize: 15.0,
style: theme.textStyle.heading4.standard(
color: theme.textColorScheme.secondary,
),
textAlign: TextAlign.end,
),
],
@@ -118,8 +125,10 @@ class _MemberItem extends StatelessWidget {
}
child = Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: EdgeInsets.symmetric(
horizontal: theme.spacing.xl,
vertical: theme.spacing.l,
),
child: child,
);
@@ -1,6 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../widgets/widgets.dart';
@@ -13,17 +17,32 @@ class WorkspaceSettingGroup extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MobileSettingGroup(
groupTitle: LocaleKeys.settings_appearance_members_label.tr(),
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_appearance_members_label.tr(),
trailing: const Icon(Icons.chevron_right),
onTap: () {
context.push(InviteMembersScreen.routeName);
},
),
],
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
builder: (context, state) {
final currentWorkspace = state.workspaces.firstWhereOrNull(
(e) => e.workspaceId == state.currentWorkspace?.workspaceId,
);
final memberCount = currentWorkspace?.memberCount;
String memberCountText = '';
// if the member count is greater than 0, show the member count
if (memberCount != null && memberCount > 0) {
memberCountText = memberCount.toString();
}
return MobileSettingGroup(
groupTitle: LocaleKeys.settings_appearance_members_label.tr(),
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_appearance_members_label.tr(),
trailing: MobileSettingTrailing(
text: memberCountText,
),
onTap: () {
context.push(InviteMembersScreen.routeName);
},
),
],
);
},
);
}
}
@@ -72,6 +72,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
final request = ViewIdPB(value: chatId);
unawaited(FolderEventCloseView(request).send());
selectedSourcesNotifier.dispose();
chatController.dispose();
return super.close();
}
@@ -576,6 +577,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
if (temporaryMessageIDMap.containsKey(messageId)) {
messageId = temporaryMessageIDMap[messageId]!;
}
final metadata = message.metadata == 'null' ? '[]' : message.metadata;
return TextMessage(
author: User(id: message.authorId),
@@ -583,7 +585,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
text: message.content,
createdAt: message.createdAt.toDateTime(),
metadata: {
messageRefSourceJsonStringKey: message.metadata,
messageRefSourceJsonStringKey: metadata,
},
);
}
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
@@ -288,7 +289,7 @@ class _ChatContentPage extends StatelessWidget {
return ChatMessage(
message: message,
animation: animation,
padding: const EdgeInsets.symmetric(vertical: 12.0),
padding: const EdgeInsets.symmetric(vertical: 18.0),
receivedMessageScaleAnimationAlignment: Alignment.center,
child: child,
);
@@ -413,7 +414,7 @@ class _InputState extends State<_Input> {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) {
return SizeTransition(
return NonClippingSizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
@@ -134,7 +134,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
@override
Widget build(BuildContext context) {
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final userProfile = userWorkspaceBloc.userProfile;
final userProfile = userWorkspaceBloc.state.userProfile;
final workspaceId =
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
@@ -448,7 +448,7 @@ class _ChangeModelButtonState extends State<ChangeModelButton> {
child: buildButton(context),
popupBuilder: (_) {
final bloc = context.read<AIPromptInputBloc>();
final (models, _) = bloc.aiModelStateNotifier.getAvailableModels();
final (models, _) = bloc.aiModelStateNotifier.getModelSelection();
return SelectModelPopoverContent(
models: models,
selectedModel: null,
@@ -516,7 +516,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
@override
Widget build(BuildContext context) {
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final userProfile = userWorkspaceBloc.userProfile;
final userProfile = userWorkspaceBloc.state.userProfile;
final workspaceId =
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
@@ -407,7 +407,7 @@ class ChatAIMessagePopup extends StatelessWidget {
return MobileQuickActionButton(
onTap: () async {
final bloc = context.read<AIPromptInputBloc>();
final (models, _) = bloc.aiModelStateNotifier.getAvailableModels();
final (models, _) = bloc.aiModelStateNotifier.getModelSelection();
final result = await showChangeModelBottomSheet(context, models);
if (result != null) {
onChangeModel?.call(result);
@@ -45,6 +45,7 @@ class ChatUserMessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Spacer(),
_buildBubble(context),
const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing),
_buildAvatar(),
@@ -3,9 +3,9 @@ import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/domain/date_cell_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/util/int64_extension.dart';
@@ -55,10 +55,11 @@ class DateCellEditorBloc
didReceiveCellUpdate: (DateCellDataPB? cellData) {
final dateCellData = DateCellData.fromPB(cellData);
ReminderOption reminderOption = ReminderOption.none;
ReminderOption reminderOption = state.reminderOption;
if (dateCellData.reminderId.isNotEmpty &&
dateCellData.dateTime != null) {
dateCellData.dateTime != null &&
reminderOption != ReminderOption.none) {
final reminder = _reminderBloc.state.reminders
.firstWhereOrNull((r) => r.id == dateCellData.reminderId);
if (reminder != null) {
@@ -112,13 +113,15 @@ class DateCellEditorBloc
clearDate: () async {
// Remove reminder if neccessary
if (state.reminderId.isNotEmpty) {
_reminderBloc
.add(ReminderEvent.remove(reminderId: state.reminderId));
_reminderBloc.add(
ReminderEvent.removeReminder(reminderId: state.reminderId),
);
}
await _clearDate();
},
setReminderOption: (ReminderOption option) async {
emit(state.copyWith(reminderOption: option));
await _setReminderOption(option);
},
);
@@ -216,7 +219,7 @@ class DateCellEditorBloc
} else {
if (option == ReminderOption.none) {
// remove reminder from reminder bloc and cell data
_reminderBloc.add(ReminderEvent.remove(reminderId: state.reminderId));
_reminderBloc.add(ReminderEvent.removeReminder(reminderId: state.reminderId));
await _updateCellReminderId("");
} else {
// Update reminder
@@ -246,9 +249,7 @@ class DateCellEditorBloc
void _updateReminderIfNecessary(
DateTime? dateTime,
) {
if (state.reminderId.isEmpty ||
state.reminderOption == ReminderOption.none ||
dateTime == null) {
if (state.reminderId.isEmpty || dateTime == null) {
return;
}
@@ -400,7 +401,7 @@ class DateCellEditorState with _$DateCellEditorState {
ReminderOption reminderOption = ReminderOption.none;
if (dateCellData.reminderId.isNotEmpty && dateCellData.dateTime != null) {
final reminder = reminderBloc.state.reminders
final reminder = reminderBloc.state.allReminders
.firstWhereOrNull((r) => r.id == dateCellData.reminderId);
if (reminder != null) {
final eventDate = dateCellData.includeTime
@@ -31,7 +31,7 @@ class DatabaseSyncBloc extends Bloc<DatabaseSyncEvent, DatabaseSyncBlocState> {
emit(
state.copyWith(
shouldShowIndicator:
userProfile?.workspaceAuthType == AuthTypePB.Server &&
userProfile?.workspaceType == WorkspaceTypePB.ServerW &&
databaseId != null,
),
);
@@ -280,6 +280,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
child: ChecklistCellTextfield(
textController: textController,
focusNode: textFieldFocusNode,
lineHeight: Platform.isWindows ? 1.2 : 1.1,
onChanged: () {
_debounceOnChanged.call(() {
if (!isComposing) {
@@ -46,6 +46,7 @@ class ChecklistCellTextfield extends StatelessWidget {
horizontal: 2,
),
this.onSubmitted,
this.lineHeight,
});
final TextEditingController textController;
@@ -53,13 +54,17 @@ class ChecklistCellTextfield extends StatelessWidget {
final EdgeInsetsGeometry contentPadding;
final VoidCallback? onSubmitted;
final VoidCallback? onChanged;
final double? lineHeight;
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.bodyMedium;
return TextField(
controller: textController,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium,
style: textStyle?.copyWith(
height: lineHeight,
),
maxLines: null,
decoration: InputDecoration(
border: InputBorder.none,
@@ -69,8 +69,8 @@ class RowBanner extends StatefulWidget {
class _RowBannerState extends State<RowBanner> {
final _isHovering = ValueNotifier(false);
late final isLocalMode =
(widget.userProfile?.workspaceAuthType ?? AuthTypePB.Local) ==
AuthTypePB.Local;
(widget.userProfile?.workspaceType ?? WorkspaceTypePB.LocalW) ==
WorkspaceTypePB.LocalW;
@override
void dispose() {
@@ -101,8 +101,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
bool get isLocalMode {
final userProfilePB = state.userProfilePB;
final type = userProfilePB?.workspaceAuthType ?? AuthTypePB.Local;
return type == AuthTypePB.Local;
final type = userProfilePB?.workspaceType ?? WorkspaceTypePB.LocalW;
return type == WorkspaceTypePB.LocalW;
}
@override
@@ -32,7 +32,7 @@ class DocumentCollaboratorsBloc
emit(
state.copyWith(
shouldShowIndicator:
userProfile?.workspaceAuthType == AuthTypePB.Server,
userProfile?.workspaceType == WorkspaceTypePB.ServerW,
),
);
final deviceId = ApplicationInfo.deviceId;
@@ -31,7 +31,7 @@ class DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {
emit(
state.copyWith(
shouldShowIndicator:
userProfile?.workspaceAuthType == AuthTypePB.Server,
userProfile?.workspaceType == WorkspaceTypePB.ServerW,
),
);
_syncStateListener.start(
@@ -68,10 +68,13 @@ Map<String, BlockComponentBuilder> buildBlockComponentBuilders({
bool editable = true,
ShowPlaceholder? showParagraphPlaceholder,
String Function(Node)? placeholderText,
EdgeInsets? customHeadingPadding,
EdgeInsets Function(Node node)? customPadding,
bool alwaysDistributeSimpleTableColumnWidths = false,
}) {
final configuration = _buildDefaultConfiguration(context);
final configuration = _buildDefaultConfiguration(
context,
padding: customPadding,
);
final builders = _buildBlockComponentBuilderMap(
context,
configuration: configuration,
@@ -97,7 +100,10 @@ Map<String, BlockComponentBuilder> buildBlockComponentBuilders({
return builders;
}
BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) {
BlockComponentConfiguration _buildDefaultConfiguration(
BuildContext context, {
EdgeInsets Function(Node node)? padding,
}) {
final configuration = BlockComponentConfiguration(
padding: (node) {
if (UniversalPlatform.isMobile) {
@@ -117,7 +123,7 @@ BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) {
right: EditorStyleCustomizer.nodeHorizontalPadding,
);
}
return edgeInsets;
return padding?.call(node) ?? edgeInsets;
}
return const EdgeInsets.symmetric(vertical: 5.0);
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
@@ -28,15 +29,14 @@ class AiWriterCubit extends Cubit<AiWriterState> {
this.onCreateNode,
this.onRemoveNode,
this.onAppendToDocument,
AppFlowyAIService? aiService,
}) : _aiService = aiService ?? AppFlowyAIService(),
}) : _aiService = getIt<AIRepository>(),
_textRobot = MarkdownTextRobot(editorState: editorState),
selectedSourcesNotifier = ValueNotifier([documentId]),
super(IdleAiWriterState());
final String documentId;
final EditorState editorState;
final AppFlowyAIService _aiService;
final AIRepository _aiService;
final MarkdownTextRobot _textRobot;
final void Function()? onCreateNode;
final void Function()? onRemoveNode;
@@ -306,12 +306,14 @@ class AiWriterCubit extends Cubit<AiWriterState> {
// check the node is registered
if (node == null) {
Log.warn('[AI writer] Node is null');
return (false, '');
}
// check the selection is valid
final selection = node.aiWriterSelection?.normalized;
if (selection == null) {
Log.warn('[AI writer]Selection is null');
return (false, '');
}
@@ -58,6 +58,7 @@ class _AiWriterScrollWrapperState extends State<AiWriterScrollWrapper> {
@override
void dispose() {
aiWriterCubit.close();
throttler.dispose();
super.dispose();
}
@@ -31,15 +31,22 @@ Future<bool> _convertBacktickToCodeBlock({
}
// only active when the backtick is at the beginning of the line
final keyword = '``';
final plainText = delta.toPlainText();
if (plainText != '``') {
if (!plainText.startsWith(keyword)) {
return false;
}
final transaction = editorState.transaction;
transaction.insertNode(
final deltaWithoutKeyword = delta.compose(Delta()..delete(keyword.length));
transaction.insertNodes(
selection.end.path,
codeBlockNode(),
[
codeBlockNode(
delta: deltaWithoutKeyword,
),
if (node.children.isNotEmpty) ...node.children.map((e) => e.copyWith()),
],
);
transaction.deleteNode(node);
transaction.afterSelection = Selection.collapsed(
Loaded 100 of 304 files, more files were not shown because too many files have changed in this diff. Show more