Compare commits
70
Commits
0.5.9
..
build/test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2991be276 | ||
|
|
3645dd1b8b | ||
|
|
4237a73810 | ||
|
|
80a2ebc216 | ||
|
|
02a9f0dfbb | ||
|
|
4a8536d403 | ||
|
|
9c3f083684 | ||
|
|
628eace341 | ||
|
|
77a53ef67a | ||
|
|
5dec6ea545 | ||
|
|
dbcf8ba86f | ||
|
|
63fe1a6ef3 | ||
|
|
a2c52508fa | ||
|
|
0709a2d2ee | ||
|
|
e73b310076 | ||
|
|
7586a0ed48 | ||
|
|
d8b32168a9 | ||
|
|
8ca541bfa1 | ||
|
|
b64da2c02f | ||
|
|
fdaca36b87 | ||
|
|
8557383a89 | ||
|
|
fa86480458 | ||
|
|
d75eb05707 | ||
|
|
a2b32585a1 | ||
|
|
75cea400d2 | ||
|
|
ed82ec8eef | ||
|
|
b6debd6c71 | ||
|
|
a05ed63a3e | ||
|
|
3e75f1f24a | ||
|
|
e607694729 | ||
|
|
91d870415e | ||
|
|
21a497924c | ||
|
|
4a126e17ce | ||
|
|
8bf97ad5c6 | ||
|
|
a85d11fb6d | ||
|
|
1fd34bc58a | ||
|
|
70a7b856a2 | ||
|
|
49683e6079 | ||
|
|
13107b63e9 | ||
|
|
ce1a6e4fca | ||
|
|
1d23e28eaf | ||
|
|
2a4d78b688 | ||
|
|
cbe452a73d | ||
|
|
27899ee993 | ||
|
|
785597f53e | ||
|
|
e2ce274718 | ||
|
|
6be9c00127 | ||
|
|
aa621a8d84 | ||
|
|
2d4300e931 | ||
|
|
dc12938ab6 | ||
|
|
2d674060c6 | ||
|
|
ecca81f3b8 | ||
|
|
15329b39d0 | ||
|
|
bd5f5f8b9e | ||
|
|
4f4be7eac7 | ||
|
|
f1a4deb459 | ||
|
|
4708c0f779 | ||
|
|
3d7a500550 | ||
|
|
815c99710e | ||
|
|
c9fe93920f | ||
|
|
5012b99c9b | ||
|
|
6b46372aa0 | ||
|
|
b6d873db1b | ||
|
|
38d2bd7ee8 | ||
|
|
bb3e9d5bd8 | ||
|
|
94060a0a99 | ||
|
|
86696b271e | ||
|
|
76fde00cc4 | ||
|
|
b794f3894e | ||
|
|
3b72f90ca5 |
No files matched your search
@@ -1,28 +0,0 @@
|
||||
name: Build Bot
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
dispatch_slash_command:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# get build name from pubspec.yaml
|
||||
- name: Get build version
|
||||
working-directory: frontend/appflowy_flutter
|
||||
id: get_build_name
|
||||
run: |
|
||||
echo "fetching version from pubspec.yaml..."
|
||||
echo "build_name=$(grep 'version: ' pubspec.yaml | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: peter-evans/slash-command-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commands: build
|
||||
static-args: |
|
||||
ref=refs/pull/${{ github.event.issue.number }}/head
|
||||
build_name=${{ steps.get_build_name.outputs.build_name }}
|
||||
@@ -69,4 +69,4 @@ jobs:
|
||||
SCRIPT_AFTER: |
|
||||
docker build -t appflowy-web-app .
|
||||
docker rm -f appflowy-web-app || true
|
||||
docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app
|
||||
docker run -d -p 80:80 -p 443:443 --env-file .env --name appflowy-web-app appflowy-web-app
|
||||
@@ -32,14 +32,14 @@ jobs:
|
||||
# swap-size-mb: 1024
|
||||
# remove-dotnet: 'true'
|
||||
|
||||
# # the following step is required to avoid running out of space
|
||||
# - name: Maximize build space
|
||||
# run: |
|
||||
# sudo rm -rf /usr/share/dotnet
|
||||
# sudo rm -rf /opt/ghc
|
||||
# sudo rm -rf "/usr/local/share/boost"
|
||||
# sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
# sudo docker image prune --all --force
|
||||
# the following step is required to avoid running out of space
|
||||
- name: Maximize build space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- ".github/workflows/web2_ci.yaml"
|
||||
- "frontend/appflowy_web_app/**"
|
||||
- "frontend/resources/**"
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
@@ -52,8 +53,13 @@ jobs:
|
||||
run: |
|
||||
pnpm run test:unit
|
||||
|
||||
- name: Generate and post coverage summary
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run merge-coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: cf9245e0-e136-4e21-b0ee-35755fa0c493
|
||||
files: frontend/appflowy_web_app/coverage/jest/lcov.info,frontend/appflowy_web_app/coverage/cypress/lcov.info
|
||||
flags: appflowy_web_app
|
||||
name: frontend/appflowy_web_app
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
# Release Notes
|
||||
## Version 0.6.0 - 19/06/2024
|
||||
### New Features
|
||||
- Introduced the "Space" feature to help you organize your pages more efficiently.
|
||||
### Bug Fixes
|
||||
- Resolved shortcut conflicts on the board page.
|
||||
- Resolved an issue where underscores could cause the editor to freeze.
|
||||
|
||||
## Version 0.5.9 - 06/06/2024
|
||||
### New Features
|
||||
- Revamped the sidebar for both Desktop and Mobile.
|
||||
- Added support for embedding videos in documents.
|
||||
- Introduced a hotkey (Cmd/Ctrl + 0) to reset the app scale.
|
||||
- Supported searching the workspace by page title.
|
||||
### Bug Fixes
|
||||
- Fixed the issue preventing the use of Backspace to delete words in Kanban boards.
|
||||
|
||||
## Version 0.5.8 - 05/20/2024
|
||||
### New Features
|
||||
- Improvement to the Callout block to insert new lines
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.5.9"
|
||||
APPFLOWY_VERSION = "0.6.1"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -52,7 +52,7 @@ android {
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "io.appflowy.appflowy"
|
||||
minSdkVersion 23
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'anon_user_continue_test.dart' as anon_user_continue_test;
|
||||
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
|
||||
import 'empty_test.dart' as preset_af_cloud_env_test;
|
||||
// import 'document_sync_test.dart' as document_sync_test;
|
||||
import 'user_setting_sync_test.dart' as user_sync_test;
|
||||
import 'workspace/change_name_and_icon_test.dart'
|
||||
as change_workspace_name_and_icon_test;
|
||||
@@ -10,13 +9,8 @@ import 'workspace/collaborative_workspace_test.dart'
|
||||
|
||||
Future<void> main() async {
|
||||
preset_af_cloud_env_test.main();
|
||||
|
||||
appflowy_cloud_auth_test.main();
|
||||
|
||||
// document_sync_test.main();
|
||||
|
||||
user_sync_test.main();
|
||||
|
||||
anon_user_continue_test.main();
|
||||
|
||||
// workspace
|
||||
|
||||
+49
-49
@@ -32,69 +32,69 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final email = '${uuid()}@appflowy.io';
|
||||
// final email = '${uuid()}@appflowy.io';
|
||||
|
||||
group('collaborative workspace', () {
|
||||
// combine the create and delete workspace test to reduce the time
|
||||
testWidgets('create a new workspace, open it and then delete it',
|
||||
(tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
}
|
||||
// if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
email: email,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
// await tester.initializeAppFlowy(
|
||||
// cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
// email: email,
|
||||
// );
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const name = 'AppFlowy.IO';
|
||||
// the workspace will be opened after created
|
||||
await tester.createCollaborativeWorkspace(name);
|
||||
// const name = 'AppFlowy.IO';
|
||||
// // the workspace will be opened after created
|
||||
// await tester.createCollaborativeWorkspace(name);
|
||||
|
||||
final loading = find.byType(Loading);
|
||||
await tester.pumpUntilNotFound(loading);
|
||||
// final loading = find.byType(Loading);
|
||||
// await tester.pumpUntilNotFound(loading);
|
||||
|
||||
Finder success;
|
||||
// Finder success;
|
||||
|
||||
final Finder items = find.byType(WorkspaceMenuItem);
|
||||
// final Finder items = find.byType(WorkspaceMenuItem);
|
||||
|
||||
// delete the newly created workspace
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.pumpUntilFound(items);
|
||||
// // delete the newly created workspace
|
||||
// await tester.openCollaborativeWorkspaceMenu();
|
||||
// await tester.pumpUntilFound(items);
|
||||
|
||||
expect(items, findsNWidgets(2));
|
||||
expect(
|
||||
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
||||
name,
|
||||
);
|
||||
// expect(items, findsNWidgets(2));
|
||||
// expect(
|
||||
// tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
||||
// name,
|
||||
// );
|
||||
|
||||
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
|
||||
await tester.hoverOnWidget(
|
||||
secondWorkspace,
|
||||
onHover: () async {
|
||||
// click the more button
|
||||
final moreButton = find.byType(WorkspaceMoreActionList);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
// click the delete button
|
||||
final deleteButton = find.text(LocaleKeys.button_delete.tr());
|
||||
expect(deleteButton, findsOneWidget);
|
||||
await tester.tapButton(deleteButton);
|
||||
// see the delete confirm dialog
|
||||
final confirm =
|
||||
find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
|
||||
expect(confirm, findsOneWidget);
|
||||
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
// delete success
|
||||
success = find.text(LocaleKeys.workspace_createSuccess.tr());
|
||||
await tester.pumpUntilFound(success);
|
||||
expect(success, findsOneWidget);
|
||||
await tester.pumpUntilNotFound(success);
|
||||
},
|
||||
);
|
||||
// final secondWorkspace = find.byType(WorkspaceMenuItem).last;
|
||||
// await tester.hoverOnWidget(
|
||||
// secondWorkspace,
|
||||
// onHover: () async {
|
||||
// // click the more button
|
||||
// final moreButton = find.byType(WorkspaceMoreActionList);
|
||||
// expect(moreButton, findsOneWidget);
|
||||
// await tester.tapButton(moreButton);
|
||||
// // click the delete button
|
||||
// final deleteButton = find.text(LocaleKeys.button_delete.tr());
|
||||
// expect(deleteButton, findsOneWidget);
|
||||
// await tester.tapButton(deleteButton);
|
||||
// // see the delete confirm dialog
|
||||
// final confirm =
|
||||
// find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
|
||||
// expect(confirm, findsOneWidget);
|
||||
// await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
// // delete success
|
||||
// success = find.text(LocaleKeys.workspace_createSuccess.tr());
|
||||
// await tester.pumpUntilFound(success);
|
||||
// expect(success, findsOneWidget);
|
||||
// await tester.pumpUntilNotFound(success);
|
||||
// },
|
||||
// );
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -29,6 +29,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
|
||||
await tester.tapOKButton();
|
||||
expect(find.text(name), findsNothing);
|
||||
});
|
||||
|
||||
|
||||
@@ -41,15 +41,16 @@ void main() {
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
await tester.hoverOnFirstRowOfGrid(() async {
|
||||
// Open the row menu and then click the delete
|
||||
await tester.tapRowMenuButtonInGrid();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapDeleteOnRowMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Open the row menu and then click the delete
|
||||
await tester.tapRowMenuButtonInGrid();
|
||||
await tester.tapDeleteOnRowMenu();
|
||||
|
||||
// 3 initial rows - 1 deleted
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
await tester.pumpAndSettle();
|
||||
// 3 initial rows - 1 deleted
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('check number of row indicator in the initial grid',
|
||||
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -17,25 +17,25 @@ void main() {
|
||||
await tester.openSettingsPage(SettingsPage.notifications);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final switchFinder = find.byType(Switch).first;
|
||||
final toggleFinder = find.byType(Toggle).first;
|
||||
|
||||
// Defaults to enabled
|
||||
Switch switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, true);
|
||||
Toggle toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, true);
|
||||
|
||||
// Disable
|
||||
await tester.tap(switchFinder);
|
||||
await tester.tap(toggleFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, false);
|
||||
toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, false);
|
||||
|
||||
// Enable again
|
||||
await tester.tap(switchFinder);
|
||||
await tester.tap(toggleFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, true);
|
||||
toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/auth_operation.dart';
|
||||
import '../../shared/base.dart';
|
||||
import '../../shared/expectation.dart';
|
||||
import '../../shared/settings.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Settings Billing', () {
|
||||
testWidgets('Local auth cannot see plan+billing', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapSignInAsGuest();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// We check that another settings page is present to ensure
|
||||
// it's not a fluke
|
||||
expect(
|
||||
find.text(
|
||||
LocaleKeys.settings_workspacePage_menuLabel.tr(),
|
||||
skipOffstage: false,
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
LocaleKeys.settings_planPage_menuLabel.tr(),
|
||||
skipOffstage: false,
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
LocaleKeys.settings_billingPage_menuLabel.tr(),
|
||||
skipOffstage: false,
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'notifications_settings_test.dart' as notifications_settings_test;
|
||||
import 'settings_billing_test.dart' as settings_billing_test;
|
||||
import 'shortcuts_settings_test.dart' as shortcuts_settings_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
notifications_settings_test.main();
|
||||
settings_billing_test.main();
|
||||
shortcuts_settings_test.main();
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/keyboard.dart';
|
||||
import '../../shared/util.dart';
|
||||
import '../board/board_hide_groups_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('shortcuts test', () {
|
||||
testWidgets('can change and overwrite shortcut', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.shortcuts);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final backspaceCmd =
|
||||
LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr();
|
||||
|
||||
// Input "Delete" into the search field
|
||||
await tester.enterText(find.byType(TextField), backspaceCmd);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
),
|
||||
onHover: () async {
|
||||
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
LogicalKeyboardKey.delete,
|
||||
LogicalKeyboardKey.enter,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
// We expect to see conflict dialog
|
||||
expect(
|
||||
find.text(
|
||||
LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Press on confirm label
|
||||
await tester.tap(
|
||||
find.text(
|
||||
LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// We expect the first ShortcutSettingTile to have one
|
||||
// [KeyBadge] with `delete` label
|
||||
final first = tester.widget(find.byType(ShortcutSettingTile).first)
|
||||
as ShortcutSettingTile;
|
||||
expect(
|
||||
first.command.command,
|
||||
'delete',
|
||||
);
|
||||
|
||||
// And the second one which is `Delete left character` to have none
|
||||
// as it will have been overwritten
|
||||
final second = tester.widget(find.byType(ShortcutSettingTile).at(1))
|
||||
as ShortcutSettingTile;
|
||||
expect(
|
||||
second.command.command,
|
||||
'',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -44,6 +45,12 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
assert(isSwitched == value);
|
||||
}
|
||||
|
||||
void assertToggleValue(Finder finder, bool value) {
|
||||
final Toggle switchWidget = widget(finder);
|
||||
final isSwitched = switchWidget.value;
|
||||
assert(isSwitched == value);
|
||||
}
|
||||
|
||||
void assertEnableEncryptSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
find.descendant(
|
||||
@@ -65,10 +72,10 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
}
|
||||
|
||||
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
assertToggleValue(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyCloudEnableSync),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Toggle),
|
||||
),
|
||||
value,
|
||||
);
|
||||
@@ -86,7 +93,7 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
Future<void> toggleEnableSync(Type syncButton) async {
|
||||
final finder = find.descendant(
|
||||
of: find.byType(syncButton),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Toggle),
|
||||
);
|
||||
|
||||
await tapButton(finder);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -37,8 +36,6 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
AuthenticatorType? cloudType,
|
||||
String? email,
|
||||
}) async {
|
||||
VideoBlockKit.ensureInitialized();
|
||||
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
// Set the window size
|
||||
await binding.setSurfaceSize(windowSize);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@@ -22,6 +27,7 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
@@ -29,10 +35,6 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'emoji.dart';
|
||||
@@ -619,6 +621,18 @@ extension SettingsFinder on CommonFinders {
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first;
|
||||
|
||||
Finder findSettingsMenuScrollable() => find
|
||||
.descendant(
|
||||
of: find
|
||||
.descendant(
|
||||
of: find.byType(SettingsMenu),
|
||||
matching: find.byType(SingleChildScrollView),
|
||||
)
|
||||
.first,
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first;
|
||||
}
|
||||
|
||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
|
||||
@@ -130,12 +130,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await openPage('v020', layout: ViewLayoutPB.Grid);
|
||||
}
|
||||
|
||||
Future<void> hoverOnFirstRowOfGrid() async {
|
||||
Future<void> hoverOnFirstRowOfGrid([Future<void> Function()? onHover]) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
expect(findRow, findsWidgets);
|
||||
|
||||
final firstRow = findRow.first;
|
||||
await hoverOnWidget(firstRow);
|
||||
await hoverOnWidget(firstRow, onHover: onHover);
|
||||
}
|
||||
|
||||
Future<void> editCell({
|
||||
@@ -876,11 +876,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> tapRowMenuButtonInGrid() async {
|
||||
expect(find.byType(RowMenuButton), findsOneWidget);
|
||||
await tapButton(find.byType(RowMenuButton));
|
||||
}
|
||||
|
||||
/// Should call [tapRowMenuButtonInGrid] first.
|
||||
Future<void> tapDeleteOnRowMenu() async {
|
||||
expect(find.text(LocaleKeys.grid_row_delete.tr()), findsOneWidget);
|
||||
await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../desktop/board/board_hide_groups_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'common_operations.dart';
|
||||
|
||||
@@ -31,6 +32,14 @@ extension AppFlowySettings on WidgetTester {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is SettingsMenuElement && widget.page == page,
|
||||
);
|
||||
|
||||
await scrollUntilVisible(
|
||||
button,
|
||||
0,
|
||||
scrollable: find.findSettingsMenuScrollable(),
|
||||
);
|
||||
await pump();
|
||||
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
return;
|
||||
|
||||
@@ -58,12 +58,6 @@ PODS:
|
||||
- Flutter
|
||||
- keyboard_height_plugin (0.0.1):
|
||||
- Flutter
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
@@ -72,8 +66,6 @@ PODS:
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.14.2):
|
||||
- SDWebImage/Core (= 5.14.2)
|
||||
- SDWebImage/Core (5.14.2)
|
||||
@@ -91,10 +83,6 @@ PODS:
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
@@ -110,20 +98,14 @@ DEPENDENCIES:
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -161,20 +143,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||
keyboard_height_plugin:
|
||||
:path: ".symlinks/plugins/keyboard_height_plugin/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
||||
media_kit_video:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@@ -185,10 +159,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
||||
@@ -206,14 +176,10 @@ SPEC CHECKSUMS:
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||
@@ -222,8 +188,6 @@ SPEC CHECKSUMS:
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
|
||||
@@ -81,14 +81,24 @@ class KVKeys {
|
||||
/// The value is a double string.
|
||||
static const String scaleFactor = 'scaleFactor';
|
||||
|
||||
/// The key for saving the last opened space
|
||||
/// The key for saving the last opened tab (favorite, recent, space etc.)
|
||||
///
|
||||
/// The value is a int string.
|
||||
static const String lastOpenedSpace = 'lastOpenedSpace';
|
||||
|
||||
/// The key for saving the space order
|
||||
/// The key for saving the space tab order
|
||||
///
|
||||
/// The value is a json string with the following format:
|
||||
/// [0, 1, 2]
|
||||
static const String spaceOrder = 'spaceOrder';
|
||||
|
||||
/// The key for saving the last opened space id (space A, space B)
|
||||
///
|
||||
/// The value is a string.
|
||||
static const String lastOpenedSpaceId = 'lastOpenedSpaceId';
|
||||
|
||||
/// The key for saving the upgrade space tag
|
||||
///
|
||||
/// The value is a boolean string
|
||||
static const String hasUpgradedSpace = 'hasUpgradedSpace060';
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/window_title_bar.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CocoaWindowChannel {
|
||||
@@ -122,15 +123,21 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
|
||||
|
||||
return FlowyTooltip(
|
||||
richMessage: textSpan,
|
||||
child: FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context
|
||||
child: Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerDown: (_) => context
|
||||
.read<HomeSettingBloc>()
|
||||
.add(const HomeSettingEvent.collapseMenu()),
|
||||
iconPadding: const EdgeInsets.all(4.0),
|
||||
icon: context.read<HomeSettingBloc>().state.isMenuCollapsed
|
||||
? const FlowySvg(FlowySvgs.show_menu_s)
|
||||
: const FlowySvg(FlowySvgs.hide_menu_m),
|
||||
child: FlowyHover(
|
||||
child: Container(
|
||||
width: 24,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: const RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: FlowySvg(FlowySvgs.hide_menu_s),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as launcher;
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:scaled_app/scaled_app.dart';
|
||||
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
|
||||
import 'startup/startup.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0);
|
||||
VideoBlockKit.ensureInitialized();
|
||||
|
||||
await runAppFlowy();
|
||||
}
|
||||
+2
-1
@@ -33,6 +33,7 @@ class BlockActionBottomSheet extends StatelessWidget {
|
||||
FlowySvgs.arrow_up_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
showTopBorder: false,
|
||||
onTap: () => onAction(BlockActionBottomSheetType.insertAbove),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
@@ -48,7 +49,7 @@ class BlockActionBottomSheet extends StatelessWidget {
|
||||
FlowyOptionTile.text(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),
|
||||
leftIcon: const FlowySvg(FlowySvgs.m_duplicate_s),
|
||||
onTap: () => onAction(BlockActionBottomSheetType.duplicate),
|
||||
),
|
||||
|
||||
|
||||
+6
-7
@@ -1,7 +1,7 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
@@ -23,7 +23,7 @@ enum MobilePaneActionType {
|
||||
|
||||
MobileSlideActionButton actionButton(
|
||||
BuildContext context, {
|
||||
MobileViewCardType? cardType,
|
||||
MobilePageCardType? cardType,
|
||||
FolderSpaceType? spaceType,
|
||||
}) {
|
||||
switch (this) {
|
||||
@@ -133,13 +133,13 @@ enum MobilePaneActionType {
|
||||
|
||||
List<MobileViewItemBottomSheetBodyAction> _buildActions(
|
||||
ViewPB view, {
|
||||
MobileViewCardType? cardType,
|
||||
MobilePageCardType? cardType,
|
||||
}) {
|
||||
final isFavorite = view.isFavorite;
|
||||
|
||||
if (cardType != null) {
|
||||
switch (cardType) {
|
||||
case MobileViewCardType.recent:
|
||||
case MobilePageCardType.recent:
|
||||
return [
|
||||
isFavorite
|
||||
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
|
||||
@@ -150,7 +150,7 @@ enum MobilePaneActionType {
|
||||
MobileViewItemBottomSheetBodyAction.divider,
|
||||
MobileViewItemBottomSheetBodyAction.removeFromRecent,
|
||||
];
|
||||
case MobileViewCardType.favorite:
|
||||
case MobilePageCardType.favorite:
|
||||
return [
|
||||
isFavorite
|
||||
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
|
||||
@@ -179,10 +179,9 @@ ActionPane buildEndActionPane(
|
||||
BuildContext context,
|
||||
List<MobilePaneActionType> actions, {
|
||||
bool needSpace = true,
|
||||
MobileViewCardType? cardType,
|
||||
MobilePageCardType? cardType,
|
||||
FolderSpaceType? spaceType,
|
||||
}) {
|
||||
debugPrint('actions: $actions');
|
||||
return ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
extentRatio: actions.length / 5,
|
||||
|
||||
+5
-1
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -19,7 +20,10 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: double.infinity),
|
||||
constraints: BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: GridSize.headerHeight,
|
||||
),
|
||||
child: TextButton.icon(
|
||||
style: Theme.of(context).textButtonTheme.style?.copyWith(
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@@ -8,7 +10,6 @@ import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'mobile_create_field_screen.dart';
|
||||
@@ -28,6 +29,7 @@ const mobileSupportedFieldTypes = [
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Checkbox,
|
||||
FieldType.Checklist,
|
||||
// FieldType.Time,
|
||||
];
|
||||
|
||||
Future<FieldType?> showFieldTypeGridBottomSheet(
|
||||
|
||||
+1
@@ -119,6 +119,7 @@ class FieldOptionValues {
|
||||
case FieldType.RichText:
|
||||
case FieldType.URL:
|
||||
case FieldType.Checkbox:
|
||||
case FieldType.Time:
|
||||
return null;
|
||||
case FieldType.Number:
|
||||
return NumberTypeOptionPB(
|
||||
|
||||
+14
-7
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
@@ -68,7 +69,7 @@ class _MobileFavoriteSpaceState extends State<MobileFavoriteSpace>
|
||||
|
||||
if (favoriteState.views.isEmpty) {
|
||||
return const EmptySpacePlaceholder(
|
||||
type: MobileViewCardType.favorite,
|
||||
type: MobilePageCardType.favorite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,11 +93,17 @@ class _FavoriteViews extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderColor = Theme.of(context).isLightMode
|
||||
? const Color(0xFFE9E9EC)
|
||||
: const Color(0x1AFFFFFF);
|
||||
return Scrollbar(
|
||||
child: ListView.separated(
|
||||
key: const PageStorageKey('favorite_views_page_storage_key'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final view = favoriteViews[index];
|
||||
@@ -105,16 +112,16 @@ class _FavoriteViews extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
color: borderColor,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: MobileViewCard(
|
||||
child: MobileViewPage(
|
||||
key: ValueKey(view.item.id),
|
||||
view: view.item,
|
||||
timestamp: view.timestamp,
|
||||
type: MobileViewCardType.favorite,
|
||||
type: MobilePageCardType.favorite,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -28,9 +28,12 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
|
||||
return Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
vertical: HomeSpaceViewSizes.mVerticalPadding,
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
top: HomeSpaceViewSizes.mVerticalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: MobileFolders(
|
||||
user: widget.userProfile,
|
||||
|
||||
@@ -3,9 +3,11 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/home.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -34,16 +36,15 @@ class MobileFolders extends StatelessWidget {
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => SidebarSectionsBloc()
|
||||
..add(
|
||||
SidebarSectionsEvent.initial(
|
||||
user,
|
||||
workspaceId,
|
||||
),
|
||||
),
|
||||
..add(SidebarSectionsEvent.initial(user, workspaceId)),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => SpaceBloc()
|
||||
..add(SpaceEvent.initial(user, workspaceId, openFirstPage: false)),
|
||||
),
|
||||
],
|
||||
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: (context, state) {
|
||||
@@ -53,53 +54,88 @@ class MobileFolders extends StatelessWidget {
|
||||
state.currentWorkspace?.workspaceId ?? workspaceId,
|
||||
),
|
||||
);
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.reset(
|
||||
user,
|
||||
state.currentWorkspace?.workspaceId ?? workspaceId,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) {
|
||||
final lastCreatedRootView = state.lastCreatedRootView;
|
||||
if (lastCreatedRootView != null) {
|
||||
context.pushView(lastCreatedRootView);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isCollaborativeWorkspace =
|
||||
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: Column(
|
||||
children: [
|
||||
...isCollaborativeWorkspace
|
||||
? [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_workspace.tr(),
|
||||
spaceType: FolderSpaceType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
const VSpace(8.0),
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_private.tr(),
|
||||
spaceType: FolderSpaceType.private,
|
||||
views: state.section.privateViews,
|
||||
),
|
||||
]
|
||||
: [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_personal.tr(),
|
||||
spaceType: FolderSpaceType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
],
|
||||
const VSpace(4.0),
|
||||
const _TrashButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<SpaceBloc, SpaceState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedPage?.id != c.lastCreatedPage?.id,
|
||||
listener: (context, state) {
|
||||
final lastCreatedPage = state.lastCreatedPage;
|
||||
if (lastCreatedPage != null) {
|
||||
context.pushView(lastCreatedPage);
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) {
|
||||
final lastCreatedPage = state.lastCreatedRootView;
|
||||
if (lastCreatedPage != null) {
|
||||
context.pushView(lastCreatedPage);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
builder: (context, state) {
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: Column(
|
||||
children: [
|
||||
..._buildSpaceOrSection(context, state),
|
||||
const VSpace(4.0),
|
||||
const _TrashButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSpaceOrSection(
|
||||
BuildContext context,
|
||||
SidebarSectionsState state,
|
||||
) {
|
||||
if (context.watch<SpaceBloc>().state.spaces.isNotEmpty) {
|
||||
return [
|
||||
const MobileSpace(),
|
||||
];
|
||||
}
|
||||
|
||||
if (context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn) {
|
||||
return [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_workspace.tr(),
|
||||
spaceType: FolderSpaceType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
const VSpace(8.0),
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_private.tr(),
|
||||
spaceType: FolderSpaceType.private,
|
||||
views: state.section.privateViews,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_personal.tr(),
|
||||
spaceType: FolderSpaceType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _TrashButton extends StatelessWidget {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
@@ -15,7 +18,6 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -57,6 +59,7 @@ class MobileHomeScreen extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Provider.value(
|
||||
value: userProfile,
|
||||
child: MobileHomePage(
|
||||
@@ -111,6 +114,9 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||
create: (context) =>
|
||||
FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
buildWhen: (previous, current) =>
|
||||
|
||||
+14
-7
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/recent/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
@@ -36,7 +37,7 @@ class _MobileRecentSpaceState extends State<MobileRecentSpace>
|
||||
|
||||
if (recentViews.isEmpty) {
|
||||
return const Center(
|
||||
child: EmptySpacePlaceholder(type: MobileViewCardType.recent),
|
||||
child: EmptySpacePlaceholder(type: MobilePageCardType.recent),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,12 +64,18 @@ class _RecentViews extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderColor = Theme.of(context).isLightMode
|
||||
? const Color(0xFFE9E9EC)
|
||||
: const Color(0x1AFFFFFF);
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
key: const PageStorageKey('recent_views_page_storage_key'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final sectionView = recentViews[index];
|
||||
@@ -77,16 +84,16 @@ class _RecentViews extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
color: borderColor,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: MobileViewCard(
|
||||
child: MobileViewPage(
|
||||
key: ValueKey(sectionView.item.id),
|
||||
view: sectionView.item,
|
||||
timestamp: sectionView.timestamp,
|
||||
type: MobileViewCardType.recent,
|
||||
type: MobilePageCardType.recent,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
|
||||
class EmptySpacePlaceholder extends StatelessWidget {
|
||||
const EmptySpacePlaceholder({super.key, required this.type});
|
||||
|
||||
final MobileViewCardType type;
|
||||
final MobilePageCardType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -42,14 +42,14 @@ class EmptySpacePlaceholder extends StatelessWidget {
|
||||
}
|
||||
|
||||
String get _emptyPageText => switch (type) {
|
||||
MobileViewCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(),
|
||||
MobileViewCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(),
|
||||
MobilePageCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(),
|
||||
MobilePageCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(),
|
||||
};
|
||||
|
||||
String get _emptyPageSubText => switch (type) {
|
||||
MobileViewCardType.recent =>
|
||||
MobilePageCardType.recent =>
|
||||
LocaleKeys.sideBar_emptyRecentDescription.tr(),
|
||||
MobileViewCardType.favorite =>
|
||||
MobilePageCardType.favorite =>
|
||||
LocaleKeys.sideBar_emptyFavoriteDescription.tr(),
|
||||
};
|
||||
}
|
||||
+72
-78
@@ -10,15 +10,16 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
|
||||
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||
import 'package:appflowy/shared/flowy_gradient_colors.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.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:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -26,19 +27,20 @@ import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:time/time.dart';
|
||||
|
||||
enum MobileViewCardType {
|
||||
enum MobilePageCardType {
|
||||
recent,
|
||||
favorite;
|
||||
|
||||
String get lastOperationHintText => switch (this) {
|
||||
MobileViewCardType.recent => LocaleKeys.sideBar_lastViewed.tr(),
|
||||
MobileViewCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(),
|
||||
MobilePageCardType.recent => LocaleKeys.sideBar_lastViewed.tr(),
|
||||
MobilePageCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(),
|
||||
};
|
||||
}
|
||||
|
||||
class MobileViewCard extends StatelessWidget {
|
||||
const MobileViewCard({
|
||||
class MobileViewPage extends StatelessWidget {
|
||||
const MobileViewPage({
|
||||
super.key,
|
||||
required this.view,
|
||||
this.timestamp,
|
||||
@@ -47,7 +49,7 @@ class MobileViewCard extends StatelessWidget {
|
||||
|
||||
final ViewPB view;
|
||||
final Int64? timestamp;
|
||||
final MobileViewCardType type;
|
||||
final MobilePageCardType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -78,7 +80,6 @@ class MobileViewCard extends StatelessWidget {
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapUp: (_) => context.pushView(view),
|
||||
onLongPressUp: () => _showActionSheet(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -157,6 +158,7 @@ class MobileViewCard extends StatelessWidget {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: _ViewCover(
|
||||
layout: view.layout,
|
||||
coverTypeV1: state.coverTypeV1,
|
||||
coverTypeV2: state.coverTypeV2,
|
||||
value: state.coverValue,
|
||||
@@ -189,6 +191,7 @@ class MobileViewCard extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -207,40 +210,45 @@ class MobileViewCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildLastViewed(BuildContext context) {
|
||||
final textColor = Theme.of(context).isLightMode
|
||||
? const Color(0xFF171717)
|
||||
: Colors.white.withOpacity(0.45);
|
||||
if (timestamp == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final date = _formatTimestamp(
|
||||
context,
|
||||
timestamp!.toInt() * 1000,
|
||||
);
|
||||
return FlowyText.regular(
|
||||
date,
|
||||
fontSize: 12.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 13.0,
|
||||
color: textColor,
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTimestamp(int timestamp) {
|
||||
String _formatTimestamp(BuildContext context, int timestamp) {
|
||||
final now = DateTime.now();
|
||||
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
final difference = now.difference(dateTime);
|
||||
final String date;
|
||||
|
||||
final dateFormate =
|
||||
context.read<AppearanceSettingsCubit>().state.dateFormat;
|
||||
final timeFormate =
|
||||
context.read<AppearanceSettingsCubit>().state.timeFormat;
|
||||
|
||||
if (difference.inMinutes < 1) {
|
||||
date = LocaleKeys.sideBar_justNow.tr();
|
||||
} else if (difference.inHours < 1) {
|
||||
} else if (difference.inHours < 1 && dateTime.isToday) {
|
||||
// Less than 1 hour
|
||||
date = LocaleKeys.sideBar_minutesAgo
|
||||
.tr(namedArgs: {'count': difference.inMinutes.toString()});
|
||||
} else if (difference.inHours >= 1 && difference.inHours < 24) {
|
||||
// Between 1 hour and 24 hours
|
||||
date = DateFormat('h:mm a').format(dateTime);
|
||||
} else if (difference.inDays >= 1 && dateTime.year == now.year) {
|
||||
// More than 24 hours but within the current year
|
||||
date = DateFormat('M/d, h:mm a').format(dateTime);
|
||||
} else if (difference.inHours >= 1 && dateTime.isToday) {
|
||||
// in same day
|
||||
date = timeFormate.formatTime(dateTime);
|
||||
} else {
|
||||
// Other cases (previous years)
|
||||
date = DateFormat('M/d/yyyy, h:mm a').format(dateTime);
|
||||
date = dateFormate.formatDate(dateTime, false);
|
||||
}
|
||||
|
||||
if (difference.inHours >= 1) {
|
||||
@@ -249,77 +257,24 @@ class MobileViewCard extends StatelessWidget {
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
Future<void> _showActionSheet(BuildContext context) async {
|
||||
final viewBloc = context.read<ViewBloc>();
|
||||
final favoriteBloc = context.read<FavoriteBloc>();
|
||||
final recentViewsBloc = context.read<RecentViewsBloc?>();
|
||||
await showMobileBottomSheet(
|
||||
context,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
backgroundColor: AFThemeExtension.of(context).background,
|
||||
useRootNavigator: true,
|
||||
builder: (context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: viewBloc),
|
||||
BlocProvider.value(value: favoriteBloc),
|
||||
if (recentViewsBloc != null)
|
||||
BlocProvider.value(value: recentViewsBloc),
|
||||
],
|
||||
child: BlocBuilder<ViewBloc, ViewState>(
|
||||
builder: (context, state) {
|
||||
return MobileViewItemBottomSheet(
|
||||
view: viewBloc.state.view,
|
||||
actions: _buildActions(state.view),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<MobileViewItemBottomSheetBodyAction> _buildActions(ViewPB view) {
|
||||
switch (type) {
|
||||
case MobileViewCardType.recent:
|
||||
return [
|
||||
view.isFavorite
|
||||
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
|
||||
: MobileViewItemBottomSheetBodyAction.addToFavorites,
|
||||
MobileViewItemBottomSheetBodyAction.divider,
|
||||
if (view.layout != ViewLayoutPB.Chat)
|
||||
MobileViewItemBottomSheetBodyAction.duplicate,
|
||||
MobileViewItemBottomSheetBodyAction.divider,
|
||||
MobileViewItemBottomSheetBodyAction.removeFromRecent,
|
||||
];
|
||||
case MobileViewCardType.favorite:
|
||||
return [
|
||||
MobileViewItemBottomSheetBodyAction.removeFromFavorites,
|
||||
MobileViewItemBottomSheetBodyAction.divider,
|
||||
MobileViewItemBottomSheetBodyAction.duplicate,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewCover extends StatelessWidget {
|
||||
const _ViewCover({
|
||||
required this.layout,
|
||||
required this.coverTypeV1,
|
||||
this.coverTypeV2,
|
||||
this.value,
|
||||
});
|
||||
|
||||
final ViewLayoutPB layout;
|
||||
final CoverType coverTypeV1;
|
||||
final PageStyleCoverImageType? coverTypeV2;
|
||||
final String? value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final placeholder = Container(
|
||||
color: const Color(0xFFE1FBFF),
|
||||
);
|
||||
final placeholder = _buildPlaceholder(context);
|
||||
final value = this.value;
|
||||
if (value == null) {
|
||||
return placeholder;
|
||||
@@ -330,6 +285,45 @@ class _ViewCover extends StatelessWidget {
|
||||
return _buildCoverV1(context, value, placeholder);
|
||||
}
|
||||
|
||||
Widget _buildPlaceholder(BuildContext context) {
|
||||
final isLightMode = Theme.of(context).isLightMode;
|
||||
final (svg, color) = switch (layout) {
|
||||
ViewLayoutPB.Document => (
|
||||
FlowySvgs.m_document_thumbnail_m,
|
||||
isLightMode ? const Color(0xCCEDFBFF) : const Color(0x33658B90)
|
||||
),
|
||||
ViewLayoutPB.Grid => (
|
||||
FlowySvgs.m_grid_thumbnail_m,
|
||||
isLightMode ? const Color(0xFFF5F4FF) : const Color(0x338B80AD)
|
||||
),
|
||||
ViewLayoutPB.Board => (
|
||||
FlowySvgs.m_board_thumbnail_m,
|
||||
isLightMode ? const Color(0x7FE0FDD9) : const Color(0x3372936B),
|
||||
),
|
||||
ViewLayoutPB.Calendar => (
|
||||
FlowySvgs.m_calendar_thumbnail_m,
|
||||
isLightMode ? const Color(0xFFFFF7F0) : const Color(0x33A68B77)
|
||||
),
|
||||
ViewLayoutPB.Chat => (
|
||||
FlowySvgs.m_chat_thumbnail_m,
|
||||
isLightMode ? const Color(0x66FFE6FD) : const Color(0x33987195)
|
||||
),
|
||||
_ => (
|
||||
FlowySvgs.m_document_thumbnail_m,
|
||||
isLightMode ? Colors.black : Colors.white
|
||||
)
|
||||
};
|
||||
return ColoredBox(
|
||||
color: color,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
svg,
|
||||
blendMode: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) {
|
||||
final type = coverTypeV2;
|
||||
if (type == null) {
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileSpace extends StatefulWidget {
|
||||
const MobileSpace({super.key});
|
||||
|
||||
@override
|
||||
State<MobileSpace> createState() => _MobileSpaceState();
|
||||
}
|
||||
|
||||
class _MobileSpaceState extends State<MobileSpace> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createNewPageNotifier.addListener(_createNewPage);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
createNewPageNotifier.removeListener(_createNewPage);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SpaceBloc, SpaceState>(
|
||||
builder: (context, state) {
|
||||
if (state.spaces.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final currentSpace = state.currentSpace ?? state.spaces.first;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
MobileSpaceHeader(
|
||||
isExpanded: state.isExpanded,
|
||||
space: currentSpace,
|
||||
onAdded: () {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
index: 0,
|
||||
),
|
||||
);
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.expand(currentSpace, true),
|
||||
);
|
||||
},
|
||||
onPressed: () => _showSpaceMenu(context),
|
||||
),
|
||||
_Pages(
|
||||
key: ValueKey(currentSpace.id),
|
||||
space: currentSpace,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showSpaceMenu(BuildContext context) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDivider: false,
|
||||
showHeader: true,
|
||||
showDragHandle: true,
|
||||
showCloseButton: true,
|
||||
showDoneButton: true,
|
||||
title: LocaleKeys.space_title.tr(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<SpaceBloc>(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: MobileSpaceMenu(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _createNewPage() {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Pages extends StatelessWidget {
|
||||
const _Pages({
|
||||
super.key,
|
||||
required this.space,
|
||||
});
|
||||
|
||||
final ViewPB space;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
ViewBloc(view: space)..add(const ViewEvent.initial()),
|
||||
child: BlocBuilder<ViewBloc, ViewState>(
|
||||
builder: (context, state) {
|
||||
final spaceType = space.spacePermission == SpacePermission.publicToAll
|
||||
? FolderSpaceType.public
|
||||
: FolderSpaceType.private;
|
||||
return Column(
|
||||
children: state.view.childViews
|
||||
.map(
|
||||
(view) => MobileViewItem(
|
||||
key: ValueKey(
|
||||
'${space.id} ${view.id}',
|
||||
),
|
||||
spaceType: spaceType,
|
||||
isFirstChild: view.id == state.view.childViews.first.id,
|
||||
view: view,
|
||||
level: 0,
|
||||
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||
isFeedback: false,
|
||||
onSelected: context.pushView,
|
||||
endActionPane: (context) {
|
||||
final view = context.read<ViewBloc>().state.view;
|
||||
return buildEndActionPane(
|
||||
context,
|
||||
[
|
||||
MobilePaneActionType.more,
|
||||
if (view.layout == ViewLayoutPB.Document)
|
||||
MobilePaneActionType.add,
|
||||
],
|
||||
spaceType: spaceType,
|
||||
needSpace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@visibleForTesting
|
||||
const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
|
||||
|
||||
class MobileSpaceHeader extends StatelessWidget {
|
||||
const MobileSpaceHeader({
|
||||
super.key,
|
||||
required this.space,
|
||||
required this.onPressed,
|
||||
required this.onAdded,
|
||||
required this.isExpanded,
|
||||
});
|
||||
|
||||
final ViewPB space;
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback onAdded;
|
||||
final bool isExpanded;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onPressed,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
SpaceIcon(
|
||||
dimension: 24,
|
||||
space: space,
|
||||
cornerRadius: 6.0,
|
||||
),
|
||||
const HSpace(8),
|
||||
FlowyText.medium(
|
||||
space.name,
|
||||
lineHeight: 1.15,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const HSpace(4.0),
|
||||
const FlowySvg(
|
||||
FlowySvgs.workspace_drop_down_menu_show_s,
|
||||
),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onAdded,
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.m_space_add_s,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Future<void> _onAction(SpaceMoreActionType type, dynamic data) async {
|
||||
// switch (type) {
|
||||
// case SpaceMoreActionType.rename:
|
||||
// await _showRenameDialog();
|
||||
// break;
|
||||
// case SpaceMoreActionType.changeIcon:
|
||||
// final (String icon, String iconColor) = data;
|
||||
// context.read<SpaceBloc>().add(SpaceEvent.changeIcon(icon, iconColor));
|
||||
// break;
|
||||
// case SpaceMoreActionType.manage:
|
||||
// _showManageSpaceDialog(context);
|
||||
// break;
|
||||
// case SpaceMoreActionType.addNewSpace:
|
||||
// break;
|
||||
// case SpaceMoreActionType.collapseAllPages:
|
||||
// break;
|
||||
// case SpaceMoreActionType.delete:
|
||||
// _showDeleteSpaceDialog(context);
|
||||
// break;
|
||||
// case SpaceMoreActionType.divider:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<void> _showRenameDialog() async {
|
||||
// await NavigatorTextFieldDialog(
|
||||
// title: LocaleKeys.space_rename.tr(),
|
||||
// value: space.name,
|
||||
// autoSelectAllText: true,
|
||||
// onConfirm: (name, _) {
|
||||
// context.read<SpaceBloc>().add(SpaceEvent.rename(space, name));
|
||||
// },
|
||||
// ).show(context);
|
||||
// }
|
||||
|
||||
// void _showManageSpaceDialog(BuildContext context) {
|
||||
// final spaceBloc = context.read<SpaceBloc>();
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (_) {
|
||||
// return Dialog(
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(12.0),
|
||||
// ),
|
||||
// child: BlocProvider.value(
|
||||
// value: spaceBloc,
|
||||
// child: const ManageSpacePopup(),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// void _showDeleteSpaceDialog(BuildContext context) {
|
||||
// final spaceBloc = context.read<SpaceBloc>();
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (_) {
|
||||
// return Dialog(
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(12.0),
|
||||
// ),
|
||||
// child: BlocProvider.value(
|
||||
// value: spaceBloc,
|
||||
// child: const SizedBox(width: 440, child: DeleteSpacePopup()),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileSpaceMenu extends StatelessWidget {
|
||||
const MobileSpaceMenu({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SpaceBloc, SpaceState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const VSpace(4.0),
|
||||
for (final space in state.spaces)
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: _SidebarSpaceMenuItem(
|
||||
space: space,
|
||||
isSelected: state.currentSpace?.id == space.id,
|
||||
),
|
||||
),
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
// child: Divider(
|
||||
// height: 0.5,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 52,
|
||||
// child: _CreateSpaceButton(),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SidebarSpaceMenuItem extends StatelessWidget {
|
||||
const _SidebarSpaceMenuItem({
|
||||
required this.space,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final ViewPB space;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
text: Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
space.name,
|
||||
),
|
||||
const HSpace(6.0),
|
||||
if (space.spacePermission == SpacePermission.private)
|
||||
const FlowySvg(
|
||||
FlowySvgs.space_lock_s,
|
||||
size: Size.square(12),
|
||||
),
|
||||
],
|
||||
),
|
||||
iconPadding: 10,
|
||||
leftIcon: SpaceIcon(
|
||||
dimension: 24,
|
||||
space: space,
|
||||
cornerRadius: 6.0,
|
||||
),
|
||||
leftIconSize: const Size.square(20),
|
||||
rightIcon: isSelected
|
||||
? const FlowySvg(
|
||||
FlowySvgs.workspace_selected_s,
|
||||
blendMode: null,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
context.read<SpaceBloc>().add(SpaceEvent.open(space));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// class _CreateSpaceButton extends StatelessWidget {
|
||||
// const _CreateSpaceButton();
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return FlowyButton(
|
||||
// text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()),
|
||||
// iconPadding: 10,
|
||||
// leftIcon: const FlowySvg(
|
||||
// FlowySvgs.space_add_s,
|
||||
// ),
|
||||
// leftIconSize: const Size.square(20),
|
||||
// onTap: () {
|
||||
// PopoverContainer.of(context).close();
|
||||
// _showCreateSpaceDialog(context);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// void _showCreateSpaceDialog(BuildContext context) {
|
||||
// final spaceBloc = context.read<SpaceBloc>();
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (_) {
|
||||
// return Dialog(
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(12.0),
|
||||
// ),
|
||||
// child: BlocProvider.value(
|
||||
// value: spaceBloc,
|
||||
// child: const CreateSpacePopup(),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -21,8 +21,8 @@ class MobileSpaceTabBar extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final baseStyle = Theme.of(context).textTheme.bodyMedium;
|
||||
final labelStyle = baseStyle?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
final unselectedLabelStyle = baseStyle?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
|
||||
+40
-58
@@ -1,15 +1,15 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final PropertyValueNotifier<ViewLayoutPB?> createNewPageNotifier =
|
||||
PropertyValueNotifier(null);
|
||||
|
||||
const _homeLabel = 'home';
|
||||
const _addLabel = 'add';
|
||||
const _notificationLabel = 'notification';
|
||||
@@ -46,22 +46,40 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLightMode = Theme.of(context).isLightMode;
|
||||
final backgroundColor = isLightMode
|
||||
? Colors.white.withOpacity(0.95)
|
||||
: const Color(0xFF23262B).withOpacity(0.95);
|
||||
return Scaffold(
|
||||
body: navigationShell,
|
||||
bottomNavigationBar: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
enableFeedback: false,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
items: _items,
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
|
||||
extendBody: true,
|
||||
bottomNavigationBar: ClipRRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 3,
|
||||
sigmaY: 3,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: isLightMode
|
||||
? Border(
|
||||
top: BorderSide(color: Theme.of(context).dividerColor),
|
||||
)
|
||||
: null,
|
||||
color: backgroundColor,
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
enableFeedback: false,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
items: _items,
|
||||
backgroundColor: Colors.transparent,
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -72,7 +90,7 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
void _onTap(BuildContext context, int bottomBarIndex) {
|
||||
if (_items[bottomBarIndex].label == _addLabel) {
|
||||
// show an add dialog
|
||||
_showCreatePageBottomSheet(context);
|
||||
createNewPageNotifier.value = ViewLayoutPB.Document;
|
||||
return;
|
||||
}
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
@@ -87,40 +105,4 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
initialLocation: bottomBarIndex == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreatePageBottomSheet(BuildContext context) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: LocaleKeys.sideBar_addAPage.tr(),
|
||||
showDragHandle: true,
|
||||
showCloseButton: true,
|
||||
useRootNavigator: true,
|
||||
builder: (sheetContext) {
|
||||
return AddNewPageWidgetBottomSheet(
|
||||
view: ViewPB(),
|
||||
onAction: (layout) async {
|
||||
Navigator.of(sheetContext).pop();
|
||||
final currentWorkspaceId =
|
||||
await FolderEventReadCurrentWorkspace().send();
|
||||
await currentWorkspaceId.fold((s) async {
|
||||
final workspaceService = WorkspaceService(workspaceId: s.id);
|
||||
final result = await workspaceService.createView(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
viewSection: ViewSectionPB.Private,
|
||||
layout: layout,
|
||||
);
|
||||
result.fold((s) {
|
||||
context.pushView(s);
|
||||
}, (e) {
|
||||
Log.error('Failed to create new page: $e');
|
||||
});
|
||||
}, (e) {
|
||||
Log.error('Failed to read current workspace: $e');
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -296,30 +296,29 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
final icon = widget.view.icon.value.isNotEmpty
|
||||
? FlowyText.emoji(
|
||||
widget.view.icon.value,
|
||||
fontSize: 20.0,
|
||||
fontSize: 18.0,
|
||||
)
|
||||
: Opacity(
|
||||
opacity: 0.7,
|
||||
child: SizedBox.square(
|
||||
dimension: 18.0,
|
||||
child: widget.view.defaultIcon(),
|
||||
),
|
||||
child: widget.view.defaultIcon(),
|
||||
);
|
||||
return icon;
|
||||
return SizedBox(width: 18.0, child: icon);
|
||||
}
|
||||
|
||||
// > button or · button
|
||||
// show > if the view is expandable.
|
||||
// show · if the view can't contain child views.
|
||||
Widget _buildLeftIcon() {
|
||||
const rightPadding = 6.0;
|
||||
if (context.read<ViewBloc>().state.view.childViews.isEmpty) {
|
||||
return HSpace(widget.leftPadding);
|
||||
return HSpace(widget.leftPadding + rightPadding);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 6.0, top: 6.0, bottom: 6.0),
|
||||
padding:
|
||||
const EdgeInsets.only(right: rightPadding, top: 6.0, bottom: 6.0),
|
||||
child: FlowySvg(
|
||||
widget.isExpanded ? FlowySvgs.m_expand_s : FlowySvgs.m_collapse_s,
|
||||
blendMode: null,
|
||||
|
||||
+102
-13
@@ -1,42 +1,131 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'chat_ai_message_bloc.freezed.dart';
|
||||
|
||||
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
ChatAIMessageBloc({
|
||||
required Message message,
|
||||
dynamic message,
|
||||
required this.chatId,
|
||||
required this.questionId,
|
||||
}) : super(ChatAIMessageState.initial(message)) {
|
||||
if (state.stream != null) {
|
||||
_subscription = state.stream!.listen((text) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.startsWith("data:")) {
|
||||
add(ChatAIMessageEvent.newText(text.substring(5)));
|
||||
} else if (text.startsWith("error:")) {
|
||||
add(ChatAIMessageEvent.receiveError(text.substring(5)));
|
||||
}
|
||||
});
|
||||
|
||||
if (state.stream!.error != null) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (!isClosed) {
|
||||
add(ChatAIMessageEvent.receiveError(state.stream!.error!));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
on<ChatAIMessageEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {},
|
||||
update: (userProfile, deviceId, states) {},
|
||||
newText: (newText) {
|
||||
emit(state.copyWith(text: state.text + newText, error: null));
|
||||
},
|
||||
receiveError: (error) {
|
||||
emit(state.copyWith(error: error));
|
||||
},
|
||||
retry: () {
|
||||
if (questionId is! Int64) {
|
||||
Log.error("Question id is not Int64: $questionId");
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
retryState: const LoadingState.loading(),
|
||||
error: null,
|
||||
),
|
||||
);
|
||||
|
||||
final payload = ChatMessageIdPB(
|
||||
chatId: chatId,
|
||||
messageId: questionId,
|
||||
);
|
||||
ChatEventGetAnswerForQuestion(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold(
|
||||
(answer) {
|
||||
add(ChatAIMessageEvent.retryResult(answer.content));
|
||||
},
|
||||
(err) {
|
||||
Log.error("Failed to get answer: $err");
|
||||
add(ChatAIMessageEvent.receiveError(err.toString()));
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
retryResult: (String text) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
text: text,
|
||||
error: null,
|
||||
retryState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_subscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
StreamSubscription<AnswerStreamElement>? _subscription;
|
||||
final String chatId;
|
||||
final Int64? questionId;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
||||
const factory ChatAIMessageEvent.initial() = Initial;
|
||||
const factory ChatAIMessageEvent.update(
|
||||
UserProfilePB userProfile,
|
||||
String deviceId,
|
||||
DocumentAwarenessStatesPB states,
|
||||
) = Update;
|
||||
const factory ChatAIMessageEvent.newText(String text) = _NewText;
|
||||
const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError;
|
||||
const factory ChatAIMessageEvent.retry() = _Retry;
|
||||
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatAIMessageState with _$ChatAIMessageState {
|
||||
const factory ChatAIMessageState({
|
||||
required Message message,
|
||||
AnswerStream? stream,
|
||||
String? error,
|
||||
required String text,
|
||||
required LoadingState retryState,
|
||||
}) = _ChatAIMessageState;
|
||||
|
||||
factory ChatAIMessageState.initial(Message message) =>
|
||||
ChatAIMessageState(message: message);
|
||||
factory ChatAIMessageState.initial(dynamic text) {
|
||||
return ChatAIMessageState(
|
||||
text: text is String ? text : "",
|
||||
stream: text is AnswerStream ? text : null,
|
||||
retryState: const LoadingState.finish(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
@@ -10,11 +16,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
import 'chat_message_listener.dart';
|
||||
|
||||
part 'chat_bloc.freezed.dart';
|
||||
|
||||
const canRetryKey = "canRetry";
|
||||
const sendMessageErrorKey = "sendMessageError";
|
||||
|
||||
class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
@@ -26,78 +32,31 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
super(
|
||||
ChatState.initial(view, userProfile),
|
||||
) {
|
||||
_startListening();
|
||||
_dispatch();
|
||||
|
||||
listener.start(
|
||||
chatMessageCallback: _handleChatMessage,
|
||||
lastUserSentMessageCallback: (message) {
|
||||
if (!isClosed) {
|
||||
add(ChatEvent.didSentUserMessage(message));
|
||||
}
|
||||
},
|
||||
chatErrorMessageCallback: (err) {
|
||||
if (!isClosed) {
|
||||
Log.error("chat error: ${err.errorMessage}");
|
||||
final metadata = OnetimeShotType.serverStreamError.toMap();
|
||||
if (state.lastSentMessage != null) {
|
||||
metadata[canRetryKey] = "true";
|
||||
}
|
||||
final error = CustomMessage(
|
||||
metadata: metadata,
|
||||
author: const User(id: "system"),
|
||||
id: 'system',
|
||||
);
|
||||
add(ChatEvent.streaming([error]));
|
||||
add(const ChatEvent.didFinishStreaming());
|
||||
}
|
||||
},
|
||||
latestMessageCallback: (list) {
|
||||
if (!isClosed) {
|
||||
final messages = list.messages.map(_createChatMessage).toList();
|
||||
add(ChatEvent.didLoadLatestMessages(messages));
|
||||
}
|
||||
},
|
||||
prevMessageCallback: (list) {
|
||||
if (!isClosed) {
|
||||
final messages = list.messages.map(_createChatMessage).toList();
|
||||
add(ChatEvent.didLoadPreviousMessages(messages, list.hasMore));
|
||||
}
|
||||
},
|
||||
finishAnswerQuestionCallback: () {
|
||||
if (!isClosed) {
|
||||
add(const ChatEvent.didFinishStreaming());
|
||||
if (state.lastSentMessage != null) {
|
||||
final payload = ChatMessageIdPB(
|
||||
chatId: chatId,
|
||||
messageId: state.lastSentMessage!.messageId,
|
||||
);
|
||||
// When user message was sent to the server, we start gettting related question
|
||||
ChatEventGetRelatedQuestion(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold(
|
||||
(list) {
|
||||
add(
|
||||
ChatEvent.didReceiveRelatedQuestion(list.items),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
Log.error("Failed to get related question: $err");
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final ChatMessageListener listener;
|
||||
final String chatId;
|
||||
|
||||
/// The last streaming message id
|
||||
String lastStreamMessageId = '';
|
||||
|
||||
/// Using a temporary map to associate the real message ID with the last streaming message ID.
|
||||
///
|
||||
/// When a message is streaming, it does not have a real message ID. To maintain the relationship
|
||||
/// between the real message ID and the last streaming message ID, we use this map to store the associations.
|
||||
///
|
||||
/// This map will be updated when receiving a message from the server and its author type
|
||||
/// is 3 (AI response).
|
||||
final HashMap<String, String> temporaryMessageIDMap = HashMap();
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
listener.stop();
|
||||
Future<void> close() async {
|
||||
if (state.answerStream != null) {
|
||||
await state.answerStream?.dispose();
|
||||
}
|
||||
await listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -110,12 +69,25 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
chatId: state.view.id,
|
||||
limit: Int64(10),
|
||||
);
|
||||
ChatEventLoadNextMessage(payload).send();
|
||||
ChatEventLoadNextMessage(payload).send().then(
|
||||
(result) {
|
||||
result.fold((list) {
|
||||
if (!isClosed) {
|
||||
final messages =
|
||||
list.messages.map(_createTextMessage).toList();
|
||||
add(ChatEvent.didLoadLatestMessages(messages));
|
||||
}
|
||||
}, (err) {
|
||||
Log.error("Failed to load messages: $err");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
startLoadingPrevMessage: () async {
|
||||
Int64? beforeMessageId;
|
||||
if (state.messages.isNotEmpty) {
|
||||
beforeMessageId = Int64.parseInt(state.messages.last.id);
|
||||
final oldestMessage = _getOlderstMessage();
|
||||
if (oldestMessage != null) {
|
||||
beforeMessageId = Int64.parseInt(oldestMessage.id);
|
||||
}
|
||||
_loadPrevMessage(beforeMessageId);
|
||||
emit(
|
||||
@@ -126,8 +98,13 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
},
|
||||
didLoadPreviousMessages: (List<Message> messages, bool hasMore) {
|
||||
Log.debug("did load previous messages: ${messages.length}");
|
||||
final uniqueMessages = {...state.messages, ...messages}.toList()
|
||||
final onetimeMessages = _getOnetimeMessages();
|
||||
final allMessages = _perminentMessages();
|
||||
final uniqueMessages = {...allMessages, ...messages}.toList()
|
||||
..sort((a, b) => b.id.compareTo(a.id));
|
||||
|
||||
uniqueMessages.insertAll(0, onetimeMessages);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: uniqueMessages,
|
||||
@@ -137,8 +114,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
},
|
||||
didLoadLatestMessages: (List<Message> messages) {
|
||||
final uniqueMessages = {...state.messages, ...messages}.toList()
|
||||
final onetimeMessages = _getOnetimeMessages();
|
||||
final allMessages = _perminentMessages();
|
||||
final uniqueMessages = {...allMessages, ...messages}.toList()
|
||||
..sort((a, b) => b.id.compareTo(a.id));
|
||||
uniqueMessages.insertAll(0, onetimeMessages);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: uniqueMessages,
|
||||
@@ -146,55 +127,43 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
streaming: (List<Message> messages) {
|
||||
streaming: (Message message) {
|
||||
final allMessages = _perminentMessages();
|
||||
allMessages.insertAll(0, messages);
|
||||
emit(state.copyWith(messages: allMessages));
|
||||
},
|
||||
didFinishStreaming: () {
|
||||
allMessages.insert(0, message);
|
||||
emit(
|
||||
state.copyWith(
|
||||
answerQuestionStatus: const LoadingState.finish(),
|
||||
messages: allMessages,
|
||||
streamingStatus: const LoadingState.loading(),
|
||||
),
|
||||
);
|
||||
},
|
||||
sendMessage: (String message) async {
|
||||
await _handleSentMessage(message, emit);
|
||||
|
||||
// Create a loading indicator
|
||||
final loadingMessage =
|
||||
_loadingMessage(state.userProfile.id.toString());
|
||||
final allMessages = List<Message>.from(state.messages)
|
||||
..insert(0, loadingMessage);
|
||||
|
||||
didFinishStreaming: () {
|
||||
emit(
|
||||
state.copyWith(streamingStatus: const LoadingState.finish()),
|
||||
);
|
||||
},
|
||||
receveMessage: (Message message) {
|
||||
final allMessages = _perminentMessages();
|
||||
// remove message with the same id
|
||||
allMessages.removeWhere((element) => element.id == message.id);
|
||||
allMessages.insert(0, message);
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: allMessages,
|
||||
),
|
||||
);
|
||||
},
|
||||
sendMessage: (String message) {
|
||||
_startStreamingMessage(message, emit);
|
||||
final allMessages = _perminentMessages();
|
||||
emit(
|
||||
state.copyWith(
|
||||
lastSentMessage: null,
|
||||
messages: allMessages,
|
||||
answerQuestionStatus: const LoadingState.loading(),
|
||||
relatedQuestions: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
retryGenerate: () {
|
||||
if (state.lastSentMessage == null) {
|
||||
return;
|
||||
}
|
||||
final payload = ChatMessageIdPB(
|
||||
chatId: chatId,
|
||||
messageId: state.lastSentMessage!.messageId,
|
||||
);
|
||||
ChatEventGetAnswerForQuestion(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold(
|
||||
(answer) => _handleChatMessage(answer),
|
||||
(err) {
|
||||
Log.error("Failed to get answer: $err");
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
didReceiveRelatedQuestion: (List<RelatedQuestionPB> questions) {
|
||||
final allMessages = _perminentMessages();
|
||||
final message = CustomMessage(
|
||||
@@ -224,11 +193,104 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
didUpdateAnswerStream: (AnswerStream stream) {
|
||||
emit(state.copyWith(answerStream: stream));
|
||||
},
|
||||
stopStream: () async {
|
||||
if (state.answerStream == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final payload = StopStreamPB(chatId: chatId);
|
||||
await ChatEventStopStream(payload).send();
|
||||
final allMessages = _perminentMessages();
|
||||
if (state.streamingStatus != const LoadingState.finish()) {
|
||||
// If the streaming is not started, remove the message from the list
|
||||
if (!state.answerStream!.hasStarted) {
|
||||
allMessages.removeWhere(
|
||||
(element) => element.id == lastStreamMessageId,
|
||||
);
|
||||
lastStreamMessageId = "";
|
||||
}
|
||||
|
||||
// when stop stream, we will set the answer stream to null. Which means the streaming
|
||||
// is finished or canceled.
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: allMessages,
|
||||
answerStream: null,
|
||||
streamingStatus: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
listener.start(
|
||||
chatMessageCallback: (pb) {
|
||||
if (!isClosed) {
|
||||
// 3 mean message response from AI
|
||||
if (pb.authorType == 3 && lastStreamMessageId.isNotEmpty) {
|
||||
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||
lastStreamMessageId;
|
||||
lastStreamMessageId = "";
|
||||
}
|
||||
|
||||
final message = _createTextMessage(pb);
|
||||
add(ChatEvent.receveMessage(message));
|
||||
}
|
||||
},
|
||||
chatErrorMessageCallback: (err) {
|
||||
if (!isClosed) {
|
||||
Log.error("chat error: ${err.errorMessage}");
|
||||
add(const ChatEvent.didFinishStreaming());
|
||||
}
|
||||
},
|
||||
latestMessageCallback: (list) {
|
||||
if (!isClosed) {
|
||||
final messages = list.messages.map(_createTextMessage).toList();
|
||||
add(ChatEvent.didLoadLatestMessages(messages));
|
||||
}
|
||||
},
|
||||
prevMessageCallback: (list) {
|
||||
if (!isClosed) {
|
||||
final messages = list.messages.map(_createTextMessage).toList();
|
||||
add(ChatEvent.didLoadPreviousMessages(messages, list.hasMore));
|
||||
}
|
||||
},
|
||||
finishStreamingCallback: () {
|
||||
if (!isClosed) {
|
||||
add(const ChatEvent.didFinishStreaming());
|
||||
// The answer strema will bet set to null after the streaming is finished or canceled.
|
||||
// so if the answer stream is null, we will not get related question.
|
||||
if (state.lastSentMessage != null && state.answerStream != null) {
|
||||
final payload = ChatMessageIdPB(
|
||||
chatId: chatId,
|
||||
messageId: state.lastSentMessage!.messageId,
|
||||
);
|
||||
// When user message was sent to the server, we start gettting related question
|
||||
ChatEventGetRelatedQuestion(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold(
|
||||
(list) {
|
||||
add(ChatEvent.didReceiveRelatedQuestion(list.items));
|
||||
},
|
||||
(err) {
|
||||
Log.error("Failed to get related question: $err");
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Returns the list of messages that are not include one-time messages.
|
||||
List<Message> _perminentMessages() {
|
||||
final allMessages = state.messages.where((element) {
|
||||
@@ -238,6 +300,22 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
return allMessages;
|
||||
}
|
||||
|
||||
List<Message> _getOnetimeMessages() {
|
||||
final messages = state.messages.where((element) {
|
||||
return (element.metadata?.containsKey(onetimeShotType) == true);
|
||||
}).toList();
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
Message? _getOlderstMessage() {
|
||||
// get the last message that is not a one-time message
|
||||
final message = state.messages.lastWhereOrNull((element) {
|
||||
return !(element.metadata?.containsKey(onetimeShotType) == true);
|
||||
});
|
||||
return message;
|
||||
}
|
||||
|
||||
void _loadPrevMessage(Int64? beforeMessageId) {
|
||||
final payload = LoadPrevChatMessagePB(
|
||||
chatId: state.view.id,
|
||||
@@ -247,68 +325,92 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
ChatEventLoadPrevMessage(payload).send();
|
||||
}
|
||||
|
||||
Future<void> _handleSentMessage(
|
||||
Future<void> _startStreamingMessage(
|
||||
String message,
|
||||
Emitter<ChatState> emit,
|
||||
) async {
|
||||
final payload = SendChatPayloadPB(
|
||||
if (state.answerStream != null) {
|
||||
await state.answerStream?.dispose();
|
||||
}
|
||||
|
||||
final answerStream = AnswerStream();
|
||||
add(ChatEvent.didUpdateAnswerStream(answerStream));
|
||||
|
||||
final payload = StreamChatPayloadPB(
|
||||
chatId: state.view.id,
|
||||
message: message,
|
||||
messageType: ChatMessageTypePB.User,
|
||||
textStreamPort: Int64(answerStream.nativePort),
|
||||
);
|
||||
final result = await ChatEventSendMessage(payload).send();
|
||||
|
||||
// Stream message to the server
|
||||
final result = await ChatEventStreamMessage(payload).send();
|
||||
result.fold(
|
||||
(_) {},
|
||||
(ChatMessagePB question) {
|
||||
if (!isClosed) {
|
||||
add(ChatEvent.didSentUserMessage(question));
|
||||
|
||||
final questionMessageId = question.messageId;
|
||||
final message = _createTextMessage(question);
|
||||
add(ChatEvent.receveMessage(message));
|
||||
|
||||
final streamAnswer =
|
||||
_createStreamMessage(answerStream, questionMessageId);
|
||||
add(ChatEvent.streaming(streamAnswer));
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
if (!isClosed) {
|
||||
Log.error("Failed to send message: ${err.msg}");
|
||||
final metadata = OnetimeShotType.invalidSendMesssage.toMap();
|
||||
metadata[sendMessageErrorKey] = err.msg;
|
||||
if (err.code != ErrorCode.Internal) {
|
||||
metadata[sendMessageErrorKey] = err.msg;
|
||||
}
|
||||
|
||||
final error = CustomMessage(
|
||||
metadata: metadata,
|
||||
author: const User(id: "system"),
|
||||
id: 'system',
|
||||
);
|
||||
|
||||
add(ChatEvent.streaming([error]));
|
||||
add(ChatEvent.receveMessage(error));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleChatMessage(ChatMessagePB pb) {
|
||||
if (!isClosed) {
|
||||
final message = _createChatMessage(pb);
|
||||
final messages = pb.hasFollowing
|
||||
? [_loadingMessage(0.toString()), message]
|
||||
: [message];
|
||||
add(ChatEvent.streaming(messages));
|
||||
}
|
||||
}
|
||||
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
|
||||
final streamMessageId = nanoid();
|
||||
lastStreamMessageId = streamMessageId;
|
||||
|
||||
Message _loadingMessage(String id) {
|
||||
return CustomMessage(
|
||||
author: User(id: id),
|
||||
metadata: OnetimeShotType.loading.toMap(),
|
||||
// fake id
|
||||
id: nanoid(),
|
||||
return TextMessage(
|
||||
author: User(id: nanoid()),
|
||||
metadata: {
|
||||
"$AnswerStream": stream,
|
||||
"question": questionMessageId,
|
||||
"chatId": chatId,
|
||||
},
|
||||
id: streamMessageId,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
text: '',
|
||||
);
|
||||
}
|
||||
|
||||
Message _createChatMessage(ChatMessagePB message) {
|
||||
final messageId = message.messageId.toString();
|
||||
Message _createTextMessage(ChatMessagePB message) {
|
||||
String messageId = message.messageId.toString();
|
||||
|
||||
/// If the message id is in the temporary map, we will use the previous fake message id
|
||||
if (temporaryMessageIDMap.containsKey(messageId)) {
|
||||
messageId = temporaryMessageIDMap[messageId]!;
|
||||
}
|
||||
|
||||
return TextMessage(
|
||||
author: User(id: message.authorId),
|
||||
id: messageId,
|
||||
text: message.content,
|
||||
createdAt: message.createdAt.toInt(),
|
||||
repliedMessage: _getReplyMessage(state.messages, messageId),
|
||||
createdAt: message.createdAt.toInt() * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
Message? _getReplyMessage(List<Message?> messages, String messageId) {
|
||||
return messages.firstWhereOrNull((element) => element?.id == messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -322,15 +424,20 @@ class ChatEvent with _$ChatEvent {
|
||||
) = _DidLoadPreviousMessages;
|
||||
const factory ChatEvent.didLoadLatestMessages(List<Message> messages) =
|
||||
_DidLoadMessages;
|
||||
const factory ChatEvent.streaming(List<Message> messages) = _DidStreamMessage;
|
||||
const factory ChatEvent.streaming(Message message) = _StreamingMessage;
|
||||
const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage;
|
||||
|
||||
const factory ChatEvent.didFinishStreaming() = _FinishStreamingMessage;
|
||||
const factory ChatEvent.didReceiveRelatedQuestion(
|
||||
List<RelatedQuestionPB> questions,
|
||||
) = _DidReceiveRelatedQueston;
|
||||
const factory ChatEvent.clearReleatedQuestion() = _ClearRelatedQuestion;
|
||||
const factory ChatEvent.retryGenerate() = _RetryGenerate;
|
||||
const factory ChatEvent.didSentUserMessage(ChatMessagePB message) =
|
||||
_DidSendUserMessage;
|
||||
const factory ChatEvent.didUpdateAnswerStream(
|
||||
AnswerStream stream,
|
||||
) = _DidUpdateAnswerStream;
|
||||
const factory ChatEvent.stopStream() = _StopStream;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -347,13 +454,14 @@ class ChatState with _$ChatState {
|
||||
required LoadingState loadingPreviousStatus,
|
||||
// When sending a user message, the status will be set as loading.
|
||||
// After the message is sent, the status will be set as finished.
|
||||
required LoadingState answerQuestionStatus,
|
||||
required LoadingState streamingStatus,
|
||||
// Indicate whether there are more previous messages to load.
|
||||
required bool hasMorePrevMessage,
|
||||
// The related questions that are received after the user message is sent.
|
||||
required List<RelatedQuestionPB> relatedQuestions,
|
||||
// The last user message that is sent to the server.
|
||||
ChatMessagePB? lastSentMessage,
|
||||
AnswerStream? answerStream,
|
||||
}) = _ChatState;
|
||||
|
||||
factory ChatState.initial(ViewPB view, UserProfilePB userProfile) =>
|
||||
@@ -363,7 +471,7 @@ class ChatState with _$ChatState {
|
||||
userProfile: userProfile,
|
||||
initialLoadingStatus: const LoadingState.finish(),
|
||||
loadingPreviousStatus: const LoadingState.finish(),
|
||||
answerQuestionStatus: const LoadingState.finish(),
|
||||
streamingStatus: const LoadingState.finish(),
|
||||
hasMorePrevMessage: true,
|
||||
relatedQuestions: [],
|
||||
);
|
||||
@@ -377,10 +485,8 @@ class LoadingState with _$LoadingState {
|
||||
|
||||
enum OnetimeShotType {
|
||||
unknown,
|
||||
loading,
|
||||
serverStreamError,
|
||||
relatedQuestion,
|
||||
invalidSendMesssage
|
||||
invalidSendMesssage,
|
||||
}
|
||||
|
||||
const onetimeShotType = "OnetimeShotType";
|
||||
@@ -388,10 +494,6 @@ const onetimeShotType = "OnetimeShotType";
|
||||
extension OnetimeMessageTypeExtension on OnetimeShotType {
|
||||
static OnetimeShotType fromString(String value) {
|
||||
switch (value) {
|
||||
case 'OnetimeShotType.loading':
|
||||
return OnetimeShotType.loading;
|
||||
case 'OnetimeShotType.serverStreamError':
|
||||
return OnetimeShotType.serverStreamError;
|
||||
case 'OnetimeShotType.relatedQuestion':
|
||||
return OnetimeShotType.relatedQuestion;
|
||||
case 'OnetimeShotType.invalidSendMesssage':
|
||||
@@ -402,7 +504,7 @@ extension OnetimeMessageTypeExtension on OnetimeShotType {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> toMap() {
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
onetimeShotType: toString(),
|
||||
};
|
||||
@@ -421,3 +523,43 @@ OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
typedef AnswerStreamElement = String;
|
||||
|
||||
class AnswerStream {
|
||||
AnswerStream() {
|
||||
_port.handler = _controller.add;
|
||||
_subscription = _controller.stream.listen(
|
||||
(event) {
|
||||
if (event.startsWith("data:")) {
|
||||
_hasStarted = true;
|
||||
} else if (event.startsWith("error:")) {
|
||||
_error = event.substring(5);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final RawReceivePort _port = RawReceivePort();
|
||||
final StreamController<AnswerStreamElement> _controller =
|
||||
StreamController.broadcast();
|
||||
late StreamSubscription<AnswerStreamElement> _subscription;
|
||||
bool _hasStarted = false;
|
||||
String? _error;
|
||||
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
bool get hasStarted => _hasStarted;
|
||||
String? get error => _error;
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _controller.close();
|
||||
await _subscription.cancel();
|
||||
_port.close();
|
||||
}
|
||||
|
||||
StreamSubscription<AnswerStreamElement> listen(
|
||||
void Function(AnswerStreamElement event)? onData,
|
||||
) {
|
||||
return _controller.stream.listen(onData);
|
||||
}
|
||||
}
|
||||
+5
-11
@@ -28,26 +28,23 @@ class ChatMessageListener {
|
||||
ChatNotificationParser? _parser;
|
||||
|
||||
ChatMessageCallback? chatMessageCallback;
|
||||
ChatMessageCallback? lastUserSentMessageCallback;
|
||||
ChatErrorMessageCallback? chatErrorMessageCallback;
|
||||
LatestMessageCallback? latestMessageCallback;
|
||||
PrevMessageCallback? prevMessageCallback;
|
||||
void Function()? finishAnswerQuestionCallback;
|
||||
void Function()? finishStreamingCallback;
|
||||
|
||||
void start({
|
||||
ChatMessageCallback? chatMessageCallback,
|
||||
ChatErrorMessageCallback? chatErrorMessageCallback,
|
||||
LatestMessageCallback? latestMessageCallback,
|
||||
PrevMessageCallback? prevMessageCallback,
|
||||
ChatMessageCallback? lastUserSentMessageCallback,
|
||||
void Function()? finishAnswerQuestionCallback,
|
||||
void Function()? finishStreamingCallback,
|
||||
}) {
|
||||
this.chatMessageCallback = chatMessageCallback;
|
||||
this.chatErrorMessageCallback = chatErrorMessageCallback;
|
||||
this.latestMessageCallback = latestMessageCallback;
|
||||
this.prevMessageCallback = prevMessageCallback;
|
||||
this.lastUserSentMessageCallback = lastUserSentMessageCallback;
|
||||
this.finishAnswerQuestionCallback = finishAnswerQuestionCallback;
|
||||
this.finishStreamingCallback = finishStreamingCallback;
|
||||
}
|
||||
|
||||
void _callback(
|
||||
@@ -59,9 +56,6 @@ class ChatMessageListener {
|
||||
case ChatNotification.DidReceiveChatMessage:
|
||||
chatMessageCallback?.call(ChatMessagePB.fromBuffer(r));
|
||||
break;
|
||||
case ChatNotification.LastUserSentMessage:
|
||||
lastUserSentMessageCallback?.call(ChatMessagePB.fromBuffer(r));
|
||||
break;
|
||||
case ChatNotification.StreamChatMessageError:
|
||||
chatErrorMessageCallback?.call(ChatMessageErrorPB.fromBuffer(r));
|
||||
break;
|
||||
@@ -71,8 +65,8 @@ class ChatMessageListener {
|
||||
case ChatNotification.DidLoadPrevChatMessage:
|
||||
prevMessageCallback?.call(ChatMessageListPB.fromBuffer(r));
|
||||
break;
|
||||
case ChatNotification.FinishAnswerQuestion:
|
||||
finishAnswerQuestionCallback?.call();
|
||||
case ChatNotification.FinishStreaming:
|
||||
finishStreamingCallback?.call();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_message_listener.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'chat_related_question_bloc.freezed.dart';
|
||||
|
||||
class ChatRelatedMessageBloc
|
||||
extends Bloc<ChatRelatedMessageEvent, ChatRelatedMessageState> {
|
||||
ChatRelatedMessageBloc({
|
||||
required String chatId,
|
||||
}) : listener = ChatMessageListener(chatId: chatId),
|
||||
super(ChatRelatedMessageState.initial()) {
|
||||
on<ChatRelatedMessageEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
listener.start(
|
||||
lastUserSentMessageCallback: (message) {
|
||||
if (!isClosed) {
|
||||
add(ChatRelatedMessageEvent.updateLastSentMessage(message));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
didReceiveRelatedQuestion: (List<RelatedQuestionPB> questions) {
|
||||
Log.debug("Related questions: $questions");
|
||||
emit(
|
||||
state.copyWith(
|
||||
relatedQuestions: questions,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateLastSentMessage: (ChatMessagePB message) {
|
||||
final payload =
|
||||
ChatMessageIdPB(chatId: chatId, messageId: message.messageId);
|
||||
ChatEventGetRelatedQuestion(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold(
|
||||
(list) {
|
||||
add(
|
||||
ChatRelatedMessageEvent.didReceiveRelatedQuestion(
|
||||
list.items,
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
Log.error("Failed to get related question: $err");
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
lastSentMessage: message,
|
||||
relatedQuestions: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
clear: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
relatedQuestions: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final ChatMessageListener listener;
|
||||
@override
|
||||
Future<void> close() {
|
||||
listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatRelatedMessageEvent with _$ChatRelatedMessageEvent {
|
||||
const factory ChatRelatedMessageEvent.initial() = Initial;
|
||||
const factory ChatRelatedMessageEvent.updateLastSentMessage(
|
||||
ChatMessagePB message,
|
||||
) = _LastSentMessage;
|
||||
const factory ChatRelatedMessageEvent.didReceiveRelatedQuestion(
|
||||
List<RelatedQuestionPB> questions,
|
||||
) = _RelatedQuestion;
|
||||
const factory ChatRelatedMessageEvent.clear() = _Clear;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatRelatedMessageState with _$ChatRelatedMessageState {
|
||||
const factory ChatRelatedMessageState({
|
||||
ChatMessagePB? lastSentMessage,
|
||||
@Default([]) List<RelatedQuestionPB> relatedQuestions,
|
||||
}) = _ChatRelatedMessageState;
|
||||
|
||||
factory ChatRelatedMessageState.initial() => const ChatRelatedMessageState();
|
||||
}
|
||||
+23
-9
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -13,9 +15,23 @@ class ChatUserMessageBloc
|
||||
}) : super(ChatUserMessageState.initial(message)) {
|
||||
on<ChatUserMessageEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {},
|
||||
update: (userProfile, deviceId, states) {},
|
||||
event.when(
|
||||
initial: () {
|
||||
final payload =
|
||||
WorkspaceMemberIdPB(uid: Int64.parseInt(message.author.id));
|
||||
UserEventGetMemberInfo(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold((member) {
|
||||
add(ChatUserMessageEvent.didReceiveMemberInfo(member));
|
||||
}, (err) {
|
||||
Log.error("Error getting member info: $err");
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
didReceiveMemberInfo: (WorkspaceMemberPB memberInfo) {
|
||||
emit(state.copyWith(member: memberInfo));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -25,11 +41,9 @@ class ChatUserMessageBloc
|
||||
@freezed
|
||||
class ChatUserMessageEvent with _$ChatUserMessageEvent {
|
||||
const factory ChatUserMessageEvent.initial() = Initial;
|
||||
const factory ChatUserMessageEvent.update(
|
||||
UserProfilePB userProfile,
|
||||
String deviceId,
|
||||
DocumentAwarenessStatesPB states,
|
||||
) = Update;
|
||||
const factory ChatUserMessageEvent.didReceiveMemberInfo(
|
||||
WorkspaceMemberPB memberInfo,
|
||||
) = _MemberInfo;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_ai_message.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_streaming_error_message.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_user_message.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/user_message_bubble.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
@@ -19,11 +18,12 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
|
||||
import 'presentation/chat_input.dart';
|
||||
import 'presentation/chat_loading.dart';
|
||||
import 'presentation/chat_popmenu.dart';
|
||||
import 'presentation/chat_theme.dart';
|
||||
import 'presentation/chat_user_invalid_message.dart';
|
||||
import 'presentation/chat_welcome_page.dart';
|
||||
import 'presentation/message/ai_text_message.dart';
|
||||
import 'presentation/message/user_text_message.dart';
|
||||
|
||||
class AIChatUILayout {
|
||||
static EdgeInsets get chatPadding =>
|
||||
@@ -108,7 +108,6 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
customBottomWidget: buildChatInput(blocContext),
|
||||
user: _user,
|
||||
theme: buildTheme(context),
|
||||
customMessageBuilder: _customMessageBuilder,
|
||||
onEndReached: () async {
|
||||
if (state.hasMorePrevMessage &&
|
||||
state.loadingPreviousStatus !=
|
||||
@@ -138,6 +137,13 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
},
|
||||
),
|
||||
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
||||
textMessageBuilder: (
|
||||
textMessage, {
|
||||
required messageWidth,
|
||||
required showName,
|
||||
}) {
|
||||
return _buildAITextMessage(blocContext, textMessage);
|
||||
},
|
||||
bubbleBuilder: (
|
||||
child, {
|
||||
required message,
|
||||
@@ -149,46 +155,7 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
final messageType = onetimeMessageTypeFromMeta(
|
||||
message.metadata,
|
||||
);
|
||||
if (messageType == OnetimeShotType.serverStreamError) {
|
||||
return ChatStreamingError(
|
||||
message: message,
|
||||
onRetryPressed: () {
|
||||
blocContext
|
||||
.read<ChatBloc>()
|
||||
.add(const ChatEvent.retryGenerate());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (messageType == OnetimeShotType.invalidSendMesssage) {
|
||||
return ChatInvalidUserMessage(
|
||||
message: message,
|
||||
);
|
||||
}
|
||||
|
||||
if (messageType == OnetimeShotType.relatedQuestion) {
|
||||
return RelatedQuestionList(
|
||||
onQuestionSelected: (question) {
|
||||
blocContext
|
||||
.read<ChatBloc>()
|
||||
.add(ChatEvent.sendMessage(question));
|
||||
blocContext
|
||||
.read<ChatBloc>()
|
||||
.add(const ChatEvent.clearReleatedQuestion());
|
||||
},
|
||||
chatId: widget.view.id,
|
||||
relatedQuestions: state.relatedQuestions,
|
||||
);
|
||||
}
|
||||
|
||||
return ChatAIMessageBubble(
|
||||
message: message,
|
||||
customMessageType: messageType,
|
||||
child: child,
|
||||
);
|
||||
return _buildAIBubble(message, blocContext, state, child);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -199,10 +166,67 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAITextMessage(BuildContext context, TextMessage message) {
|
||||
final isAuthor = message.author.id == _user.id;
|
||||
if (isAuthor) {
|
||||
return ChatTextMessageWidget(
|
||||
user: message.author,
|
||||
messageUserId: message.id,
|
||||
text: message.text,
|
||||
);
|
||||
} else {
|
||||
final stream = message.metadata?["$AnswerStream"];
|
||||
final questionId = message.metadata?["question"];
|
||||
return ChatAITextMessageWidget(
|
||||
user: message.author,
|
||||
messageUserId: message.id,
|
||||
text: stream is AnswerStream ? stream : message.text,
|
||||
key: ValueKey(message.id),
|
||||
questionId: questionId,
|
||||
chatId: widget.view.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAIBubble(
|
||||
Message message,
|
||||
BuildContext blocContext,
|
||||
ChatState state,
|
||||
Widget child,
|
||||
) {
|
||||
final messageType = onetimeMessageTypeFromMeta(
|
||||
message.metadata,
|
||||
);
|
||||
|
||||
if (messageType == OnetimeShotType.invalidSendMesssage) {
|
||||
return ChatInvalidUserMessage(
|
||||
message: message,
|
||||
);
|
||||
}
|
||||
|
||||
if (messageType == OnetimeShotType.relatedQuestion) {
|
||||
return RelatedQuestionList(
|
||||
onQuestionSelected: (question) {
|
||||
blocContext.read<ChatBloc>().add(ChatEvent.sendMessage(question));
|
||||
blocContext
|
||||
.read<ChatBloc>()
|
||||
.add(const ChatEvent.clearReleatedQuestion());
|
||||
},
|
||||
chatId: widget.view.id,
|
||||
relatedQuestions: state.relatedQuestions,
|
||||
);
|
||||
}
|
||||
|
||||
return ChatAIMessageBubble(
|
||||
message: message,
|
||||
customMessageType: messageType,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBubble(Message message, Widget child) {
|
||||
final isAuthor = message.author.id == _user.id;
|
||||
const borderRadius = BorderRadius.all(Radius.circular(6));
|
||||
|
||||
final childWithPadding = isAuthor
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
@@ -261,33 +285,25 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _customMessageBuilder(
|
||||
types.CustomMessage message, {
|
||||
required int messageWidth,
|
||||
}) {
|
||||
// iteration custom message type
|
||||
final messageType = onetimeMessageTypeFromMeta(message.metadata);
|
||||
if (messageType == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
switch (messageType) {
|
||||
case OnetimeShotType.loading:
|
||||
return const ChatAILoading();
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildChatInput(BuildContext context) {
|
||||
return ClipRect(
|
||||
child: Padding(
|
||||
padding: AIChatUILayout.safeAreaInsets(context),
|
||||
child: Column(
|
||||
children: [
|
||||
ChatInput(
|
||||
chatId: widget.view.id,
|
||||
onSendPressed: (message) => onSendPressed(context, message.text),
|
||||
BlocSelector<ChatBloc, ChatState, LoadingState>(
|
||||
selector: (state) => state.streamingStatus,
|
||||
builder: (context, state) {
|
||||
return ChatInput(
|
||||
chatId: widget.view.id,
|
||||
onSendPressed: (message) =>
|
||||
onSendPressed(context, message.text),
|
||||
isStreaming: state != const LoadingState.finish(),
|
||||
onStopStreaming: () {
|
||||
context.read<ChatBloc>().add(const ChatEvent.stopStream());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(6),
|
||||
Opacity(
|
||||
|
||||
+16
-26
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart';
|
||||
@@ -13,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -35,30 +33,22 @@ class ChatAIMessageBubble extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
const padding = EdgeInsets.symmetric(horizontal: _leftPadding);
|
||||
final childWithPadding = Padding(padding: padding, child: child);
|
||||
final widget = isMobile
|
||||
? _wrapPopMenu(childWithPadding)
|
||||
: _wrapHover(childWithPadding);
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => ChatAIMessageBloc(message: message),
|
||||
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
|
||||
builder: (context, state) {
|
||||
final widget = isMobile
|
||||
? _wrapPopMenu(childWithPadding)
|
||||
: _wrapHover(childWithPadding);
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ChatBorderedCircleAvatar(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.flowy_ai_chat_logo_s,
|
||||
size: Size.square(24),
|
||||
),
|
||||
),
|
||||
Expanded(child: widget),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ChatBorderedCircleAvatar(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.flowy_ai_chat_logo_s,
|
||||
size: Size.square(24),
|
||||
),
|
||||
),
|
||||
Expanded(child: widget),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,7 +108,7 @@ class _ChatAIMessageHoverState extends State<ChatAIMessageHover> {
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
@@ -18,13 +18,17 @@ class ChatInput extends StatefulWidget {
|
||||
required this.onSendPressed,
|
||||
required this.chatId,
|
||||
this.options = const InputOptions(),
|
||||
required this.isStreaming,
|
||||
required this.onStopStreaming,
|
||||
});
|
||||
|
||||
final bool? isAttachmentUploading;
|
||||
final VoidCallback? onAttachmentPressed;
|
||||
final void Function(types.PartialText) onSendPressed;
|
||||
final void Function() onStopStreaming;
|
||||
final InputOptions options;
|
||||
final String chatId;
|
||||
final bool isStreaming;
|
||||
|
||||
@override
|
||||
State<ChatInput> createState() => _ChatInputState();
|
||||
@@ -68,26 +72,23 @@ class _ChatInputState extends State<ChatInput> {
|
||||
|
||||
void _handleSendButtonVisibilityModeChange() {
|
||||
_textController.removeListener(_handleTextControllerChange);
|
||||
if (widget.options.sendButtonVisibilityMode ==
|
||||
SendButtonVisibilityMode.hidden) {
|
||||
_sendButtonVisible = false;
|
||||
} else if (widget.options.sendButtonVisibilityMode ==
|
||||
SendButtonVisibilityMode.editing) {
|
||||
_sendButtonVisible = _textController.text.trim() != '';
|
||||
_textController.addListener(_handleTextControllerChange);
|
||||
} else {
|
||||
_sendButtonVisible = true;
|
||||
}
|
||||
_sendButtonVisible =
|
||||
_textController.text.trim() != '' || widget.isStreaming;
|
||||
_textController.addListener(_handleTextControllerChange);
|
||||
}
|
||||
|
||||
void _handleSendPressed() {
|
||||
final trimmedText = _textController.text.trim();
|
||||
if (trimmedText != '') {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
if (widget.isStreaming) {
|
||||
widget.onStopStreaming();
|
||||
} else {
|
||||
final trimmedText = _textController.text.trim();
|
||||
if (trimmedText != '') {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
|
||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
||||
_textController.clear();
|
||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
||||
_textController.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +139,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
padding: textPadding,
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
readOnly: widget.isStreaming,
|
||||
focusNode: _inputFocusNode,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
@@ -153,7 +155,6 @@ class _ChatInputState extends State<ChatInput> {
|
||||
autocorrect: widget.options.autocorrect,
|
||||
autofocus: widget.options.autofocus,
|
||||
enableSuggestions: widget.options.enableSuggestions,
|
||||
spellCheckConfiguration: const SpellCheckConfiguration(),
|
||||
keyboardType: widget.options.keyboardType,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
maxLines: 10,
|
||||
@@ -173,8 +174,14 @@ class _ChatInputState extends State<ChatInput> {
|
||||
visible: _sendButtonVisible,
|
||||
child: Padding(
|
||||
padding: buttonPadding,
|
||||
child: SendButton(
|
||||
onPressed: _handleSendPressed,
|
||||
child: AccessoryButton(
|
||||
onSendPressed: () {
|
||||
_handleSendPressed();
|
||||
},
|
||||
onStopStreaming: () {
|
||||
widget.onStopStreaming();
|
||||
},
|
||||
isStreaming: widget.isStreaming,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -184,10 +191,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
@override
|
||||
void didUpdateWidget(covariant ChatInput oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.options.sendButtonVisibilityMode !=
|
||||
oldWidget.options.sendButtonVisibilityMode) {
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -211,7 +215,6 @@ class InputOptions {
|
||||
this.keyboardType = TextInputType.multiline,
|
||||
this.onTextChanged,
|
||||
this.onTextFieldTap,
|
||||
this.sendButtonVisibilityMode = SendButtonVisibilityMode.editing,
|
||||
this.textEditingController,
|
||||
this.autocorrect = true,
|
||||
this.autofocus = false,
|
||||
@@ -231,11 +234,6 @@ class InputOptions {
|
||||
/// Will be called on [TextField] tap.
|
||||
final VoidCallback? onTextFieldTap;
|
||||
|
||||
/// Controls the visibility behavior of the [SendButton] based on the
|
||||
/// [TextField] state inside the [ChatInput] widget.
|
||||
/// Defaults to [SendButtonVisibilityMode.editing].
|
||||
final SendButtonVisibilityMode sendButtonVisibilityMode;
|
||||
|
||||
/// Custom [TextEditingController]. If not provided, defaults to the
|
||||
/// [InputTextFieldController], which extends [TextEditingController] and has
|
||||
/// additional fatures like markdown support. If you want to keep additional
|
||||
@@ -260,24 +258,46 @@ class InputOptions {
|
||||
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
class SendButton extends StatelessWidget {
|
||||
const SendButton({required this.onPressed, super.key});
|
||||
class AccessoryButton extends StatelessWidget {
|
||||
const AccessoryButton({
|
||||
required this.onSendPressed,
|
||||
required this.onStopStreaming,
|
||||
required this.isStreaming,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final void Function() onPressed;
|
||||
final void Function() onSendPressed;
|
||||
final void Function() onStopStreaming;
|
||||
final bool isStreaming;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
width: 36,
|
||||
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
radius: BorderRadius.circular(18),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.send_s,
|
||||
size: const Size.square(24),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
if (isStreaming) {
|
||||
return FlowyIconButton(
|
||||
width: 36,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_stream_stop_s,
|
||||
size: const Size.square(28),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onStopStreaming,
|
||||
radius: BorderRadius.circular(18),
|
||||
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
);
|
||||
} else {
|
||||
return FlowyIconButton(
|
||||
width: 36,
|
||||
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
radius: BorderRadius.circular(18),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.send_s,
|
||||
size: const Size.square(24),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onSendPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,15 +53,15 @@ class ContentPlaceholder extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: 140,
|
||||
height: 16.0,
|
||||
margin: const EdgeInsets.only(bottom: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
),
|
||||
// Container(
|
||||
// width: 140,
|
||||
// height: 16.0,
|
||||
// margin: const EdgeInsets.only(bottom: 8.0),
|
||||
// decoration: BoxDecoration(
|
||||
// color: AFThemeExtension.of(context).lightGreyHover,
|
||||
// borderRadius: BorderRadius.circular(4.0),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,47 +1,11 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_related_question_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class RelatedQuestionPage extends StatefulWidget {
|
||||
const RelatedQuestionPage({
|
||||
required this.chatId,
|
||||
required this.onQuestionSelected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String chatId;
|
||||
final Function(String) onQuestionSelected;
|
||||
|
||||
@override
|
||||
State<RelatedQuestionPage> createState() => _RelatedQuestionPageState();
|
||||
}
|
||||
|
||||
class _RelatedQuestionPageState extends State<RelatedQuestionPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ChatRelatedMessageBloc(chatId: widget.chatId)
|
||||
..add(
|
||||
const ChatRelatedMessageEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<ChatRelatedMessageBloc, ChatRelatedMessageState>(
|
||||
builder: (blocContext, state) {
|
||||
return RelatedQuestionList(
|
||||
chatId: widget.chatId,
|
||||
onQuestionSelected: widget.onQuestionSelected,
|
||||
relatedQuestions: state.relatedQuestions,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RelatedQuestionList extends StatelessWidget {
|
||||
const RelatedQuestionList({
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class StreamTextField extends StatelessWidget {
|
||||
const StreamTextField({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
+78
-79
@@ -1,84 +1,83 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
// import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
// import 'package:easy_localization/easy_localization.dart';
|
||||
// import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
|
||||
class ChatStreamingError extends StatelessWidget {
|
||||
const ChatStreamingError({
|
||||
required this.message,
|
||||
required this.onRetryPressed,
|
||||
super.key,
|
||||
});
|
||||
// class ChatStreamingError extends StatelessWidget {
|
||||
// const ChatStreamingError({
|
||||
// required this.message,
|
||||
// required this.onRetryPressed,
|
||||
// super.key,
|
||||
// });
|
||||
|
||||
final void Function() onRetryPressed;
|
||||
final Message message;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canRetry = message.metadata?[canRetryKey] != null;
|
||||
// final void Function() onRetryPressed;
|
||||
// final Message message;
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final canRetry = message.metadata?[canRetryKey] != null;
|
||||
|
||||
if (canRetry) {
|
||||
return Column(
|
||||
children: [
|
||||
const Divider(height: 4, thickness: 1),
|
||||
const VSpace(16),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
_aiUnvaliable(),
|
||||
const VSpace(10),
|
||||
_retryButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Divider(height: 20, thickness: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_serverUnavailable.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// if (canRetry) {
|
||||
// return Column(
|
||||
// children: [
|
||||
// const Divider(height: 4, thickness: 1),
|
||||
// const VSpace(16),
|
||||
// Center(
|
||||
// child: Column(
|
||||
// children: [
|
||||
// _aiUnvaliable(),
|
||||
// const VSpace(10),
|
||||
// _retryButton(),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// } else {
|
||||
// return Center(
|
||||
// child: Column(
|
||||
// children: [
|
||||
// const Divider(height: 20, thickness: 1),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: FlowyText(
|
||||
// LocaleKeys.chat_serverUnavailable.tr(),
|
||||
// fontSize: 14,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
FlowyButton _retryButton() {
|
||||
return FlowyButton(
|
||||
radius: BorderRadius.circular(20),
|
||||
useIntrinsicWidth: true,
|
||||
text: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_regenerateAnswer.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
onTap: onRetryPressed,
|
||||
iconPadding: 0,
|
||||
leftIcon: const Icon(
|
||||
Icons.refresh,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
// FlowyButton _retryButton() {
|
||||
// return FlowyButton(
|
||||
// radius: BorderRadius.circular(20),
|
||||
// useIntrinsicWidth: true,
|
||||
// text: Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
// child: FlowyText(
|
||||
// LocaleKeys.chat_regenerateAnswer.tr(),
|
||||
// fontSize: 14,
|
||||
// ),
|
||||
// ),
|
||||
// onTap: onRetryPressed,
|
||||
// iconPadding: 0,
|
||||
// leftIcon: const Icon(
|
||||
// Icons.refresh,
|
||||
// size: 20,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
Padding _aiUnvaliable() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_aiServerUnavailable.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Padding _aiUnvaliable() {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: FlowyText(
|
||||
// LocaleKeys.chat_aiServerUnavailable.tr(),
|
||||
// fontSize: 14,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -20,29 +20,33 @@ class ChatWelcomePage extends StatelessWidget {
|
||||
];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.flowy_ai_chat_logo_s,
|
||||
size: Size.square(44),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: isMobile ? 2 : 4,
|
||||
crossAxisSpacing: 6,
|
||||
mainAxisSpacing: 6,
|
||||
childAspectRatio: 16.0 / 9.0,
|
||||
return AnimatedOpacity(
|
||||
opacity: 1.0,
|
||||
duration: const Duration(seconds: 3),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.flowy_ai_chat_logo_s,
|
||||
size: Size.square(44),
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => WelcomeQuestion(
|
||||
question: items[index],
|
||||
onSelected: onSelectedQuestion,
|
||||
const SizedBox(height: 40),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: isMobile ? 2 : 4,
|
||||
crossAxisSpacing: 6,
|
||||
mainAxisSpacing: 6,
|
||||
childAspectRatio: 16.0 / 9.0,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => WelcomeQuestion(
|
||||
question: items[index],
|
||||
onSelected: onSelectedQuestion,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:markdown_widget/markdown_widget.dart';
|
||||
|
||||
import 'selectable_highlight.dart';
|
||||
|
||||
class ChatAITextMessageWidget extends StatelessWidget {
|
||||
const ChatAITextMessageWidget({
|
||||
super.key,
|
||||
required this.user,
|
||||
required this.messageUserId,
|
||||
required this.text,
|
||||
required this.questionId,
|
||||
required this.chatId,
|
||||
});
|
||||
|
||||
final User user;
|
||||
final String messageUserId;
|
||||
final dynamic text;
|
||||
final Int64? questionId;
|
||||
final String chatId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ChatAIMessageBloc(
|
||||
message: text,
|
||||
chatId: chatId,
|
||||
questionId: questionId,
|
||||
)..add(const ChatAIMessageEvent.initial()),
|
||||
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
|
||||
builder: (context, state) {
|
||||
if (state.error != null) {
|
||||
return StreamingError(
|
||||
onRetryPressed: () {
|
||||
context.read<ChatAIMessageBloc>().add(
|
||||
const ChatAIMessageEvent.retry(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (state.retryState == const LoadingState.loading()) {
|
||||
return const ChatAILoading();
|
||||
}
|
||||
|
||||
if (state.text.isEmpty) {
|
||||
return const ChatAILoading();
|
||||
} else {
|
||||
return _textWidgetBuilder(user, context, state.text);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _textWidgetBuilder(
|
||||
User user,
|
||||
BuildContext context,
|
||||
String text,
|
||||
) {
|
||||
return MarkdownWidget(
|
||||
data: text,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
config: configFromContext(context),
|
||||
);
|
||||
}
|
||||
|
||||
MarkdownConfig configFromContext(BuildContext context) {
|
||||
return MarkdownConfig(
|
||||
configs: [
|
||||
HrConfig(color: AFThemeExtension.of(context).textColor),
|
||||
ChatH1Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
ChatH2Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
ChatH3Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
H4Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
H5Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
H6Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
PreConfig(
|
||||
builder: (code, language) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 800,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
|
||||
child: SelectableHighlightView(
|
||||
code,
|
||||
language: language,
|
||||
theme: getHightlineTheme(context),
|
||||
padding: const EdgeInsets.all(14),
|
||||
textStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
PConfig(
|
||||
textStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
CodeConfig(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
BlockquoteConfig(
|
||||
sideColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
textColor: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, TextStyle> getHightlineTheme(BuildContext context) {
|
||||
return {
|
||||
'root': TextStyle(
|
||||
color: const Color(0xffabb2bf),
|
||||
backgroundColor:
|
||||
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
|
||||
),
|
||||
'comment':
|
||||
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
|
||||
'quote':
|
||||
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
|
||||
'doctag': const TextStyle(color: Color(0xffc678dd)),
|
||||
'keyword': const TextStyle(color: Color(0xffc678dd)),
|
||||
'formula': const TextStyle(color: Color(0xffc678dd)),
|
||||
'section': const TextStyle(color: Color(0xffe06c75)),
|
||||
'name': const TextStyle(color: Color(0xffe06c75)),
|
||||
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
|
||||
'deletion': const TextStyle(color: Color(0xffe06c75)),
|
||||
'subst': const TextStyle(color: Color(0xffe06c75)),
|
||||
'literal': const TextStyle(color: Color(0xff56b6c2)),
|
||||
'string': const TextStyle(color: Color(0xff98c379)),
|
||||
'regexp': const TextStyle(color: Color(0xff98c379)),
|
||||
'addition': const TextStyle(color: Color(0xff98c379)),
|
||||
'attribute': const TextStyle(color: Color(0xff98c379)),
|
||||
'meta-string': const TextStyle(color: Color(0xff98c379)),
|
||||
'built_in': const TextStyle(color: Color(0xffe6c07b)),
|
||||
'attr': const TextStyle(color: Color(0xffd19a66)),
|
||||
'variable': const TextStyle(color: Color(0xffd19a66)),
|
||||
'template-variable': const TextStyle(color: Color(0xffd19a66)),
|
||||
'type': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-class': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
|
||||
'number': const TextStyle(color: Color(0xffd19a66)),
|
||||
'symbol': const TextStyle(color: Color(0xff61aeee)),
|
||||
'bullet': const TextStyle(color: Color(0xff61aeee)),
|
||||
'link': const TextStyle(color: Color(0xff61aeee)),
|
||||
'meta': const TextStyle(color: Color(0xff61aeee)),
|
||||
'selector-id': const TextStyle(color: Color(0xff61aeee)),
|
||||
'title': const TextStyle(color: Color(0xff61aeee)),
|
||||
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
|
||||
'strong': const TextStyle(fontWeight: FontWeight.bold),
|
||||
};
|
||||
}
|
||||
|
||||
class ChatH1Config extends HeadingConfig {
|
||||
const ChatH1Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 32,
|
||||
height: 40 / 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h1.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
|
||||
///config class for h2
|
||||
class ChatH2Config extends HeadingConfig {
|
||||
const ChatH2Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 24,
|
||||
height: 30 / 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h2.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
|
||||
class ChatH3Config extends HeadingConfig {
|
||||
const ChatH3Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 24,
|
||||
height: 30 / 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h3.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
|
||||
class StreamingError extends StatelessWidget {
|
||||
const StreamingError({
|
||||
required this.onRetryPressed,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final void Function() onRetryPressed;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const Divider(height: 4, thickness: 1),
|
||||
const VSpace(16),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
_aiUnvaliable(),
|
||||
const VSpace(10),
|
||||
_retryButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
FlowyButton _retryButton() {
|
||||
return FlowyButton(
|
||||
radius: BorderRadius.circular(20),
|
||||
useIntrinsicWidth: true,
|
||||
text: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_regenerateAnswer.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
onTap: onRetryPressed,
|
||||
iconPadding: 0,
|
||||
leftIcon: const Icon(
|
||||
Icons.refresh,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _aiUnvaliable() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_aiServerUnavailable.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:highlight/highlight.dart';
|
||||
|
||||
/// Highlight Flutter Widget
|
||||
class SelectableHighlightView extends StatelessWidget {
|
||||
SelectableHighlightView(
|
||||
String input, {
|
||||
super.key,
|
||||
this.language,
|
||||
this.theme = const {},
|
||||
this.padding,
|
||||
this.textStyle,
|
||||
int tabSize = 8,
|
||||
}) : source = input.replaceAll('\t', ' ' * tabSize);
|
||||
|
||||
/// The original code to be highlighted
|
||||
final String source;
|
||||
|
||||
/// Highlight language
|
||||
///
|
||||
/// It is recommended to give it a value for performance
|
||||
///
|
||||
/// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages)
|
||||
final String? language;
|
||||
|
||||
/// Highlight theme
|
||||
///
|
||||
/// [All available themes](https://github.com/pd4d10/highlight/blob/master/flutter_highlight/lib/themes)
|
||||
final Map<String, TextStyle> theme;
|
||||
|
||||
/// Padding
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// Text styles
|
||||
///
|
||||
/// Specify text styles such as font family and font size
|
||||
final TextStyle? textStyle;
|
||||
|
||||
List<TextSpan> _convert(List<Node> nodes) {
|
||||
final List<TextSpan> spans = [];
|
||||
var currentSpans = spans;
|
||||
final List<List<TextSpan>> stack = [];
|
||||
|
||||
// ignore: always_declare_return_types
|
||||
traverse(Node node) {
|
||||
if (node.value != null) {
|
||||
currentSpans.add(
|
||||
node.className == null
|
||||
? TextSpan(text: node.value)
|
||||
: TextSpan(text: node.value, style: theme[node.className!]),
|
||||
);
|
||||
} else if (node.children != null) {
|
||||
final List<TextSpan> tmp = [];
|
||||
currentSpans
|
||||
.add(TextSpan(children: tmp, style: theme[node.className!]));
|
||||
stack.add(currentSpans);
|
||||
currentSpans = tmp;
|
||||
|
||||
for (final n in node.children!) {
|
||||
traverse(n);
|
||||
if (n == node.children!.last) {
|
||||
currentSpans = stack.isEmpty ? spans : stack.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final node in nodes) {
|
||||
traverse(node);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
static const _rootKey = 'root';
|
||||
static const _defaultBackgroundColor = Color(0xffffffff);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor,
|
||||
padding: padding,
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: textStyle,
|
||||
children:
|
||||
_convert(highlight.parse(source, language: language).nodes!),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
|
||||
class ChatTextMessageWidget extends StatelessWidget {
|
||||
const ChatTextMessageWidget({
|
||||
super.key,
|
||||
required this.user,
|
||||
required this.messageUserId,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
final User user;
|
||||
final String messageUserId;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _textWidgetBuilder(user, context, text);
|
||||
}
|
||||
|
||||
Widget _textWidgetBuilder(
|
||||
User user,
|
||||
BuildContext context,
|
||||
String text,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextMessageText(
|
||||
text: text,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget to reuse the markdown capabilities, e.g., for previews.
|
||||
class TextMessageText extends StatelessWidget {
|
||||
const TextMessageText({
|
||||
super.key,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
/// Text that is shown as markdown.
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyText(
|
||||
text,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
lineHeight: 1.5,
|
||||
maxLines: null,
|
||||
selectable: true,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -29,7 +29,8 @@ class ChatUserMessageBubble extends StatelessWidget {
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest;
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => ChatUserMessageBloc(message: message),
|
||||
create: (context) => ChatUserMessageBloc(message: message)
|
||||
..add(const ChatUserMessageEvent.initial()),
|
||||
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:appflowy/util/time.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
|
||||
part 'time_cell_bloc.freezed.dart';
|
||||
|
||||
class TimeCellBloc extends Bloc<TimeCellEvent, TimeCellState> {
|
||||
TimeCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(TimeCellState.initial(cellController)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
}
|
||||
|
||||
final TimeCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(
|
||||
onCellChanged: _onCellChangedFn!,
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<TimeCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
content:
|
||||
content != null ? formatTime(content.time.toInt()) : "",
|
||||
),
|
||||
);
|
||||
},
|
||||
didUpdateField: (fieldInfo) {
|
||||
final wrap = fieldInfo.wrapCellContent;
|
||||
if (wrap != null) {
|
||||
emit(state.copyWith(wrap: wrap));
|
||||
}
|
||||
},
|
||||
updateCell: (text) async {
|
||||
text = parseTime(text)?.toString() ?? text;
|
||||
if (state.content != text) {
|
||||
emit(state.copyWith(content: text));
|
||||
await cellController.saveCellData(text);
|
||||
|
||||
// If the input content is "abc" that can't parsered as number
|
||||
// then the data stored in the backend will be an empty string.
|
||||
// So for every cell data that will be formatted in the backend.
|
||||
// It needs to get the formatted data after saving.
|
||||
add(
|
||||
TimeCellEvent.didReceiveCellUpdate(
|
||||
cellController.getCellData(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cellContent) {
|
||||
if (!isClosed) {
|
||||
add(TimeCellEvent.didReceiveCellUpdate(cellContent));
|
||||
}
|
||||
},
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFieldChangedListener(FieldInfo fieldInfo) {
|
||||
if (!isClosed) {
|
||||
add(TimeCellEvent.didUpdateField(fieldInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimeCellEvent with _$TimeCellEvent {
|
||||
const factory TimeCellEvent.didReceiveCellUpdate(TimeCellDataPB? cell) =
|
||||
_DidReceiveCellUpdate;
|
||||
const factory TimeCellEvent.didUpdateField(FieldInfo fieldInfo) =
|
||||
_DidUpdateField;
|
||||
const factory TimeCellEvent.updateCell(String text) = _UpdateCell;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimeCellState with _$TimeCellState {
|
||||
const factory TimeCellState({
|
||||
required String content,
|
||||
required bool wrap,
|
||||
}) = _TimeCellState;
|
||||
|
||||
factory TimeCellState.initial(TimeCellController cellController) {
|
||||
final wrap = cellController.fieldInfo.wrapCellContent;
|
||||
final cellData = cellController.getCellData();
|
||||
return TimeCellState(
|
||||
content: cellData != null ? formatTime(cellData.time.toInt()) : "",
|
||||
wrap: wrap ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'translate_cell_bloc.freezed.dart';
|
||||
|
||||
class TranslateCellBloc extends Bloc<TranslateCellEvent, TranslateCellState> {
|
||||
TranslateCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(TranslateCellState.initial(cellController)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
}
|
||||
|
||||
final TranslateCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(
|
||||
onCellChanged: _onCellChangedFn!,
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<TranslateCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(
|
||||
state.copyWith(content: cellData ?? ""),
|
||||
);
|
||||
},
|
||||
didUpdateField: (fieldInfo) {
|
||||
final wrap = fieldInfo.wrapCellContent;
|
||||
if (wrap != null) {
|
||||
emit(state.copyWith(wrap: wrap));
|
||||
}
|
||||
},
|
||||
updateCell: (text) async {
|
||||
if (state.content != text) {
|
||||
emit(state.copyWith(content: text));
|
||||
await cellController.saveCellData(text);
|
||||
|
||||
// If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
|
||||
// So for every cell data that will be formatted in the backend.
|
||||
// It needs to get the formatted data after saving.
|
||||
add(
|
||||
TranslateCellEvent.didReceiveCellUpdate(
|
||||
cellController.getCellData() ?? "",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cellContent) {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
TranslateCellEvent.didReceiveCellUpdate(cellContent ?? ""),
|
||||
);
|
||||
}
|
||||
},
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFieldChangedListener(FieldInfo fieldInfo) {
|
||||
if (!isClosed) {
|
||||
add(TranslateCellEvent.didUpdateField(fieldInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TranslateCellEvent with _$TranslateCellEvent {
|
||||
const factory TranslateCellEvent.didReceiveCellUpdate(String? cellContent) =
|
||||
_DidReceiveCellUpdate;
|
||||
const factory TranslateCellEvent.didUpdateField(FieldInfo fieldInfo) =
|
||||
_DidUpdateField;
|
||||
const factory TranslateCellEvent.updateCell(String text) = _UpdateCell;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TranslateCellState with _$TranslateCellState {
|
||||
const factory TranslateCellState({
|
||||
required String content,
|
||||
required bool wrap,
|
||||
}) = _TranslateCellState;
|
||||
|
||||
factory TranslateCellState.initial(TranslateCellController cellController) {
|
||||
final wrap = cellController.fieldInfo.wrapCellContent;
|
||||
return TranslateCellState(
|
||||
content: cellController.getCellData() ?? "",
|
||||
wrap: wrap ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'translate_row_bloc.freezed.dart';
|
||||
|
||||
class TranslateRowBloc extends Bloc<TranslateRowEvent, TranslateRowState> {
|
||||
TranslateRowBloc({
|
||||
required this.viewId,
|
||||
required this.rowId,
|
||||
required this.fieldId,
|
||||
}) : super(TranslateRowState.initial()) {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final String viewId;
|
||||
final String rowId;
|
||||
final String fieldId;
|
||||
|
||||
void _dispatch() {
|
||||
on<TranslateRowEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
startTranslate: () {
|
||||
final params = TranslateRowPB(
|
||||
viewId: viewId,
|
||||
rowId: rowId,
|
||||
fieldId: fieldId,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: const LoadingState.loading(),
|
||||
error: null,
|
||||
),
|
||||
);
|
||||
|
||||
DatabaseEventTranslateRow(params).send().then(
|
||||
(result) => {
|
||||
if (!isClosed)
|
||||
add(TranslateRowEvent.finishTranslate(result)),
|
||||
},
|
||||
);
|
||||
},
|
||||
finishTranslate: (result) {
|
||||
result.fold(
|
||||
(s) => {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: const LoadingState.finish(),
|
||||
error: null,
|
||||
),
|
||||
),
|
||||
},
|
||||
(err) => {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: const LoadingState.finish(),
|
||||
error: err,
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TranslateRowEvent with _$TranslateRowEvent {
|
||||
const factory TranslateRowEvent.startTranslate() = _DidStartTranslate;
|
||||
const factory TranslateRowEvent.finishTranslate(
|
||||
FlowyResult<void, FlowyError> result,
|
||||
) = _DidFinishTranslate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TranslateRowState with _$TranslateRowState {
|
||||
const factory TranslateRowState({
|
||||
required LoadingState loadingState,
|
||||
required FlowyError? error,
|
||||
}) = _TranslateRowState;
|
||||
|
||||
factory TranslateRowState.initial() {
|
||||
return const TranslateRowState(
|
||||
loadingState: LoadingState.finish(),
|
||||
error: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
const factory LoadingState.loading() = _Loading;
|
||||
const factory LoadingState.finish() = _Finish;
|
||||
}
|
||||
+26
-1
@@ -16,6 +16,8 @@ typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
|
||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||
typedef RelationCellController = CellController<RelationCellDataPB, String>;
|
||||
typedef SummaryCellController = CellController<String, String>;
|
||||
typedef TimeCellController = CellController<TimeCellDataPB, String>;
|
||||
typedef TranslateCellController = CellController<String, String>;
|
||||
|
||||
CellController makeCellController(
|
||||
DatabaseController databaseController,
|
||||
@@ -120,7 +122,6 @@ CellController makeCellController(
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
|
||||
case FieldType.Relation:
|
||||
return RelationCellController(
|
||||
viewId: viewId,
|
||||
@@ -145,6 +146,30 @@ CellController makeCellController(
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
case FieldType.Time:
|
||||
return TimeCellController(
|
||||
viewId: viewId,
|
||||
fieldController: fieldController,
|
||||
cellContext: cellContext,
|
||||
rowCache: rowCache,
|
||||
cellDataLoader: CellDataLoader(
|
||||
parser: TimeCellDataParser(),
|
||||
reloadOnFieldChange: true,
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
case FieldType.Translate:
|
||||
return TranslateCellController(
|
||||
viewId: viewId,
|
||||
fieldController: fieldController,
|
||||
cellContext: cellContext,
|
||||
rowCache: rowCache,
|
||||
cellDataLoader: CellDataLoader(
|
||||
parser: StringCellDataParser(),
|
||||
reloadOnFieldChange: true,
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
@@ -181,3 +181,18 @@ class RelationCellDataParser implements CellDataParser<RelationCellDataPB> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TimeCellDataParser implements CellDataParser<TimeCellDataPB> {
|
||||
@override
|
||||
TimeCellDataPB? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return TimeCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse timer data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,8 @@ class FieldController {
|
||||
List<FieldInfo> get fieldInfos => [..._fieldNotifier.fieldInfos];
|
||||
List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
|
||||
List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
|
||||
List<GroupSettingPB> get groupSettings =>
|
||||
_groupConfigurationByFieldId.entries.map((e) => e.value).toList();
|
||||
|
||||
FieldInfo? getField(String fieldId) {
|
||||
return _fieldNotifier.fieldInfos
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'field_info.freezed.dart';
|
||||
|
||||
@@ -63,6 +64,7 @@ class FieldInfo with _$FieldInfo {
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.Checklist:
|
||||
case FieldType.URL:
|
||||
case FieldType.Time:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -84,9 +86,19 @@ class FieldInfo with _$FieldInfo {
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
case FieldType.Checklist:
|
||||
case FieldType.Time:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<ProtobufEnum> get groupConditions {
|
||||
switch (field.fieldType) {
|
||||
case FieldType.DateTime:
|
||||
return DateConditionPB.values;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/translate_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'translate_type_option_bloc.freezed.dart';
|
||||
|
||||
class TranslateTypeOptionBloc
|
||||
extends Bloc<TranslateTypeOptionEvent, TranslateTypeOptionState> {
|
||||
TranslateTypeOptionBloc({required TranslateTypeOptionPB option})
|
||||
: super(TranslateTypeOptionState.initial(option)) {
|
||||
on<TranslateTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
selectLanguage: (languageType) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
option: _updateLanguage(languageType),
|
||||
language: languageTypeToLanguage(languageType),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TranslateTypeOptionPB _updateLanguage(TranslateLanguagePB languageType) {
|
||||
state.option.freeze();
|
||||
return state.option.rebuild((option) {
|
||||
option.language = languageType;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TranslateTypeOptionEvent with _$TranslateTypeOptionEvent {
|
||||
const factory TranslateTypeOptionEvent.selectLanguage(
|
||||
TranslateLanguagePB languageType,
|
||||
) = _SelectLanguage;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TranslateTypeOptionState with _$TranslateTypeOptionState {
|
||||
const factory TranslateTypeOptionState({
|
||||
required TranslateTypeOptionPB option,
|
||||
required String language,
|
||||
}) = _TranslateTypeOptionState;
|
||||
|
||||
factory TranslateTypeOptionState.initial(TranslateTypeOptionPB option) =>
|
||||
TranslateTypeOptionState(
|
||||
option: option,
|
||||
language: languageTypeToLanguage(option.language),
|
||||
);
|
||||
}
|
||||
|
||||
String languageTypeToLanguage(TranslateLanguagePB langaugeType) {
|
||||
switch (langaugeType) {
|
||||
case TranslateLanguagePB.SimplifiedChinese:
|
||||
return 'Simplified Chinese';
|
||||
case TranslateLanguagePB.TraditionalChinese:
|
||||
return 'Traditional Chinese';
|
||||
case TranslateLanguagePB.English:
|
||||
return 'English';
|
||||
case TranslateLanguagePB.French:
|
||||
return 'French';
|
||||
case TranslateLanguagePB.German:
|
||||
return 'German';
|
||||
case TranslateLanguagePB.Spanish:
|
||||
return 'Spanish';
|
||||
case TranslateLanguagePB.Hindi:
|
||||
return 'Hindi';
|
||||
case TranslateLanguagePB.Portuguese:
|
||||
return 'Portuguese';
|
||||
case TranslateLanguagePB.StandardArabic:
|
||||
return 'Standard Arabic';
|
||||
default:
|
||||
Log.error('Unknown language type: $langaugeType');
|
||||
return 'English';
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -4,8 +4,6 @@ abstract class TypeOptionParser<T> {
|
||||
T fromBuffer(List<int> buffer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
|
||||
@override
|
||||
NumberTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
@@ -51,3 +49,11 @@ class RelationTypeOptionDataParser
|
||||
return RelationTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class TranslateTypeOptionDataParser
|
||||
extends TypeOptionParser<TranslateTypeOptionPB> {
|
||||
@override
|
||||
TranslateTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return TranslateTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database/domain/group_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@@ -22,6 +23,7 @@ class DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {
|
||||
viewId,
|
||||
databaseController.fieldController.fieldInfos,
|
||||
databaseController.databaseLayoutSetting!.board,
|
||||
databaseController.fieldController.groupSettings,
|
||||
),
|
||||
) {
|
||||
_dispatch();
|
||||
@@ -47,15 +49,24 @@ class DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {
|
||||
on<DatabaseGroupEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
initial: () async => _startListening(),
|
||||
didReceiveFieldUpdate: (fieldInfos) {
|
||||
emit(state.copyWith(fieldInfos: fieldInfos));
|
||||
emit(
|
||||
state.copyWith(
|
||||
fieldInfos: fieldInfos,
|
||||
groupSettings:
|
||||
_databaseController.fieldController.groupSettings,
|
||||
),
|
||||
);
|
||||
},
|
||||
setGroupByField: (String fieldId, FieldType fieldType) async {
|
||||
setGroupByField: (
|
||||
String fieldId,
|
||||
FieldType fieldType, [
|
||||
List<int>? settingContent,
|
||||
]) async {
|
||||
final result = await _groupBackendSvc.groupByField(
|
||||
fieldId: fieldId,
|
||||
settingContent: settingContent ?? [],
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
},
|
||||
@@ -96,8 +107,9 @@ class DatabaseGroupEvent with _$DatabaseGroupEvent {
|
||||
const factory DatabaseGroupEvent.initial() = _Initial;
|
||||
const factory DatabaseGroupEvent.setGroupByField(
|
||||
String fieldId,
|
||||
FieldType fieldType,
|
||||
) = _DatabaseGroupEvent;
|
||||
FieldType fieldType, [
|
||||
@Default([]) List<int> settingContent,
|
||||
]) = _DatabaseGroupEvent;
|
||||
const factory DatabaseGroupEvent.didReceiveFieldUpdate(
|
||||
List<FieldInfo> fields,
|
||||
) = _DidReceiveFieldUpdate;
|
||||
@@ -112,16 +124,19 @@ class DatabaseGroupState with _$DatabaseGroupState {
|
||||
required String viewId,
|
||||
required List<FieldInfo> fieldInfos,
|
||||
required BoardLayoutSettingPB layoutSettings,
|
||||
required List<GroupSettingPB> groupSettings,
|
||||
}) = _DatabaseGroupState;
|
||||
|
||||
factory DatabaseGroupState.initial(
|
||||
String viewId,
|
||||
List<FieldInfo> fieldInfos,
|
||||
BoardLayoutSettingPB layoutSettings,
|
||||
List<GroupSettingPB> groupSettings,
|
||||
) =>
|
||||
DatabaseGroupState(
|
||||
viewId: viewId,
|
||||
fieldInfos: fieldInfos,
|
||||
layoutSettings: layoutSettings,
|
||||
groupSettings: groupSettings,
|
||||
);
|
||||
}
|
||||
@@ -16,8 +16,10 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
|
||||
import '../../application/database_controller.dart';
|
||||
import '../../application/field/field_controller.dart';
|
||||
@@ -527,6 +529,9 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
return "No ${field.name}";
|
||||
}
|
||||
|
||||
final groupSettings = databaseController.fieldController.groupSettings
|
||||
.firstWhereOrNull((gs) => gs.fieldId == field.id);
|
||||
|
||||
switch (field.fieldType) {
|
||||
case FieldType.SingleSelect:
|
||||
final options =
|
||||
@@ -547,33 +552,61 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
case FieldType.URL:
|
||||
return group.groupId;
|
||||
case FieldType.DateTime:
|
||||
// Assume DateCondition::Relative as there isn't an option for this
|
||||
// right now.
|
||||
final config = groupSettings?.content != null
|
||||
? DateGroupConfigurationPB.fromBuffer(groupSettings!.content)
|
||||
: DateGroupConfigurationPB();
|
||||
final dateFormat = DateFormat("y/MM/dd");
|
||||
try {
|
||||
final targetDateTime = dateFormat.parseLoose(group.groupId);
|
||||
final targetDateTimeDay = DateTime(
|
||||
targetDateTime.year,
|
||||
targetDateTime.month,
|
||||
targetDateTime.day,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final nowDay = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
);
|
||||
final diff = targetDateTimeDay.difference(nowDay).inDays;
|
||||
return switch (diff) {
|
||||
0 => "Today",
|
||||
-1 => "Yesterday",
|
||||
1 => "Tomorrow",
|
||||
-7 => "Last 7 days",
|
||||
2 => "Next 7 days",
|
||||
-30 => "Last 30 days",
|
||||
8 => "Next 30 days",
|
||||
_ => DateFormat("MMM y").format(targetDateTimeDay)
|
||||
};
|
||||
switch (config.condition) {
|
||||
case DateConditionPB.Day:
|
||||
return DateFormat("MMM dd, y").format(targetDateTime);
|
||||
case DateConditionPB.Week:
|
||||
final beginningOfWeek = targetDateTime
|
||||
.subtract(Duration(days: targetDateTime.weekday - 1));
|
||||
final endOfWeek = targetDateTime.add(
|
||||
Duration(days: DateTime.daysPerWeek - targetDateTime.weekday),
|
||||
);
|
||||
|
||||
final beginningOfWeekFormat =
|
||||
beginningOfWeek.year != endOfWeek.year
|
||||
? "MMM dd y"
|
||||
: "MMM dd";
|
||||
final endOfWeekFormat = beginningOfWeek.month != endOfWeek.month
|
||||
? "MMM dd y"
|
||||
: "dd y";
|
||||
|
||||
return LocaleKeys.board_dateCondition_weekOf.tr(
|
||||
args: [
|
||||
DateFormat(beginningOfWeekFormat).format(beginningOfWeek),
|
||||
DateFormat(endOfWeekFormat).format(endOfWeek),
|
||||
],
|
||||
);
|
||||
case DateConditionPB.Month:
|
||||
return DateFormat("MMM y").format(targetDateTime);
|
||||
case DateConditionPB.Year:
|
||||
return DateFormat("y").format(targetDateTime);
|
||||
case DateConditionPB.Relative:
|
||||
final targetDateTimeDay = DateTime(
|
||||
targetDateTime.year,
|
||||
targetDateTime.month,
|
||||
targetDateTime.day,
|
||||
);
|
||||
final nowDay = DateTime.now().withoutTime;
|
||||
final diff = targetDateTimeDay.difference(nowDay).inDays;
|
||||
return switch (diff) {
|
||||
0 => LocaleKeys.board_dateCondition_today.tr(),
|
||||
-1 => LocaleKeys.board_dateCondition_yesterday.tr(),
|
||||
1 => LocaleKeys.board_dateCondition_tomorrow.tr(),
|
||||
-7 => LocaleKeys.board_dateCondition_lastSevenDays.tr(),
|
||||
2 => LocaleKeys.board_dateCondition_nextSevenDays.tr(),
|
||||
-30 => LocaleKeys.board_dateCondition_lastThirtyDays.tr(),
|
||||
8 => LocaleKeys.board_dateCondition_nextThirtyDays.tr(),
|
||||
_ => DateFormat("MMM y").format(targetDateTimeDay)
|
||||
};
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
} on FormatException {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/shared/callback_shortcuts.dart';
|
||||
import 'package:appflowy/shared/conditional_listenable_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -29,7 +28,6 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart' hide Card;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../widgets/card/card.dart';
|
||||
import '../../widgets/cell/card_cell_builder.dart';
|
||||
@@ -322,67 +320,64 @@ class _BoardContentState extends State<_BoardContent> {
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Provider(
|
||||
create: (context) => AFCallbackShortcutsProvider(),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: BoardShortcutContainer(
|
||||
focusScope: widget.focusScope,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 256),
|
||||
config: config,
|
||||
leading: HiddenGroupsColumn(margin: config.groupHeaderPadding),
|
||||
trailing: context
|
||||
.read<BoardBloc>()
|
||||
.groupingFieldType
|
||||
?.canCreateNewGroup ??
|
||||
false
|
||||
? BoardTrailing(scrollController: scrollController)
|
||||
: const HSpace(40),
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: BoardColumnHeader(
|
||||
groupData: groupData,
|
||||
margin: config.groupHeaderPadding,
|
||||
),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: BoardShortcutContainer(
|
||||
focusScope: widget.focusScope,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 256),
|
||||
config: config,
|
||||
leading: HiddenGroupsColumn(margin: config.groupHeaderPadding),
|
||||
trailing: context
|
||||
.read<BoardBloc>()
|
||||
.groupingFieldType
|
||||
?.canCreateNewGroup ??
|
||||
false
|
||||
? BoardTrailing(scrollController: scrollController)
|
||||
: const HSpace(40),
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: BoardColumnHeader(
|
||||
groupData: groupData,
|
||||
margin: config.groupHeaderPadding,
|
||||
),
|
||||
footerBuilder: (_, groupData) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardActionsCubit>(),
|
||||
),
|
||||
],
|
||||
child: BoardColumnFooter(
|
||||
columnData: groupData,
|
||||
boardConfig: config,
|
||||
scrollManager: scrollManager,
|
||||
),
|
||||
footerBuilder: (_, groupData) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardActionsCubit>(),
|
||||
),
|
||||
],
|
||||
child: BoardColumnFooter(
|
||||
columnData: groupData,
|
||||
boardConfig: config,
|
||||
scrollManager: scrollManager,
|
||||
),
|
||||
cardBuilder: (_, column, columnItem) => MultiBlocProvider(
|
||||
key: ValueKey("board_card_${column.id}_${columnItem.id}"),
|
||||
providers: [
|
||||
BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardActionsCubit>(),
|
||||
),
|
||||
],
|
||||
child: _BoardCard(
|
||||
afGroupData: column,
|
||||
groupItem: columnItem as GroupItem,
|
||||
boardConfig: config,
|
||||
notifier: widget.focusScope,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
cardBuilder: (_, column, columnItem) => MultiBlocProvider(
|
||||
key: ValueKey("board_card_${column.id}_${columnItem.id}"),
|
||||
providers: [
|
||||
BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardActionsCubit>(),
|
||||
),
|
||||
],
|
||||
child: _BoardCard(
|
||||
afGroupData: column,
|
||||
groupItem: columnItem as GroupItem,
|
||||
boardConfig: config,
|
||||
notifier: widget.focusScope,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -469,7 +464,7 @@ class _BoardColumnFooterState extends State<BoardColumnFooter> {
|
||||
const SingleActivator(LogicalKeyboardKey.keyE): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.keyN): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.delete): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.backspace): nada,
|
||||
// const SingleActivator(LogicalKeyboardKey.backspace): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.enter): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.comma): nada,
|
||||
@@ -615,7 +610,7 @@ class _BoardCardState extends State<_BoardCard> {
|
||||
const SingleActivator(LogicalKeyboardKey.keyE): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.keyN): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.delete): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.backspace): nada,
|
||||
// const SingleActivator(LogicalKeyboardKey.backspace): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.enter): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.comma): nada,
|
||||
|
||||
-8
@@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/shared/callback_shortcuts.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
@@ -32,7 +31,6 @@ class BoardColumnHeader extends StatefulWidget {
|
||||
class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
final FocusNode _keyboardListenerFocusNode = FocusNode();
|
||||
late final AFCallbackShortcutsProvider _shortcutsProvider;
|
||||
|
||||
late final TextEditingController _controller =
|
||||
TextEditingController.fromValue(
|
||||
@@ -47,21 +45,15 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_shortcutsProvider = context.read<AFCallbackShortcutsProvider>();
|
||||
_focusNode.addListener(() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
_saveEdit();
|
||||
}
|
||||
});
|
||||
_keyboardListenerFocusNode.addListener(() {
|
||||
_shortcutsProvider.isShortcutsEnabled.value =
|
||||
!_keyboardListenerFocusNode.hasFocus;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shortcutsProvider.isShortcutsEnabled.value = true;
|
||||
_focusNode.dispose();
|
||||
_keyboardListenerFocusNode.dispose();
|
||||
_controller.dispose();
|
||||
|
||||
+25
-17
@@ -39,14 +39,14 @@ class BoardFocusScope extends ChangeNotifier
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void focusNext() {
|
||||
bool focusNext() {
|
||||
_deepCopy();
|
||||
|
||||
// if no card is focused, focus on the first card in the board
|
||||
if (_focusedCards.isEmpty) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
final lastFocusedCard = _focusedCards.last;
|
||||
@@ -58,7 +58,7 @@ class BoardFocusScope extends ChangeNotifier
|
||||
if (iterable == null || iterable.isEmpty) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (iterable.length == 1) {
|
||||
@@ -90,16 +90,18 @@ class BoardFocusScope extends ChangeNotifier
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void focusPrevious() {
|
||||
bool focusPrevious() {
|
||||
_deepCopy();
|
||||
|
||||
// if no card is focused, focus on the last card in the board
|
||||
if (_focusedCards.isEmpty) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
final lastFocusedCard = _focusedCards.last;
|
||||
@@ -111,7 +113,7 @@ class BoardFocusScope extends ChangeNotifier
|
||||
if (iterable == null || iterable.isEmpty) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (iterable.length == 1) {
|
||||
@@ -143,16 +145,18 @@ class BoardFocusScope extends ChangeNotifier
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void adjustRangeDown() {
|
||||
bool adjustRangeDown() {
|
||||
_deepCopy();
|
||||
|
||||
// if no card is focused, focus on the first card in the board
|
||||
if (_focusedCards.isEmpty) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
final firstFocusedCard = _focusedCards.first;
|
||||
@@ -171,7 +175,7 @@ class BoardFocusScope extends ChangeNotifier
|
||||
if (firstGroupIndex == -1 || lastGroupIndex == -1) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstGroupIndex < lastGroupIndex) {
|
||||
@@ -189,7 +193,7 @@ class BoardFocusScope extends ChangeNotifier
|
||||
if (firstCardIndex == -1 || lastCardIndex == -1) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
isExpand = firstCardIndex < lastCardIndex;
|
||||
@@ -203,7 +207,7 @@ class BoardFocusScope extends ChangeNotifier
|
||||
if (groupController == null) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
final iterable = groupController.items
|
||||
@@ -236,16 +240,17 @@ class BoardFocusScope extends ChangeNotifier
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
void adjustRangeUp() {
|
||||
bool adjustRangeUp() {
|
||||
_deepCopy();
|
||||
|
||||
// if no card is focused, focus on the first card in the board
|
||||
if (_focusedCards.isEmpty) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
final firstFocusedCard = _focusedCards.first;
|
||||
@@ -264,7 +269,7 @@ class BoardFocusScope extends ChangeNotifier
|
||||
if (firstGroupIndex == -1 || lastGroupIndex == -1) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstGroupIndex < lastGroupIndex) {
|
||||
@@ -282,7 +287,7 @@ class BoardFocusScope extends ChangeNotifier
|
||||
if (firstCardIndex == -1 || lastCardIndex == -1) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
isExpand = firstCardIndex > lastCardIndex;
|
||||
@@ -296,7 +301,7 @@ class BoardFocusScope extends ChangeNotifier
|
||||
if (groupController == null) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
final iterable = groupController.items.reversed
|
||||
@@ -329,12 +334,15 @@ class BoardFocusScope extends ChangeNotifier
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
bool clear() {
|
||||
_deepCopy();
|
||||
_focusedCards.clear();
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
void _focusFirstCard() {
|
||||
|
||||
+91
-63
@@ -1,8 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/shared/callback_shortcuts.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -22,56 +25,7 @@ class BoardShortcutContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AFCallbackShortcuts(
|
||||
canAcceptEvent: (_, __) =>
|
||||
context.read<AFCallbackShortcutsProvider>().isShortcutsEnabled.value,
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp):
|
||||
focusScope.focusPrevious,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown):
|
||||
focusScope.focusNext,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true):
|
||||
focusScope.adjustRangeUp,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true):
|
||||
focusScope.adjustRangeDown,
|
||||
const SingleActivator(LogicalKeyboardKey.escape): focusScope.clear,
|
||||
const SingleActivator(LogicalKeyboardKey.keyE): () {
|
||||
if (focusScope.value.length != 1) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.startEditingRow(focusScope.value.first);
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.keyN): () {
|
||||
if (focusScope.value.length != 1) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.startCreateBottomRow(focusScope.value.first.groupId);
|
||||
focusScope.clear();
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.delete): () =>
|
||||
_removeHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.backspace): () =>
|
||||
_removeHandler(context),
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
shift: true,
|
||||
meta: Platform.isMacOS,
|
||||
control: !Platform.isMacOS,
|
||||
): () => _shiftCmdUpHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.enter): () =>
|
||||
_enterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.numpadEnter): () =>
|
||||
_enterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.enter, shift: true): () =>
|
||||
_shitEnterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.comma): () =>
|
||||
_moveGroupToAdjacentGroup(context, true),
|
||||
const SingleActivator(LogicalKeyboardKey.period): () =>
|
||||
_moveGroupToAdjacentGroup(context, false),
|
||||
},
|
||||
bindings: _shortcutBindings(context),
|
||||
child: FocusScope(
|
||||
child: Focus(
|
||||
child: Builder(
|
||||
@@ -92,16 +46,75 @@ class BoardShortcutContainer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _enterHandler(BuildContext context) {
|
||||
Map<ShortcutActivator, AFBindingCallback> _shortcutBindings(
|
||||
BuildContext context,
|
||||
) {
|
||||
return {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp):
|
||||
focusScope.focusPrevious,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown): focusScope.focusNext,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true):
|
||||
focusScope.adjustRangeUp,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true):
|
||||
focusScope.adjustRangeDown,
|
||||
const SingleActivator(LogicalKeyboardKey.escape): focusScope.clear,
|
||||
const SingleActivator(LogicalKeyboardKey.delete): () =>
|
||||
_removeHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.backspace): () =>
|
||||
_removeHandler(context),
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
shift: true,
|
||||
meta: Platform.isMacOS,
|
||||
control: !Platform.isMacOS,
|
||||
): () => _shiftCmdUpHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.enter): () =>
|
||||
_enterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.numpadEnter): () =>
|
||||
_enterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.enter, shift: true): () =>
|
||||
_shiftEnterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.comma): () =>
|
||||
_moveGroupToAdjacentGroup(context, true),
|
||||
const SingleActivator(LogicalKeyboardKey.period): () =>
|
||||
_moveGroupToAdjacentGroup(context, false),
|
||||
const SingleActivator(LogicalKeyboardKey.keyE): () =>
|
||||
_keyEHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.keyN): () =>
|
||||
_keyNHandler(context),
|
||||
};
|
||||
}
|
||||
|
||||
bool _keyEHandler(BuildContext context) {
|
||||
if (focusScope.value.length != 1) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
context.read<BoardActionsCubit>().startEditingRow(focusScope.value.first);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _keyNHandler(BuildContext context) {
|
||||
if (focusScope.value.length != 1) {
|
||||
return false;
|
||||
}
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.startCreateBottomRow(focusScope.value.first.groupId);
|
||||
focusScope.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _enterHandler(BuildContext context) {
|
||||
if (focusScope.value.length != 1) {
|
||||
return false;
|
||||
}
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.openCardWithRowId(focusScope.value.first.rowId);
|
||||
return true;
|
||||
}
|
||||
|
||||
void _shitEnterHandler(BuildContext context) {
|
||||
bool _shiftEnterHandler(BuildContext context) {
|
||||
if (focusScope.value.isEmpty) {
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
@@ -111,10 +124,13 @@ class BoardShortcutContainer extends StatelessWidget {
|
||||
focusScope.value.first,
|
||||
CreateBoardCardRelativePosition.after,
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _shiftCmdUpHandler(BuildContext context) {
|
||||
bool _shiftCmdUpHandler(BuildContext context) {
|
||||
if (focusScope.value.isEmpty) {
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
@@ -124,19 +140,30 @@ class BoardShortcutContainer extends StatelessWidget {
|
||||
focusScope.value.first,
|
||||
CreateBoardCardRelativePosition.before,
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _removeHandler(BuildContext context) {
|
||||
if (focusScope.value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));
|
||||
}
|
||||
|
||||
void _moveGroupToAdjacentGroup(BuildContext context, bool toPrevious) {
|
||||
bool _removeHandler(BuildContext context) {
|
||||
if (focusScope.value.length != 1) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
NavigatorOkCancelDialog(
|
||||
message: LocaleKeys.grid_row_deleteCardPrompt.tr(),
|
||||
onOkPressed: () {
|
||||
context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));
|
||||
},
|
||||
).show(context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _moveGroupToAdjacentGroup(BuildContext context, bool toPrevious) {
|
||||
if (focusScope.value.length != 1) {
|
||||
return false;
|
||||
}
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.moveGroupToAdjacentGroup(
|
||||
@@ -145,5 +172,6 @@ class BoardShortcutContainer extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
focusScope.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
@@ -5,12 +7,10 @@ import 'package:appflowy/plugins/database/application/setting/property_bloc.dart
|
||||
import 'package:appflowy/plugins/database/calendar/application/calendar_setting_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Widget that displays a list of settings that alters the appearance of the
|
||||
@@ -325,7 +325,6 @@ Widget _toggleItem({
|
||||
Toggle(
|
||||
value: value,
|
||||
onChanged: (value) => onToggle(!value),
|
||||
style: ToggleStyle.big,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -202,6 +202,30 @@ class FilterBackendService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> insertTimeFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required NumberFilterConditionPB condition,
|
||||
String content = "",
|
||||
}) {
|
||||
final filter = TimeFilterPB()
|
||||
..condition = condition
|
||||
..content = content;
|
||||
|
||||
return filterId == null
|
||||
? insertFilter(
|
||||
fieldId: fieldId,
|
||||
fieldType: FieldType.Time,
|
||||
data: filter.writeToBuffer(),
|
||||
)
|
||||
: updateFilter(
|
||||
filterId: filterId,
|
||||
fieldId: fieldId,
|
||||
fieldType: FieldType.Time,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> insertFilter({
|
||||
required String fieldId,
|
||||
required FieldType fieldType,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
@@ -10,10 +10,12 @@ class GroupBackendService {
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> groupByField({
|
||||
required String fieldId,
|
||||
required List<int> settingContent,
|
||||
}) {
|
||||
final payload = GroupByFieldPayloadPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId;
|
||||
..fieldId = fieldId
|
||||
..settingContent = settingContent;
|
||||
|
||||
return DatabaseEventSetGroupByField(payload).send();
|
||||
}
|
||||
|
||||
+5
@@ -127,6 +127,11 @@ class GridCreateFilterBloc
|
||||
fieldId: fieldId,
|
||||
condition: NumberFilterConditionPB.Equal,
|
||||
);
|
||||
case FieldType.Time:
|
||||
return _filterBackendSvc.insertTimeFilter(
|
||||
fieldId: fieldId,
|
||||
condition: NumberFilterConditionPB.Equal,
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return _filterBackendSvc.insertTextFilter(
|
||||
fieldId: fieldId,
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/domain/filter_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/filter_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'time_filter_editor_bloc.freezed.dart';
|
||||
|
||||
class TimeFilterEditorBloc
|
||||
extends Bloc<TimeFilterEditorEvent, TimeFilterEditorState> {
|
||||
TimeFilterEditorBloc({required this.filterInfo})
|
||||
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
filterId: filterInfo.filter.id,
|
||||
),
|
||||
super(TimeFilterEditorState.initial(filterInfo)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
}
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
final FilterBackendService _filterBackendSvc;
|
||||
final FilterListener _listener;
|
||||
|
||||
void _dispatch() {
|
||||
on<TimeFilterEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
didReceiveFilter: (filter) {
|
||||
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
||||
emit(
|
||||
state.copyWith(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.timeFilter()!,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateCondition: (NumberFilterConditionPB condition) {
|
||||
_filterBackendSvc.insertTimeFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
content: state.filter.content,
|
||||
);
|
||||
},
|
||||
updateContent: (content) {
|
||||
_filterBackendSvc.insertTimeFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
content: content,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_filterBackendSvc.deleteFilter(
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_listener.start(
|
||||
onUpdated: (filter) {
|
||||
if (!isClosed) {
|
||||
add(TimeFilterEditorEvent.didReceiveFilter(filter));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimeFilterEditorEvent with _$TimeFilterEditorEvent {
|
||||
const factory TimeFilterEditorEvent.didReceiveFilter(FilterPB filter) =
|
||||
_DidReceiveFilter;
|
||||
const factory TimeFilterEditorEvent.updateCondition(
|
||||
NumberFilterConditionPB condition,
|
||||
) = _UpdateCondition;
|
||||
const factory TimeFilterEditorEvent.updateContent(String content) =
|
||||
_UpdateContent;
|
||||
const factory TimeFilterEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimeFilterEditorState with _$TimeFilterEditorState {
|
||||
const factory TimeFilterEditorState({
|
||||
required FilterInfo filterInfo,
|
||||
required TimeFilterPB filter,
|
||||
}) = _TimeFilterEditorState;
|
||||
|
||||
factory TimeFilterEditorState.initial(FilterInfo filterInfo) {
|
||||
return TimeFilterEditorState(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.timeFilter()!,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class GridSize {
|
||||
static double get cellVPadding => 10 * scale;
|
||||
static double get popoverItemHeight => 26 * scale;
|
||||
static double get typeOptionSeparatorHeight => 4 * scale;
|
||||
static double get newPropertyButtonWidth => 140 * scale;
|
||||
|
||||
static EdgeInsets get cellContentInsets => EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellHPadding,
|
||||
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/time_filter_editor_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
import 'choicechip.dart';
|
||||
|
||||
class TimeFilterChoiceChip extends StatefulWidget {
|
||||
const TimeFilterChoiceChip({
|
||||
super.key,
|
||||
required this.filterInfo,
|
||||
});
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
|
||||
@override
|
||||
State<TimeFilterChoiceChip> createState() => _TimeFilterChoiceChipState();
|
||||
}
|
||||
|
||||
class _TimeFilterChoiceChipState extends State<TimeFilterChoiceChip> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => TimeFilterEditorBloc(
|
||||
filterInfo: widget.filterInfo,
|
||||
),
|
||||
child: BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(200, 100)),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
popupBuilder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<TimeFilterEditorBloc>(),
|
||||
child: const TimeFilterEditor(),
|
||||
);
|
||||
},
|
||||
child: ChoiceChipButton(
|
||||
filterInfo: state.filterInfo,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeFilterEditor extends StatefulWidget {
|
||||
const TimeFilterEditor({super.key});
|
||||
|
||||
@override
|
||||
State<TimeFilterEditor> createState() => _TimeFilterEditorState();
|
||||
}
|
||||
|
||||
class _TimeFilterEditorState extends State<TimeFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [
|
||||
_buildFilterPanel(context, state),
|
||||
if (state.filter.condition != NumberFilterConditionPB.NumberIsEmpty &&
|
||||
state.filter.condition !=
|
||||
NumberFilterConditionPB.NumberIsNotEmpty) ...[
|
||||
const VSpace(4),
|
||||
_buildFilterTimeField(context, state),
|
||||
],
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
child: IntrinsicHeight(child: Column(children: children)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterPanel(
|
||||
BuildContext context,
|
||||
TimeFilterEditorState state,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
state.filterInfo.fieldInfo.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
Expanded(
|
||||
child: TimeFilterConditionPBList(
|
||||
filterInfo: state.filterInfo,
|
||||
popoverMutex: popoverMutex,
|
||||
onCondition: (condition) {
|
||||
context
|
||||
.read<TimeFilterEditorBloc>()
|
||||
.add(TimeFilterEditorEvent.updateCondition(condition));
|
||||
},
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
DisclosureButton(
|
||||
popoverMutex: popoverMutex,
|
||||
onAction: (action) {
|
||||
switch (action) {
|
||||
case FilterDisclosureAction.delete:
|
||||
context
|
||||
.read<TimeFilterEditorBloc>()
|
||||
.add(const TimeFilterEditorEvent.delete());
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterTimeField(
|
||||
BuildContext context,
|
||||
TimeFilterEditorState state,
|
||||
) {
|
||||
return FlowyTextField(
|
||||
text: state.filter.content,
|
||||
hintText: LocaleKeys.grid_settings_typeAValue.tr(),
|
||||
debounceDuration: const Duration(milliseconds: 300),
|
||||
autoFocus: false,
|
||||
onChanged: (text) {
|
||||
context
|
||||
.read<TimeFilterEditorBloc>()
|
||||
.add(TimeFilterEditorEvent.updateContent(text));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeFilterConditionPBList extends StatelessWidget {
|
||||
const TimeFilterConditionPBList({
|
||||
super.key,
|
||||
required this.filterInfo,
|
||||
required this.popoverMutex,
|
||||
required this.onCondition,
|
||||
});
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
final PopoverMutex popoverMutex;
|
||||
final Function(NumberFilterConditionPB) onCondition;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final timeFilter = filterInfo.timeFilter()!;
|
||||
return PopoverActionList<ConditionWrapper>(
|
||||
asBarrier: true,
|
||||
mutex: popoverMutex,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: NumberFilterConditionPB.values
|
||||
.map(
|
||||
(action) => ConditionWrapper(
|
||||
action,
|
||||
timeFilter.condition == action,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return ConditionButton(
|
||||
conditionName: timeFilter.condition.filterName,
|
||||
onTap: () => controller.show(),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) {
|
||||
onCondition(action.inner);
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConditionWrapper extends ActionCell {
|
||||
ConditionWrapper(this.inner, this.isSelected);
|
||||
|
||||
final NumberFilterConditionPB inner;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget? rightIcon(Color iconColor) =>
|
||||
isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
|
||||
|
||||
@override
|
||||
String get name => inner.filterName;
|
||||
}
|
||||
|
||||
extension TimeFilterConditionPBExtension on NumberFilterConditionPB {
|
||||
String get filterName {
|
||||
return switch (this) {
|
||||
NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(),
|
||||
NumberFilterConditionPB.NotEqual =>
|
||||
LocaleKeys.grid_numberFilter_notEqual.tr(),
|
||||
NumberFilterConditionPB.LessThan =>
|
||||
LocaleKeys.grid_numberFilter_lessThan.tr(),
|
||||
NumberFilterConditionPB.LessThanOrEqualTo =>
|
||||
LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(),
|
||||
NumberFilterConditionPB.GreaterThan =>
|
||||
LocaleKeys.grid_numberFilter_greaterThan.tr(),
|
||||
NumberFilterConditionPB.GreaterThanOrEqualTo =>
|
||||
LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(),
|
||||
NumberFilterConditionPB.NumberIsEmpty =>
|
||||
LocaleKeys.grid_numberFilter_isEmpty.tr(),
|
||||
NumberFilterConditionPB.NumberIsNotEmpty =>
|
||||
LocaleKeys.grid_numberFilter_isNotEmpty.tr(),
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
}
|
||||
+6
@@ -60,4 +60,10 @@ class FilterInfo {
|
||||
? NumberFilterPB.fromBuffer(filter.data.data)
|
||||
: null;
|
||||
}
|
||||
|
||||
TimeFilterPB? timeFilter() {
|
||||
return filter.data.fieldType == FieldType.Time
|
||||
? TimeFilterPB.fromBuffer(filter.data.data)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
|
||||
import 'choicechip/checkbox.dart';
|
||||
import 'choicechip/checklist/checklist.dart';
|
||||
import 'choicechip/date.dart';
|
||||
@@ -28,6 +29,8 @@ class FilterMenuItem extends StatelessWidget {
|
||||
SelectOptionFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
|
||||
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
|
||||
// FieldType.Time =>
|
||||
// TimeFilterChoiceChip(filterInfo: filterInfo),
|
||||
_ => const SizedBox(),
|
||||
};
|
||||
}
|
||||
|
||||
+12
-3
@@ -93,9 +93,12 @@ class _GridFieldCellState extends State<GridFieldCell> {
|
||||
onFieldInserted: widget.onFieldInsertedOnEitherSide,
|
||||
);
|
||||
},
|
||||
child: FieldCellButton(
|
||||
field: widget.fieldInfo.field,
|
||||
onTap: widget.onTap,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: FieldCellButton(
|
||||
field: widget.fieldInfo.field,
|
||||
onTap: widget.onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -217,6 +220,12 @@ class FieldCellButton extends StatelessWidget {
|
||||
field.fieldType.svgData,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
rightIcon: field.fieldType.rightIcon != null
|
||||
? FlowySvg(
|
||||
field.fieldType.rightIcon!,
|
||||
blendMode: null,
|
||||
)
|
||||
: null,
|
||||
radius: radius,
|
||||
text: FlowyText.medium(
|
||||
field.name,
|
||||
|
||||
+4
-1
@@ -151,7 +151,10 @@ class _CellTrailing extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: GridSize.trailHeaderPadding,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: GridSize.newPropertyButtonWidth,
|
||||
minHeight: GridSize.headerHeight,
|
||||
),
|
||||
margin: EdgeInsets.only(right: GridSize.scrollBarSize + Insets.m),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
|
||||
+4
-1
@@ -196,7 +196,10 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 200,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: GridSize.newPropertyButtonWidth,
|
||||
minHeight: GridSize.headerHeight,
|
||||
),
|
||||
decoration: _getDecoration(context),
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
|
||||
|
||||
+12
-1
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -52,7 +53,17 @@ class RowActionMenu extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(action.text, overflow: TextOverflow.ellipsis),
|
||||
onTap: () {
|
||||
action.performAction(context, viewId, rowId);
|
||||
if (action == RowAction.delete) {
|
||||
NavigatorOkCancelDialog(
|
||||
message: LocaleKeys.grid_row_deleteRowPrompt.tr(),
|
||||
onOkPressed: () {
|
||||
action.performAction(context, viewId, rowId);
|
||||
},
|
||||
).show(context);
|
||||
} else {
|
||||
action.performAction(context, viewId, rowId);
|
||||
}
|
||||
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
leftIcon: icon,
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'card_cell_skeleton/card_cell.dart';
|
||||
import 'card_cell_skeleton/checkbox_card_cell.dart';
|
||||
import 'card_cell_skeleton/checklist_card_cell.dart';
|
||||
import 'card_cell_skeleton/date_card_cell.dart';
|
||||
import 'card_cell_skeleton/number_card_cell.dart';
|
||||
import 'card_cell_skeleton/relation_card_cell.dart';
|
||||
import 'card_cell_skeleton/select_option_card_cell.dart';
|
||||
import 'card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'card_cell_skeleton/text_card_cell.dart';
|
||||
import 'card_cell_skeleton/time_card_cell.dart';
|
||||
import 'card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import 'card_cell_skeleton/translate_card_cell.dart';
|
||||
import 'card_cell_skeleton/url_card_cell.dart';
|
||||
|
||||
typedef CardCellStyleMap = Map<FieldType, CardCellStyle>;
|
||||
@@ -98,6 +101,18 @@ class CardCellBuilder {
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
FieldType.Time => TimeCardCell(
|
||||
key: key,
|
||||
style: isStyleOrNull(style),
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
FieldType.Translate => TranslateCardCell(
|
||||
key: key,
|
||||
style: isStyleOrNull(style),
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
_ => throw UnimplementedError,
|
||||
};
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
|
||||
import 'card_cell.dart';
|
||||
|
||||
class TimeCardCellStyle extends CardCellStyle {
|
||||
const TimeCardCellStyle({
|
||||
required super.padding,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final TextStyle textStyle;
|
||||
}
|
||||
|
||||
class TimeCardCell extends CardCell<TimeCardCellStyle> {
|
||||
const TimeCardCell({
|
||||
super.key,
|
||||
required super.style,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
|
||||
@override
|
||||
State<TimeCardCell> createState() => _TimeCellState();
|
||||
}
|
||||
|
||||
class _TimeCellState extends State<TimeCardCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return TimeCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<TimeCellBloc, TimeCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: widget.style.padding,
|
||||
child: Text(state.content, style: widget.style.textStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'card_cell.dart';
|
||||
|
||||
class TranslateCardCellStyle extends CardCellStyle {
|
||||
const TranslateCardCellStyle({
|
||||
required super.padding,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final TextStyle textStyle;
|
||||
}
|
||||
|
||||
class TranslateCardCell extends CardCell<TranslateCardCellStyle> {
|
||||
const TranslateCardCell({
|
||||
super.key,
|
||||
required super.style,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
|
||||
@override
|
||||
State<TranslateCardCell> createState() => _TranslateCellState();
|
||||
}
|
||||
|
||||
class _TranslateCellState extends State<TranslateCardCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return TranslateCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<TranslateCellBloc, TranslateCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: widget.style.padding,
|
||||
child: Text(state.content, style: widget.style.textStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -1,7 +1,9 @@
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
|
||||
import '../card_cell_builder.dart';
|
||||
import '../card_cell_skeleton/checkbox_card_cell.dart';
|
||||
import '../card_cell_skeleton/checklist_card_cell.dart';
|
||||
@@ -84,5 +86,9 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: TranslateCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
+13
-2
@@ -1,7 +1,9 @@
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
|
||||
import '../card_cell_builder.dart';
|
||||
import '../card_cell_skeleton/checkbox_card_cell.dart';
|
||||
import '../card_cell_skeleton/checklist_card_cell.dart';
|
||||
@@ -10,6 +12,7 @@ import '../card_cell_skeleton/number_card_cell.dart';
|
||||
import '../card_cell_skeleton/relation_card_cell.dart';
|
||||
import '../card_cell_skeleton/select_option_card_cell.dart';
|
||||
import '../card_cell_skeleton/text_card_cell.dart';
|
||||
import '../card_cell_skeleton/time_card_cell.dart';
|
||||
import '../card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import '../card_cell_skeleton/url_card_cell.dart';
|
||||
|
||||
@@ -84,5 +87,13 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Time: TimeCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: TranslateCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
+13
-2
@@ -1,7 +1,9 @@
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
|
||||
import '../card_cell_builder.dart';
|
||||
import '../card_cell_skeleton/checkbox_card_cell.dart';
|
||||
import '../card_cell_skeleton/checklist_card_cell.dart';
|
||||
@@ -10,6 +12,7 @@ import '../card_cell_skeleton/number_card_cell.dart';
|
||||
import '../card_cell_skeleton/relation_card_cell.dart';
|
||||
import '../card_cell_skeleton/select_option_card_cell.dart';
|
||||
import '../card_cell_skeleton/text_card_cell.dart';
|
||||
import '../card_cell_skeleton/time_card_cell.dart';
|
||||
import '../card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import '../card_cell_skeleton/url_card_cell.dart';
|
||||
|
||||
@@ -83,5 +86,13 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Time: TimeCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: TranslateCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
+51
-43
@@ -27,50 +27,58 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
onExit: (p) =>
|
||||
Provider.of<SummaryMouseNotifier>(context, listen: false)
|
||||
.onEnter = false,
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
enabled: false,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: GridSize.headerHeight,
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Center(
|
||||
child: TextField(
|
||||
controller: textEditingController,
|
||||
enabled: false,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellVPadding,
|
||||
),
|
||||
child: Consumer<SummaryMouseNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
SummaryMouseNotifier notifier,
|
||||
Widget? child,
|
||||
) {
|
||||
if (notifier.onEnter) {
|
||||
return SummaryCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
).positioned(right: 0, bottom: 0),
|
||||
],
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellVPadding,
|
||||
),
|
||||
child: Consumer<SummaryMouseNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
SummaryMouseNotifier notifier,
|
||||
Widget? child,
|
||||
) {
|
||||
if (notifier.onEnter) {
|
||||
return SummaryCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
).positioned(right: 0, bottom: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/time.dart';
|
||||
|
||||
class DesktopGridTimeCellSkin extends IEditableTimeCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TimeCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
maxLines: context.watch<TimeCellBloc>().state.wrap ? null : 1,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TranslateCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => TranslateMouseNotifier(),
|
||||
builder: (context, child) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) =>
|
||||
Provider.of<TranslateMouseNotifier>(context, listen: false)
|
||||
.onEnter = true,
|
||||
onExit: (p) =>
|
||||
Provider.of<TranslateMouseNotifier>(context, listen: false)
|
||||
.onEnter = false,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: GridSize.headerHeight,
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Center(
|
||||
child: TextField(
|
||||
controller: textEditingController,
|
||||
readOnly: true,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellVPadding,
|
||||
),
|
||||
child: Consumer<TranslateMouseNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
TranslateMouseNotifier notifier,
|
||||
Widget? child,
|
||||
) {
|
||||
if (notifier.onEnter) {
|
||||
return TranslateCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
).positioned(right: 0, bottom: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TranslateMouseNotifier extends ChangeNotifier {
|
||||
TranslateMouseNotifier();
|
||||
|
||||
bool _onEnter = false;
|
||||
|
||||
set onEnter(bool value) {
|
||||
if (_onEnter != value) {
|
||||
_onEnter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get onEnter => _onEnter;
|
||||
}
|
||||
+55
-18
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -62,22 +62,24 @@ class _ChecklistItemsState extends State<ChecklistItems> {
|
||||
if (showIncompleteOnly) {
|
||||
tasks.removeWhere((task) => task.isSelected);
|
||||
}
|
||||
final children = tasks
|
||||
.mapIndexed(
|
||||
(index, task) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: ChecklistItem(
|
||||
key: ValueKey(task.data.id),
|
||||
task: task,
|
||||
autofocus: widget.state.newTask && index == tasks.length - 1,
|
||||
onSubmitted: index == tasks.length - 1
|
||||
? () => widget.bloc
|
||||
.add(const ChecklistCellEvent.createNewTask(""))
|
||||
: null,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
// final children = tasks
|
||||
// .mapIndexed(
|
||||
// (index, task) => Padding(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
// child: ChecklistItem(
|
||||
// key: ValueKey('${task.data.id}$index'),
|
||||
// task: task,
|
||||
// autofocus: widget.state.newTask && index == tasks.length - 1,
|
||||
// onSubmitted: () {
|
||||
// if (index == tasks.length - 1) {
|
||||
// // create a new task under the last task if the users press enter
|
||||
// widget.bloc.add(const ChecklistCellEvent.createNewTask(''));
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Column(
|
||||
@@ -115,7 +117,7 @@ class _ChecklistItemsState extends State<ChecklistItems> {
|
||||
),
|
||||
),
|
||||
const VSpace(2.0),
|
||||
...children,
|
||||
_ChecklistCellEditors(tasks: tasks),
|
||||
ChecklistItemControl(cellNotifer: widget.cellContainerNotifier),
|
||||
],
|
||||
),
|
||||
@@ -123,6 +125,41 @@ class _ChecklistItemsState extends State<ChecklistItems> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistCellEditors extends StatelessWidget {
|
||||
const _ChecklistCellEditors({
|
||||
required this.tasks,
|
||||
});
|
||||
|
||||
final List<ChecklistSelectOption> tasks;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<ChecklistCellBloc>();
|
||||
final state = bloc.state;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...tasks.mapIndexed(
|
||||
(index, task) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: ChecklistItem(
|
||||
key: ValueKey('${task.data.id}$index'),
|
||||
task: task,
|
||||
autofocus: state.newTask && index == tasks.length - 1,
|
||||
onSubmitted: () {
|
||||
if (index == tasks.length - 1) {
|
||||
// create a new task under the last task if the users press enter
|
||||
bloc.add(const ChecklistCellEvent.createNewTask(''));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistItemControl extends StatelessWidget {
|
||||
const ChecklistItemControl({super.key, required this.cellNotifer});
|
||||
|
||||
|
||||
+47
-34
@@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DesktopRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
@override
|
||||
@@ -13,41 +14,53 @@ class DesktopRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SummaryCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
readOnly: true,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
ChangeNotifierProvider.value(
|
||||
value: cellContainerNotifier,
|
||||
child: Selector<CellContainerNotifier, bool>(
|
||||
selector: (_, notifier) => notifier.isHover,
|
||||
builder: (context, isHover, child) {
|
||||
return Visibility(
|
||||
visible: isHover,
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
SummaryCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../editable_cell_skeleton/time.dart';
|
||||
|
||||
class DesktopRowDetailTimeCellSkin extends IEditableTimeCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TimeCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 707 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user