Compare commits

...
Author SHA1 Message Date
Kilu f2991be276 fix: modified web server 2024-06-26 15:23:23 +08:00
Kilu 3645dd1b8b fix: lint error 2024-06-25 18:33:34 +08:00
Kilu 4237a73810 feat: support publish interfaces 2024-06-25 18:26:53 +08:00
Lucas.Xu 80a2ebc216 feat: fetch the publish info to show the publish status 2024-06-21 21:26:52 +08:00
Lucas.Xu 02a9f0dfbb feat: integrate unpublish api 2024-06-21 21:08:48 +08:00
Lucas.Xu 4a8536d403 feat: integrate publish api 2024-06-21 21:02:05 +08:00
Lucas.Xu 9c3f083684 Merge branch 'feat/support-get-encoded-collab-event' of https://github.com/AppFlowy-IO/AppFlowy into feat/support-get-encoded-collab-event 2024-06-21 20:21:47 +08:00
Lucas.Xu 628eace341 Merge branch 'publish' into feat/support-get-encoded-collab-event 2024-06-21 20:20:33 +08:00
qinluhe 77a53ef67a feat: support publish view and unpublish views 2024-06-21 18:46:31 +08:00
Lucas.Xu 5dec6ea545 feat: call the publish event 2024-06-21 16:25:06 +08:00
qinluhe dbcf8ba86f feat: support publish view and unpublish views 2024-06-21 16:19:35 +08:00
qinluhe 63fe1a6ef3 feat: support a event for getting encoded collab of document 2024-06-21 16:07:05 +08:00
Lucas.Xu a2c52508fa chore: refacotor share bloc 2024-06-21 15:32:44 +08:00
Lucas.Xu 0709a2d2ee feat: publish page to the web 2024-06-21 15:23:11 +08:00
Lucas.Xu e73b310076 chore: merge release/0.6.1 2024-06-21 11:24:38 +08:00
Mathias Mogensen 7586a0ed48 fix: settings ui improvements (#5590)
* fix: settings ui improvements

* fix: replace placeholder + fix open folder

* fix: collapse sidebar button on windows

* test: fix tests after changes

* test: missed type cast
2024-06-20 21:01:20 +02:00
Mathias MogensenandLucas.Xu d8b32168a9 feat: hide application to dock macos (#5592)
* feat: hide application to tray macos

* chore: enable reopen the app after closing it

* fix: remove tray

* fix: remove asset

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-20 21:00:56 +02:00
Mathias Mogensen 8ca541bfa1 fix: reminder race condition (#5587) 2024-06-20 10:13:59 +02:00
Nathan.fooo b64da2c02f refactor: File upload (#5542)
* chore: rename service

* refactor: upload

* chore: save upload meta data

* chore: add sql test

* chore: uploader

* chore: fix upload

* chore: cache file and remove after finish

* chore: retry upload

* chore: pause when netowork unreachable

* chore: add event test

* chore: add test

* chore: clippy

* chore: update client-api commit id

* chore: fix flutter test
2024-06-20 07:44:57 +08:00
Lucas.Xu fdaca36b87 fix: automatically open page after switching space (#5580) 2024-06-19 21:28:49 +08:00
Lucas.Xu 8557383a89 fix: cursor height is inconsistent after upgrading to Flutter 3.22 (#5574)
* fix: cursor height is inconsistent after upgrading to Flutter 3.22

* fix: cursor height is inconsistent after upgrading to Flutter 3.22

* fix: android toolbar height issue
2024-06-19 19:34:47 +08:00
Mathias Mogensen fa86480458 feat: shortcuts page remake (#5567)
* feat: settings shortcuts page remake

* test: add shortcut test

* fix: physics on listview

* fix: menu icon
2024-06-19 10:24:34 +02:00
Nathan.fooo d75eb05707 chore: fix flutter test (#5572) 2024-06-19 13:26:18 +08:00
Lucas.Xu a2b32585a1 chore: bump version 0.6.0 (#5571) 2024-06-19 12:10:05 +08:00
Lucas.Xu 75cea400d2 fix: cursor jump randomly in check list item (#5565)
* chore: remove debug logs

* fix: cursor jump ramdomly in checklist item
2024-06-19 09:33:27 +08:00
Lucas.XuandMathias Mogensen ed82ec8eef chore: remove gl_es env (#5564)
* fix: try without media kit

* chore: remove gl_es env

* Revert "fix: try without media kit"

This reverts commit e331dbfabf.

---------

Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
2024-06-18 16:16:31 +08:00
Lucas.XuandMathias Mogensen b6debd6c71 fix: remove media_kit (#5563)
Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
2024-06-18 15:47:52 +08:00
Nathan.fooo a05ed63a3e chore: fix can't delete characters in card (#5562)
* chore: fix can't delete characters in card

* chore: i18n
2024-06-18 13:12:57 +08:00
Nathan.fooo 3e75f1f24a chore: fix checklist potential panic (#5561)
* chore: fix checklist

* chore: fix checklist
2024-06-18 10:16:39 +08:00
Lucas.Xu e607694729 chore: change slash menu hover color (#5555)
* feat: add a pinned icon after page name

* chore: change slack menu hover color

* Revert "feat: add a pinned icon after page name"

This reverts commit a668e0bd09.
2024-06-18 09:55:43 +08:00
ansizheshi 91d870415e chore: update zh-CN translations (#5545)
* chore: update translations with Fink 🐦

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

* Add files via upload

* Restore en.json

* Restore en.json
2024-06-18 09:52:44 +08:00
Nathan.fooo 21a497924c chore: adjust height (#5558) 2024-06-17 21:38:05 +08:00
Mathias Mogensen 4a126e17ce fix: settings improvements (#5547)
* fix: user workspace bloc changed

* fix: use translate card cell style

* fix: add getworkspacemember

* fix: billing launch review

* fix: disable time field

* fix: member tooltip

* fix: remove my account description

* fix: punctuation

* fix: filter workspace font

* fix: cloud toggle

* fix: minor adjustments

* chore: disable cloud document search

* fix: improve workspace name textfield

* test: move billing test to cloud

* fix: use cache over remote

* fix: clippy and tests

* chore: amend flowy tooltip

* test: add pump and settle

* test: integration test for local auth
2024-06-17 14:30:19 +02:00
Lucas.Xu 8bf97ad5c6 feat: add a pinned icon after page name (#5553) 2024-06-17 19:18:06 +08:00
Lucas.Xu a85d11fb6d feat: collapse space and switch space by shortcuts (#5554)
* chore: improve delete popup style

* feat: support collapsing space and switching space by shortcuts
2024-06-17 18:59:08 +08:00
Lucas.Xu 1fd34bc58a chore: turn off the billing feature (#5551) 2024-06-17 17:33:40 +08:00
Lucas.Xu 70a7b856a2 feat: improve space UX (#5549)
* fix: scroll bar hover color

* chore: replace board icon

* chore: remove close button in upgrade menu

* chore: add translation

* feat: switch to the next space by shortcut

* chore: replace space lock icon

* chore: remove arrow icon in change icon button

* feat: only show other favorites header if the other two is empty

* fix: create new page in current space

* feat: create a new page in a newly created space by default

* chore: update translations

* feat: open the first page after switching space by default

* chore: replace board icon

* fix: open the first page(non-space) after switching workspace

* chore: revert more icon

* chore: revert editor version
2024-06-17 16:23:17 +08:00
Lucas.Xu 49683e6079 fix: migrate the private views only if they are not empty (#5544) 2024-06-17 10:29:15 +08:00
Lucas.Xu 13107b63e9 feat: improve upgrade space toast (#5543)
* fix: divider color

* feat: improve upgrade space toast
2024-06-15 09:03:09 +08:00
Nathan.fooo ce1a6e4fca chore: highlight code block (#5540) 2024-06-14 14:53:53 +08:00
Lucas.Xu 1d23e28eaf chore: enable billing system (#5539) 2024-06-14 08:42:20 +02:00
Lucas.Xu 2a4d78b688 feat: improve hover color in dark mode (#5538) 2024-06-14 14:31:55 +08:00
Lucas.Xu cbe452a73d feat: improve page thumbnail on mobile (#5535)
* feat: improve view thumbnail

* chore: blur the bottom navigation bar

* feat: improve date format

* feat: support url avatar

* fix: remove duplicated divider
2024-06-14 13:15:41 +08:00
Nathan.fooo 27899ee993 chore: save workspace member info to disk (#5536)
* chore: save workspace member info to disk

* chore: fix clippy
2024-06-14 11:34:23 +08:00
Lucas.Xu 785597f53e fix: space issues (#5531)
* fix: display the space icon in breadcrumb and disable the space entry

* fix: the icon and the title are not aligned center in more menu

* fix: incorrect space icon corner radius

* fix: missed tooltip for add new page button

* fix: disable space migration for local user

* chore: use general as default space name

* fix: space name overflow

* fix: only show arrow button when hovering on page

* fix: sidebar tooltip font size

* fix: use space name as hint text

* feat: support adding a new space from space menu

* fix: filter the space view
2024-06-14 09:32:02 +08:00
Nathan.fooo e2ce274718 chore: polish chat UI (#5533)
* chore: adjust chat ui

* chore: clippy
2024-06-14 09:02:06 +08:00
421 changed files with 11005 additions and 7341 deletions

No files matched your search

+1 -1
View File
@@ -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
+16
View File
@@ -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
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.0"
APPFLOWY_VERSION = "0.6.1"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -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
@@ -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',
@@ -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);
});
});
}
@@ -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();
}
@@ -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
@@ -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),
),
),
),
),
);
}
-3
View File
@@ -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();
}
@@ -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),
),
@@ -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,7 +179,7 @@ ActionPane buildEndActionPane(
BuildContext context,
List<MobilePaneActionType> actions, {
bool needSpace = true,
MobileViewCardType? cardType,
MobilePageCardType? cardType,
FolderSpaceType? spaceType,
}) {
return ActionPane(
@@ -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,7 +29,7 @@ const mobileSupportedFieldTypes = [
FieldType.CreatedTime,
FieldType.Checkbox,
FieldType.Checklist,
FieldType.Time,
// FieldType.Time,
];
Future<FieldType?> showFieldTypeGridBottomSheet(
@@ -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,
@@ -42,8 +42,8 @@ class MobileFolders extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
BlocProvider(
create: (_) =>
SpaceBloc()..add(SpaceEvent.initial(user, workspaceId)),
create: (_) => SpaceBloc()
..add(SpaceEvent.initial(user, workspaceId, openFirstPage: false)),
),
],
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
@@ -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) =>
@@ -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,
),
);
},
@@ -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(),
};
}
@@ -10,6 +10,10 @@ 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/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';
@@ -23,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,
@@ -44,7 +49,7 @@ class MobileViewCard extends StatelessWidget {
final ViewPB view;
final Int64? timestamp;
final MobileViewCardType type;
final MobilePageCardType type;
@override
Widget build(BuildContext context) {
@@ -153,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,
@@ -185,6 +191,7 @@ class MobileViewCard extends StatelessWidget {
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.w600,
height: 1.3,
),
),
],
@@ -203,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,20 +261,20 @@ class MobileViewCard extends StatelessWidget {
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;
@@ -273,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) {
@@ -4,6 +4,7 @@ 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';
@@ -22,6 +23,18 @@ class MobileSpace extends StatefulWidget {
}
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>(
@@ -42,10 +55,6 @@ class _MobileSpaceState extends State<MobileSpace> {
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: currentSpace.spacePermission ==
SpacePermission.publicToAll
? ViewSectionPB.Public
: ViewSectionPB.Private,
),
);
context.read<SpaceBloc>().add(
@@ -85,6 +94,14 @@ class _MobileSpaceState extends State<MobileSpace> {
},
);
}
void _createNewPage() {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
),
);
}
}
class _Pages extends StatelessWidget {
@@ -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,
@@ -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');
});
},
);
},
);
}
}
@@ -16,7 +16,9 @@ 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 sendMessageErrorKey = "sendMessageError";
@@ -67,7 +69,19 @@ 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;
@@ -377,6 +391,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
"chatId": chatId,
},
id: streamMessageId,
createdAt: DateTime.now().millisecondsSinceEpoch,
text: '',
);
}
@@ -393,7 +408,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
author: User(id: message.authorId),
id: messageId,
text: message.content,
createdAt: message.createdAt.toInt(),
createdAt: message.createdAt.toInt() * 1000,
);
}
}
@@ -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
@@ -2,6 +2,7 @@ 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';
@@ -13,6 +14,8 @@ 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,
@@ -132,13 +135,28 @@ class ChatAITextMessageWidget extends StatelessWidget {
),
),
PreConfig(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.all(
Radius.circular(8.0),
),
),
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(
@@ -156,12 +174,60 @@ class ChatAITextMessageWidget extends StatelessWidget {
height: 1.5,
),
),
BlockquoteConfig.darkConfig,
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(
@@ -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!),
),
),
);
}
}
@@ -53,7 +53,8 @@ class TextMessageText extends StatelessWidget {
fontSize: 16,
fontWeight: FontWeight.w500,
lineHeight: 1.5,
maxLines: 2000,
maxLines: null,
selectable: true,
color: AFThemeExtension.of(context).textColor,
);
}
@@ -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(
@@ -49,9 +49,7 @@ class DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {
on<DatabaseGroupEvent>(
(event, emit) async {
await event.when(
initial: () {
_startListening();
},
initial: () async => _startListening(),
didReceiveFieldUpdate: (fieldInfos) {
emit(
state.copyWith(
@@ -464,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,
@@ -610,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,
@@ -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';
@@ -147,7 +150,14 @@ class BoardShortcutContainer extends StatelessWidget {
if (focusScope.value.length != 1) {
return false;
}
context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));
NavigatorOkCancelDialog(
message: LocaleKeys.grid_row_deleteCardPrompt.tr(),
onOkPressed: () {
context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));
},
).show(context);
return true;
}
@@ -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,
),
],
@@ -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';
@@ -8,7 +9,6 @@ import 'choicechip/number.dart';
import 'choicechip/select_option/select_option.dart';
import 'choicechip/text.dart';
import 'choicechip/url.dart';
import 'choicechip/time.dart';
import 'filter_info.dart';
class FilterMenuItem extends StatelessWidget {
@@ -23,15 +23,14 @@ class FilterMenuItem extends StatelessWidget {
FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo),
FieldType.MultiSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo),
FieldType.Number =>
NumberFilterChoiceChip(filterInfo: filterInfo),
FieldType.Number => NumberFilterChoiceChip(filterInfo: filterInfo),
FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo),
FieldType.SingleSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo),
FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
FieldType.Time =>
TimeFilterChoiceChip(filterInfo: filterInfo),
// FieldType.Time =>
// TimeFilterChoiceChip(filterInfo: filterInfo),
_ => const SizedBox(),
};
}
@@ -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,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,7 +86,7 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
FieldType.Translate: TranslateCardCellStyle(
padding: padding,
textStyle: textStyle,
),
@@ -1,6 +1,7 @@
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';
@@ -90,7 +91,7 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
FieldType.Translate: TranslateCardCellStyle(
padding: padding,
textStyle: textStyle,
),
@@ -1,6 +1,7 @@
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';
@@ -89,7 +90,7 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
FieldType.Translate: TranslateCardCellStyle(
padding: padding,
textStyle: textStyle,
),
@@ -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});
@@ -1,23 +1,21 @@
import 'dart:async';
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/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/util/debounce.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/cell/bloc/checklist_cell_bloc.dart';
import 'checklist_cell_textfield.dart';
import 'checklist_progress_bar.dart';
class ChecklistCellEditor extends StatefulWidget {
@@ -89,7 +87,7 @@ class _ChecklistCellEditorState extends State<ChecklistCellEditor> {
}
}
/// Displays the a list of all the exisiting tasks and an input field to create
/// Displays the a list of all the existing tasks and an input field to create
/// a new task if `isAddingNewTask` is true
class ChecklistItemList extends StatelessWidget {
const ChecklistItemList({
@@ -159,154 +157,136 @@ class ChecklistItem extends StatefulWidget {
}
class _ChecklistItemState extends State<ChecklistItem> {
late final TextEditingController _textController;
final FocusNode _focusNode = FocusNode(skipTraversal: true);
final FocusNode _textFieldFocusNode = FocusNode();
TextEditingController textController = TextEditingController();
final textFieldFocusNode = FocusNode();
final focusNode = FocusNode(skipTraversal: true);
bool _isHovered = false;
bool _isFocused = false;
Timer? _debounceOnChanged;
bool isHovered = false;
bool isFocused = false;
final _debounceOnChanged = Debounce(
duration: const Duration(milliseconds: 300),
);
final selectTaskShortcut = {
SingleActivator(
LogicalKeyboardKey.enter,
meta: Platform.isMacOS,
control: !Platform.isMacOS,
): const _SelectTaskIntent(),
const SingleActivator(LogicalKeyboardKey.escape):
const _EndEditingTaskIntent(),
};
@override
void initState() {
super.initState();
_textController = TextEditingController(text: widget.task.data.name);
}
@override
void dispose() {
_debounceOnChanged?.cancel();
_textController.dispose();
_focusNode.dispose();
_textFieldFocusNode.dispose();
super.dispose();
}
@override
void didUpdateWidget(ChecklistItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.task.data.name != oldWidget.task.data.name) {
final selection = _textController.selection;
_textController.text = widget.task.data.name;
_textController.selection = selection;
textController.text = widget.task.data.name;
if (widget.autofocus) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
textFieldFocusNode.requestFocus();
});
}
}
@override
void dispose() {
_debounceOnChanged.dispose();
textController.dispose();
focusNode.dispose();
textFieldFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isFocusedOrHovered =
isHovered || isFocused || textFieldFocusNode.hasFocus;
final color = isFocusedOrHovered
? AFThemeExtension.of(context).lightGreyHover
: Colors.transparent;
return FocusableActionDetector(
focusNode: _focusNode,
onShowHoverHighlight: (isHovered) {
setState(() => _isHovered = isHovered);
},
onFocusChange: (isFocused) {
setState(() => _isFocused = isFocused);
},
actions: {
_SelectTaskIntent: CallbackAction<_SelectTaskIntent>(
onInvoke: (_SelectTaskIntent intent) => context
.read<ChecklistCellBloc>()
.add(ChecklistCellEvent.selectTask(widget.task.data.id)),
),
_EndEditingTaskIntent: CallbackAction<_EndEditingTaskIntent>(
onInvoke: (_EndEditingTaskIntent intent) =>
_textFieldFocusNode.unfocus(),
),
},
shortcuts: {
SingleActivator(
LogicalKeyboardKey.enter,
meta: Platform.isMacOS,
control: !Platform.isMacOS,
): const _SelectTaskIntent(),
},
focusNode: focusNode,
onShowHoverHighlight: (value) => setState(() {
isHovered = value;
}),
onFocusChange: (value) => setState(() {
isFocused = value;
}),
actions: _buildActions(),
shortcuts: selectTaskShortcut,
child: Container(
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
constraints: BoxConstraints(
minHeight: GridSize.popoverItemHeight,
),
decoration: BoxDecoration(
color: _isHovered || _isFocused || _textFieldFocusNode.hasFocus
? AFThemeExtension.of(context).lightGreyHover
: Colors.transparent,
color: color,
borderRadius: Corners.s6Border,
),
child: Row(
children: [
ExcludeFocus(
child: FlowyIconButton(
width: 32,
icon: FlowySvg(
widget.task.isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
),
hoverColor: Colors.transparent,
onPressed: () => context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.selectTask(widget.task.data.id),
),
),
),
Expanded(
child: Shortcuts(
shortcuts: const {
SingleActivator(LogicalKeyboardKey.escape):
_EndEditingTaskIntent(),
},
child: Builder(
builder: (context) {
return TextField(
controller: _textController,
focusNode: _textFieldFocusNode,
autofocus: widget.autofocus,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
isCollapsed: true,
contentPadding: EdgeInsets.only(
top: 8.0,
bottom: 8.0,
left: 2.0,
right: _isHovered ? 2.0 : 8.0,
),
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
),
textInputAction: widget.onSubmitted == null
? TextInputAction.next
: null,
onChanged: (text) {
if (_textController.value.composing.isCollapsed) {
_debounceOnChangedText(text);
}
},
onSubmitted: (description) {
_submitUpdateTaskDescription(description);
if (widget.onSubmitted != null) {
widget.onSubmitted?.call();
} else {
Actions.invoke(context, const NextFocusIntent());
}
},
);
},
),
),
),
if (_isHovered || _isFocused || _textFieldFocusNode.hasFocus)
_DeleteTaskButton(
onPressed: () => context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.deleteTask(widget.task.data.id),
),
),
],
child: _buildChild(
context,
isFocusedOrHovered,
),
),
);
}
void _debounceOnChangedText(String text) {
_debounceOnChanged?.cancel();
_debounceOnChanged = Timer(const Duration(milliseconds: 300), () {
_submitUpdateTaskDescription(text);
});
Widget _buildChild(BuildContext context, bool isFocusedOrHovered) {
return Row(
children: [
ChecklistCellCheckIcon(task: widget.task),
Expanded(
child: ChecklistCellTextfield(
textController: textController,
focusNode: textFieldFocusNode,
autofocus: widget.autofocus,
onChanged: () {
_debounceOnChanged.call(() {
if (textController.selection.isCollapsed) {
_submitUpdateTaskDescription(textController.text);
}
});
},
onSubmitted: () {
_submitUpdateTaskDescription(textController.text);
if (widget.onSubmitted != null) {
widget.onSubmitted?.call();
} else {
Actions.invoke(context, const NextFocusIntent());
}
},
),
),
if (isFocusedOrHovered)
ChecklistCellDeleteButton(
onPressed: () => context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.deleteTask(widget.task.data.id),
),
),
],
);
}
Map<Type, Action<Intent>> _buildActions() {
return {
_SelectTaskIntent: CallbackAction<_SelectTaskIntent>(
onInvoke: (_SelectTaskIntent intent) {
context
.read<ChecklistCellBloc>()
.add(ChecklistCellEvent.selectTask(widget.task.data.id));
return;
},
),
_EndEditingTaskIntent: CallbackAction<_EndEditingTaskIntent>(
onInvoke: (_EndEditingTaskIntent intent) {
textFieldFocusNode.unfocus();
return;
},
),
};
}
void _submitUpdateTaskDescription(String description) {
@@ -410,56 +390,3 @@ class _NewTaskItemState extends State<NewTaskItem> {
);
}
}
class _DeleteTaskButton extends StatefulWidget {
const _DeleteTaskButton({
required this.onPressed,
});
final VoidCallback onPressed;
@override
State<_DeleteTaskButton> createState() => _DeleteTaskButtonState();
}
class _DeleteTaskButtonState extends State<_DeleteTaskButton> {
final _materialStatesController = WidgetStatesController();
@override
void dispose() {
_materialStatesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: widget.onPressed,
onHover: (_) => setState(() {}),
onFocusChange: (_) => setState(() {}),
style: ButtonStyle(
fixedSize: const WidgetStatePropertyAll(Size.square(32)),
minimumSize: const WidgetStatePropertyAll(Size.square(32)),
maximumSize: const WidgetStatePropertyAll(Size.square(32)),
overlayColor: WidgetStateProperty.resolveWith((state) {
if (state.contains(WidgetState.focused)) {
return AFThemeExtension.of(context).greyHover;
}
return Colors.transparent;
}),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: Corners.s6Border),
),
),
statesController: _materialStatesController,
child: FlowySvg(
FlowySvgs.delete_s,
color: _materialStatesController.value
.contains(WidgetState.hovered) ||
_materialStatesController.value.contains(WidgetState.focused)
? Theme.of(context).colorScheme.error
: null,
),
);
}
}
@@ -0,0 +1,129 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/cell/bloc/checklist_cell_bloc.dart';
class ChecklistCellCheckIcon extends StatelessWidget {
const ChecklistCellCheckIcon({
super.key,
required this.task,
});
final ChecklistSelectOption task;
@override
Widget build(BuildContext context) {
return ExcludeFocus(
child: FlowyIconButton(
width: 32,
icon: FlowySvg(
task.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
),
hoverColor: Colors.transparent,
onPressed: () => context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.selectTask(task.data.id),
),
),
);
}
}
class ChecklistCellTextfield extends StatelessWidget {
const ChecklistCellTextfield({
super.key,
required this.textController,
required this.focusNode,
required this.autofocus,
required this.onChanged,
this.onSubmitted,
});
final TextEditingController textController;
final FocusNode focusNode;
final bool autofocus;
final VoidCallback? onSubmitted;
final VoidCallback onChanged;
@override
Widget build(BuildContext context) {
const contentPadding = EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 2.0,
);
return TextField(
controller: textController,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
isCollapsed: true,
contentPadding: contentPadding,
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
),
textInputAction: onSubmitted == null ? TextInputAction.next : null,
onChanged: (_) => onChanged(),
onSubmitted: (_) => onSubmitted?.call(),
);
}
}
class ChecklistCellDeleteButton extends StatefulWidget {
const ChecklistCellDeleteButton({
super.key,
required this.onPressed,
});
final VoidCallback onPressed;
@override
State<ChecklistCellDeleteButton> createState() =>
_ChecklistCellDeleteButtonState();
}
class _ChecklistCellDeleteButtonState extends State<ChecklistCellDeleteButton> {
final _materialStatesController = WidgetStatesController();
@override
void dispose() {
_materialStatesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: widget.onPressed,
onHover: (_) => setState(() {}),
onFocusChange: (_) => setState(() {}),
style: ButtonStyle(
fixedSize: const WidgetStatePropertyAll(Size.square(32)),
minimumSize: const WidgetStatePropertyAll(Size.square(32)),
maximumSize: const WidgetStatePropertyAll(Size.square(32)),
overlayColor: WidgetStateProperty.resolveWith((state) {
if (state.contains(WidgetState.focused)) {
return AFThemeExtension.of(context).greyHover;
}
return Colors.transparent;
}),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: Corners.s6Border),
),
),
statesController: _materialStatesController,
child: FlowySvg(
FlowySvgs.delete_s,
color: _materialStatesController.value.contains(WidgetState.hovered) ||
_materialStatesController.value.contains(WidgetState.focused)
? Theme.of(context).colorScheme.error
: null,
),
);
}
}
@@ -94,7 +94,9 @@ class SelectOptionTag extends StatelessWidget {
color: AFThemeExtension.of(context).textColor,
textAlign: textAlign,
);
return Container(
height: 20,
padding: onRemove == null ? padding : padding.copyWith(right: 2.0),
decoration: BoxDecoration(
color: optionColor,
@@ -122,4 +124,3 @@ class SelectOptionTag extends StatelessWidget {
);
}
}
@@ -1,5 +1,7 @@
import 'dart:typed_data';
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/field/field_controller.dart';
@@ -11,12 +13,10 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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';
import 'field_type_list.dart';
@@ -258,7 +258,6 @@ enum FieldAction {
onChanged: (_) => context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.toggleWrapCellContent()),
style: ToggleStyle.big,
padding: EdgeInsets.zero,
);
}
@@ -22,7 +22,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.CreatedTime,
FieldType.Relation,
FieldType.Summary,
FieldType.Time,
// FieldType.Time,
FieldType.Translate,
];
@@ -1,12 +1,12 @@
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/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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class DateFormatButton extends StatelessWidget {
const DateFormatButton({
@@ -248,7 +248,6 @@ class IncludeTimeButton extends StatelessWidget {
Toggle(
value: value,
onChanged: onChanged,
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
@@ -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';
@@ -7,17 +9,13 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/util/field_type_extension.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:collection/collection.dart';
import 'package:easy_localization/easy_localization.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:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
@@ -75,7 +73,6 @@ class DatabaseGroupList extends StatelessWidget {
value: !state.layoutSettings.hideUngroupedColumn,
onChanged: (value) =>
_updateLayoutSettings(state.layoutSettings, value),
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
@@ -114,14 +114,14 @@ class DocumentService {
/// Upload a file to the cloud storage.
Future<FlowyResult<UploadedFilePB, FlowyError>> uploadFile({
required String localFilePath,
bool isAsync = true,
required String documentId,
}) async {
final workspace = await FolderEventReadCurrentWorkspace().send();
return workspace.fold((l) async {
final payload = UploadFileParamsPB(
workspaceId: l.id,
localFilePath: localFilePath,
isAsync: isAsync,
documentId: documentId,
);
final result = await DocumentEventUploadFile(payload).send();
return result;
@@ -1,52 +1,102 @@
import 'dart:io';
import 'package:appflowy/workspace/application/export/document_exporter.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_share_bloc.freezed.dart';
const _url = 'https://test.appflowy.io';
class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
DocumentShareBloc({
required this.view,
}) : super(const DocumentShareState.initial()) {
}) : super(DocumentShareState.initial()) {
on<DocumentShareEvent>((event, emit) async {
await event.when(
initial: () async {
final publishInfo = await ViewBackendService.getPublishInfo(view);
publishInfo.fold((s) {
emit(
state.copyWith(
isPublished: true,
url: '$_url/${s.namespace}/${s.publishName}',
),
);
}, (f) {
emit(state.copyWith(isPublished: false, url: ''));
});
},
share: (type, path) async {
if (DocumentShareType.unimplemented.contains(type)) {
Log.error('DocumentShareType $type is not implemented');
return;
}
emit(const DocumentShareState.loading());
emit(state.copyWith(isLoading: true));
final exporter = DocumentExporter(view);
final FlowyResult<ExportDataPB, FlowyError> result =
await exporter.export(type.exportType).then((value) {
return value.fold(
(s) {
if (path != null) {
switch (type) {
case DocumentShareType.markdown:
return FlowyResult.success(_saveMarkdownToPath(s, path));
case DocumentShareType.html:
return FlowyResult.success(_saveHTMLToPath(s, path));
default:
break;
}
}
return FlowyResult.failure(FlowyError());
},
(f) => FlowyResult.failure(f),
final result = await _export(type, path);
emit(
state.copyWith(
isLoading: false,
exportResult: result,
),
);
},
publish: (url) async {
// todo: optimize the logic
const prefix = 'appflowy';
final name = '${view.name}-${uuid()}'.substring(0, 19);
// set space name
try {
final nameSpace =
await ViewBackendService.getPublishNameSpace().getOrThrow();
if (nameSpace.namespace != prefix) {
await ViewBackendService.setPublishNameSpace(prefix).getOrThrow();
}
final result = await ViewBackendService.publish(view, name: name);
emit(
state.copyWith(
isPublished: true,
publishResult: result,
url: '$_url/${nameSpace.namespace}/$name',
),
);
});
} catch (e) {
Log.error('publish error: $e');
emit(DocumentShareState.finish(result));
emit(
state.copyWith(
isPublished: false,
publishResult: FlowyResult.failure(
FlowyError(msg: 'publish error: $e'),
),
url: '',
),
);
}
},
unPublish: () async {
await ViewBackendService.unpublish(view);
emit(
state.copyWith(
isPublished: false,
publishResult: null,
url: '',
),
);
},
);
});
@@ -54,6 +104,31 @@ class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
final ViewPB view;
late final exporter = DocumentExporter(view);
Future<FlowyResult<ExportDataPB, FlowyError>> _export(
DocumentShareType type,
String? path,
) async {
final result = await exporter.export(type.exportType);
return result.fold(
(s) {
if (path != null) {
switch (type) {
case DocumentShareType.markdown:
return FlowySuccess(_saveMarkdownToPath(s, path));
case DocumentShareType.html:
return FlowySuccess(_saveHTMLToPath(s, path));
default:
break;
}
}
return FlowyResult.failure(FlowyError());
},
(f) => FlowyResult.failure(f),
);
}
ExportDataPB _saveMarkdownToPath(String markdown, String path) {
File(path).writeAsStringSync(markdown);
return ExportDataPB()
@@ -93,15 +168,28 @@ enum DocumentShareType {
@freezed
class DocumentShareEvent with _$DocumentShareEvent {
const factory DocumentShareEvent.share(DocumentShareType type, String? path) =
Share;
const factory DocumentShareEvent.initial() = _Initial;
const factory DocumentShareEvent.share(
DocumentShareType type,
String? path,
) = _Share;
const factory DocumentShareEvent.publish(String url) = _Publish;
const factory DocumentShareEvent.unPublish() = _UnPublish;
}
@freezed
class DocumentShareState with _$DocumentShareState {
const factory DocumentShareState.initial() = _Initial;
const factory DocumentShareState.loading() = _Loading;
const factory DocumentShareState.finish(
FlowyResult<ExportDataPB, FlowyError> successOrFail,
) = _Finish;
const factory DocumentShareState({
required bool isLoading,
FlowyResult<ExportDataPB, FlowyError>? exportResult,
required bool isPublished,
FlowyResult<void, FlowyError>? publishResult,
required String url,
}) = _DocumentShareState;
factory DocumentShareState.initial() => const DocumentShareState(
isLoading: false,
isPublished: false,
url: '',
);
}
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:avatar_stack/avatar_stack.dart';
import 'package:avatar_stack/positions.dart';
import 'package:flutter/material.dart';
class CollaboratorAvatarStack extends StatelessWidget {
const CollaboratorAvatarStack({
@@ -31,14 +30,14 @@ class CollaboratorAvatarStack extends StatelessWidget {
Widget build(BuildContext context) {
final settings = this.settings ??
RestrictedPositions(
maxCoverage: 0.3,
minCoverage: 0.2,
maxCoverage: 0.4,
minCoverage: 0.3,
align: StackAlign.right,
laying: StackLaying.first,
);
final border = BorderSide(
color: borderColor ?? Theme.of(context).colorScheme.onPrimary,
color: borderColor ?? Theme.of(context).dividerColor,
width: borderWidth ?? 2.0,
);
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart';
import 'package:appflowy/plugins/document/application/document_collaborators_bloc.dart';
import 'package:appflowy/plugins/document/presentation/collaborator_avater_stack.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@@ -7,6 +8,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
class DocumentCollaborators extends StatelessWidget {
const DocumentCollaborators({
@@ -66,21 +68,11 @@ class DocumentCollaborators extends StatelessWidget {
),
);
},
avatars: collaborators
.map(
(c) => FlowyTooltip(
message: c.userName,
child: CircleAvatar(
backgroundColor: c.cursorColor.tryToColor(),
child: FlowyText(
c.userName.characters.firstOrNull ?? ' ',
fontSize: fontSize,
color: Colors.black,
),
),
),
)
.toList(),
avatars: [
...collaborators.map(
(c) => _UserAvatar(fontSize: fontSize, user: c, width: width),
),
],
),
);
},
@@ -88,3 +80,55 @@ class DocumentCollaborators extends StatelessWidget {
);
}
}
class _UserAvatar extends StatelessWidget {
const _UserAvatar({
this.fontSize,
required this.user,
required this.width,
});
final DocumentAwarenessMetadata user;
final double? fontSize;
final double width;
@override
Widget build(BuildContext context) {
final Widget child;
if (isURL(user.userAvatar)) {
child = _buildUrlAvatar(context);
} else {
child = _buildNameAvatar(context);
}
return FlowyTooltip(
message: user.userName,
child: child,
);
}
Widget _buildNameAvatar(BuildContext context) {
return CircleAvatar(
backgroundColor: user.cursorColor.tryToColor(),
child: FlowyText(
user.userName.characters.firstOrNull ?? ' ',
fontSize: fontSize,
color: Colors.black,
),
);
}
Widget _buildUrlAvatar(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(width),
child: CircleAvatar(
backgroundColor: user.cursorColor.tryToColor(),
child: Image.network(
user.userAvatar,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_buildNameAvatar(context),
),
),
);
}
}
@@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
@@ -5,15 +8,11 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mo
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/video/video_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/video/video_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Map<String, BlockComponentBuilder> getEditorBuilderMap({
@@ -231,16 +230,6 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
configuration: configuration,
),
VideoBlockKeys.type: VideoBlockComponentBuilder(
configuration: configuration,
showMenu: true,
menuBuilder: (node, state) => Positioned(
top: 10,
right: 10,
child: VideoMenu(node: node, state: state),
),
placeholderBuilder: (node) => VideoPlaceholder(node: node),
),
};
final builders = {
@@ -1,5 +1,8 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
@@ -28,23 +31,21 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
final codeBlockLocalization = CodeBlockLocalizations(
codeBlockNewParagraph:
LocaleKeys.settings_shortcuts_commands_codeBlockNewParagraph.tr(),
LocaleKeys.settings_shortcutsPage_commands_codeBlockNewParagraph.tr(),
codeBlockIndentLines:
LocaleKeys.settings_shortcuts_commands_codeBlockIndentLines.tr(),
LocaleKeys.settings_shortcutsPage_commands_codeBlockIndentLines.tr(),
codeBlockOutdentLines:
LocaleKeys.settings_shortcuts_commands_codeBlockOutdentLines.tr(),
LocaleKeys.settings_shortcutsPage_commands_codeBlockOutdentLines.tr(),
codeBlockSelectAll:
LocaleKeys.settings_shortcuts_commands_codeBlockSelectAll.tr(),
LocaleKeys.settings_shortcutsPage_commands_codeBlockSelectAll.tr(),
codeBlockPasteText:
LocaleKeys.settings_shortcuts_commands_codeBlockPasteText.tr(),
LocaleKeys.settings_shortcutsPage_commands_codeBlockPasteText.tr(),
codeBlockAddTwoSpaces:
LocaleKeys.settings_shortcuts_commands_codeBlockAddTwoSpaces.tr(),
LocaleKeys.settings_shortcutsPage_commands_codeBlockAddTwoSpaces.tr(),
);
final localizedCodeBlockCommands =
@@ -411,7 +412,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
emojiMenuItem,
autoGeneratorMenuItem,
dateMenuItem,
videoBlockItem(LocaleKeys.document_plugins_video_label.tr()),
];
}
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
final List<CommandShortcutEvent> customTextAlignCommands = [
customTextLeftAlignCommand,
@@ -21,7 +22,7 @@ final List<CommandShortcutEvent> customTextAlignCommands = [
final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent(
key: 'Align text to the left',
command: 'ctrl+shift+l',
getDescription: LocaleKeys.settings_shortcuts_commands_textAlignLeft.tr,
getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignLeft.tr,
handler: (editorState) => _textAlignHandler(editorState, leftAlignmentKey),
);
@@ -36,7 +37,7 @@ final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent(
final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent(
key: 'Align text to the center',
command: 'ctrl+shift+e',
getDescription: LocaleKeys.settings_shortcuts_commands_textAlignCenter.tr,
getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignCenter.tr,
handler: (editorState) => _textAlignHandler(editorState, centerAlignmentKey),
);
@@ -51,7 +52,7 @@ final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent(
final CommandShortcutEvent customTextRightAlignCommand = CommandShortcutEvent(
key: 'Align text to the right',
command: 'ctrl+shift+r',
getDescription: LocaleKeys.settings_shortcuts_commands_textAlignRight.tr,
getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignRight.tr,
handler: (editorState) => _textAlignHandler(editorState, rightAlignmentKey),
);
@@ -53,13 +53,6 @@ extension PasteFromImage on EditorState {
await File(copyToPath).writeAsBytes(imageBytes);
final String? path;
if (context.mounted) {
showSnackBarMessage(
context,
LocaleKeys.document_imageBlock_imageIsUploading.tr(),
);
}
if (isLocalMode) {
path = await saveImageToLocalStorage(copyToPath);
} else {
@@ -49,9 +49,11 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
);
}
final documentService = DocumentService();
Log.debug("Uploading image local path: $localImagePath");
final result = await documentService.uploadFile(
localFilePath: localImagePath,
isAsync: false,
// TODO(lucas): replace with actual documentId
documentId: "temp",
);
return result.fold(
(s) async {
@@ -197,15 +197,6 @@ class _AddBlockMenu extends StatelessWidget {
},
),
// video
TypeOptionMenuItemValue(
value: VideoBlockKeys.type,
backgroundColor: colorMap[VideoBlockKeys.type]!,
text: LocaleKeys.document_plugins_video_label.tr(),
icon: FlowySvgs.m_add_block_video_s,
onTap: (_, __) => _insertBlock(videoBlockNode()),
),
// date
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
@@ -296,7 +287,6 @@ class _AddBlockMenu extends StatelessWidget {
NumberedListBlockKeys.type: const Color(0xFFA35F94),
ToggleListBlockKeys.type: const Color(0xFFA35F94),
ImageBlockKeys.type: const Color(0xFFBAAC74),
VideoBlockKeys.type: const Color(0xFFBAAC74),
MentionBlockKeys.type: const Color(0xFF40AAB8),
DividerBlockKeys.type: const Color(0xFF4BB299),
CalloutBlockKeys.type: const Color(0xFF66599B),
@@ -313,7 +303,6 @@ class _AddBlockMenu extends StatelessWidget {
NumberedListBlockKeys.type: const Color(0xFFFFB9EF),
ToggleListBlockKeys.type: const Color(0xFFFFB9EF),
ImageBlockKeys.type: const Color(0xFFFDEDA7),
VideoBlockKeys.type: const Color(0xFFFDEDA7),
MentionBlockKeys.type: const Color(0xFF91EAF5),
DividerBlockKeys.type: const Color(0xFF98F4CD),
CalloutBlockKeys.type: const Color(0xFFCABDFF),
@@ -73,8 +73,7 @@ class _AppFlowyMobileToolbarState extends State<AppFlowyMobileToolbar> {
ValueListenableBuilder(
valueListenable: isKeyboardShow,
builder: (context, isKeyboardShow, __) {
return AnimatedContainer(
duration: const Duration(milliseconds: 110),
return SizedBox(
// only adding padding when the keyboard is triggered by editor
height: isKeyboardShow && widget.editorState.selection != null
? widget.toolbarHeight
@@ -383,30 +382,26 @@ class _MobileToolbarState extends State<_MobileToolbar>
return ValueListenableBuilder(
valueListenable: cachedKeyboardHeight,
builder: (_, height, ___) {
var paddingHeight = height;
if (Platform.isAndroid) {
paddingHeight += MediaQuery.of(context).viewPadding.bottom;
}
return AnimatedContainer(
duration: const Duration(microseconds: 110),
height: paddingHeight,
child: ValueListenableBuilder(
valueListenable: showMenuNotifier,
builder: (_, showingMenu, __) {
return AnimatedContainer(
duration: const Duration(microseconds: 110),
height: height,
child: (showingMenu && selectedMenuIndex != null)
? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(
context,
widget.editorState,
this,
) ??
const SizedBox.shrink()
: const SizedBox.shrink(),
);
},
),
return ValueListenableBuilder(
valueListenable: showMenuNotifier,
builder: (_, showingMenu, __) {
var paddingHeight = height;
if (Platform.isAndroid) {
paddingHeight =
height + MediaQuery.of(context).viewPadding.bottom;
}
return SizedBox(
height: paddingHeight,
child: (showingMenu && selectedMenuIndex != null)
? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(
context,
widget.editorState,
this,
) ??
const SizedBox.shrink()
: const SizedBox.shrink(),
);
},
);
},
);
@@ -1,4 +1,5 @@
import 'package:appflowy/startup/tasks/prelude.dart';
import 'dart:io';
import 'package:keyboard_height_plugin/keyboard_height_plugin.dart';
typedef KeyboardHeightCallback = void Function(double height);
@@ -9,8 +10,6 @@ class KeyboardHeightObserver {
KeyboardHeightObserver._() {
_keyboardHeightPlugin.onKeyboardHeightChanged((height) {
notify(height);
currentKeyboardHeight = height;
});
}
@@ -34,14 +33,13 @@ class KeyboardHeightObserver {
}
void notify(double height) {
// the keyboard height will notify twice with the same value on Android 14
if (ApplicationInfo.androidSDKVersion == 34) {
if (height == 0 && currentKeyboardHeight == 0) {
return;
}
// the keyboard height will notify twice with the same value on Android
if (Platform.isAndroid && height == currentKeyboardHeight) {
return;
}
for (final listener in _listeners) {
listener(height);
}
currentKeyboardHeight = height;
}
}
@@ -1,102 +0,0 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class UploadVideoMenu extends StatefulWidget {
const UploadVideoMenu({
super.key,
required this.onUrlSubmitted,
this.onSelectedColor,
});
final void Function(String url) onUrlSubmitted;
final void Function(String color)? onSelectedColor;
@override
State<UploadVideoMenu> createState() => _UploadVideoMenuState();
}
class _UploadVideoMenuState extends State<UploadVideoMenu> {
@override
Widget build(BuildContext context) {
final constraints =
PlatformExtension.isMobile ? const BoxConstraints(minHeight: 92) : null;
return Container(
padding: const EdgeInsets.all(8.0),
constraints: constraints,
child: _EmbedUrl(onSubmit: widget.onUrlSubmitted),
);
}
}
class _EmbedUrl extends StatefulWidget {
const _EmbedUrl({required this.onSubmit});
final void Function(String url) onSubmit;
@override
State<_EmbedUrl> createState() => _EmbedUrlState();
}
class _EmbedUrlState extends State<_EmbedUrl> {
bool isUrlValid = true;
bool isYouTubeError = false;
String inputText = '';
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
FlowyTextField(
hintText: LocaleKeys.document_plugins_video_placeholder.tr(),
onChanged: (value) => inputText = value,
onEditingComplete: submit,
),
if (!isUrlValid) ...[
const VSpace(8),
FlowyText(
isYouTubeError
? LocaleKeys.document_plugins_video_invalidVideoUrlYouTube.tr()
: LocaleKeys.document_plugins_video_invalidVideoUrl.tr(),
color: Theme.of(context).colorScheme.error,
),
],
const VSpace(8),
FlowyText(
LocaleKeys.document_plugins_video_supportedFormats.tr(),
color: Theme.of(context).hintColor,
),
const VSpace(8),
SizedBox(
width: 160,
child: FlowyButton(
showDefaultBoxDecorationOnMobile: true,
margin: const EdgeInsets.all(8.0),
text: FlowyText(
LocaleKeys.document_plugins_video_insertVideo.tr(),
textAlign: TextAlign.center,
),
onTap: submit,
),
),
],
);
}
void submit() {
if (checkUrlValidity(inputText)) {
return widget.onSubmit(inputText);
}
isYouTubeError = youtubeUrlRegex.hasMatch(inputText);
setState(() => isUrlValid = false);
}
bool checkUrlValidity(String url) => videoUrlRegex.hasMatch(url);
}
@@ -1,314 +0,0 @@
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/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class VideoMenu extends StatefulWidget {
const VideoMenu({
super.key,
required this.node,
required this.state,
});
final Node node;
final VideoBlockComponentState state;
@override
State<VideoMenu> createState() => _VideoMenuState();
}
class _VideoMenuState extends State<VideoMenu> {
late final String? url = widget.node.attributes[VideoBlockKeys.url];
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
height: 32,
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(4.0),
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withOpacity(0.1),
),
],
),
child: PlatformExtension.isMobile
? MenuBlockButton(
tooltip: LocaleKeys.button_edit.tr(),
iconData: FlowySvgs.more_s,
onTap: showMobileMenu,
)
: Row(
children: [
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.editor_copyLink.tr(),
iconData: FlowySvgs.copy_s,
onTap: copyVideoLink,
),
const HSpace(4),
_VideoAlignButton(
node: widget.node,
state: widget.state,
),
const _Divider(),
MenuBlockButton(
tooltip: LocaleKeys.button_delete.tr(),
iconData: FlowySvgs.delete_s,
onTap: deleteVideo,
),
const HSpace(4),
],
),
);
}
void copyVideoLink() {
if (url != null) {
Clipboard.setData(ClipboardData(text: url!));
showSnackBarMessage(
context,
LocaleKeys.document_plugins_video_copiedToPasteBoard.tr(),
);
}
}
void showMobileMenu() {
final editorState = context.read<EditorState>()
..updateSelectionWithReason(null, extraInfo: {});
final src = widget.node.attributes[VideoBlockKeys.url];
showMobileBottomSheet(
context,
showHeader: true,
showCloseButton: true,
showDragHandle: true,
title: LocaleKeys.document_plugins_action.tr(),
builder: (context) {
return BlockActionBottomSheet(
extendActionWidgets: [
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.editor_copyLink.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_field_copy_s,
),
onTap: () async {
context.pop();
showSnackBarMessage(
context,
LocaleKeys.document_plugins_video_copiedToPasteBoard.tr(),
);
await getIt<ClipboardService>().setPlainText(src);
},
),
],
onAction: (action) async {
context.pop();
final transaction = editorState.transaction;
switch (action) {
case BlockActionBottomSheetType.delete:
transaction.deleteNode(widget.node);
break;
case BlockActionBottomSheetType.duplicate:
transaction.insertNode(
widget.node.path.next,
widget.node.copyWith(),
);
break;
case BlockActionBottomSheetType.insertAbove:
case BlockActionBottomSheetType.insertBelow:
final path = action == BlockActionBottomSheetType.insertAbove
? widget.node.path
: widget.node.path.next;
transaction
..insertNode(path, paragraphNode())
..afterSelection = Selection.collapsed(Position(path: path));
break;
default:
}
if (transaction.operations.isNotEmpty) {
await editorState.apply(transaction);
}
},
);
},
);
}
Future<void> deleteVideo() async {
final node = widget.node;
final editorState = context.read<EditorState>();
final transaction = editorState.transaction;
transaction.deleteNode(node);
transaction.afterSelection = null;
await editorState.apply(transaction);
}
}
class _VideoAlignButton extends StatefulWidget {
const _VideoAlignButton({
required this.node,
required this.state,
});
final Node node;
final VideoBlockComponentState state;
@override
State<_VideoAlignButton> createState() => _VideoAlignButtonState();
}
const interceptorKey = 'video-align';
class _VideoAlignButtonState extends State<_VideoAlignButton> {
final gestureInterceptor = SelectionGestureInterceptor(
key: interceptorKey,
canTap: (_) => false,
);
String get align =>
widget.node.attributes[VideoBlockKeys.alignment] ?? centerAlignmentKey;
final popoverController = PopoverController();
late final EditorState editorState;
@override
void initState() {
super.initState();
editorState = context.read<EditorState>();
}
@override
void dispose() {
allowMenuClose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return IgnoreParentGestureWidget(
child: AppFlowyPopover(
onClose: allowMenuClose,
controller: popoverController,
windowPadding: const EdgeInsets.all(0),
margin: const EdgeInsets.all(0),
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 10),
child: MenuBlockButton(
tooltip: LocaleKeys.document_plugins_optionAction_align.tr(),
iconData: iconFor(align),
),
popupBuilder: (_) {
preventMenuClose();
return _AlignButtons(onAlignChanged: onAlignChanged);
},
),
);
}
void onAlignChanged(String align) {
popoverController.close();
final transaction = editorState.transaction;
transaction.updateNode(widget.node, {VideoBlockKeys.alignment: align});
editorState.apply(transaction);
allowMenuClose();
}
void preventMenuClose() {
widget.state.preventClose = true;
editorState.service.selectionService
.registerGestureInterceptor(gestureInterceptor);
}
void allowMenuClose() {
widget.state.preventClose = false;
editorState.service.selectionService
.unregisterGestureInterceptor(interceptorKey);
}
FlowySvgData iconFor(String alignment) {
switch (alignment) {
case leftAlignmentKey:
return FlowySvgs.align_left_s;
case rightAlignmentKey:
return FlowySvgs.align_right_s;
case centerAlignmentKey:
default:
return FlowySvgs.align_center_s;
}
}
}
class _AlignButtons extends StatelessWidget {
const _AlignButtons({required this.onAlignChanged});
final Function(String align) onAlignChanged;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.document_plugins_optionAction_left,
iconData: FlowySvgs.align_left_s,
onTap: () => onAlignChanged(leftAlignmentKey),
),
const _Divider(),
MenuBlockButton(
tooltip: LocaleKeys.document_plugins_optionAction_center,
iconData: FlowySvgs.align_center_s,
onTap: () => onAlignChanged(centerAlignmentKey),
),
const _Divider(),
MenuBlockButton(
tooltip: LocaleKeys.document_plugins_optionAction_right,
iconData: FlowySvgs.align_right_s,
onTap: () => onAlignChanged(rightAlignmentKey),
),
const HSpace(4),
],
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Container(width: 1, color: Colors.grey),
);
}
}
@@ -1,136 +0,0 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/video/upload_video_menu.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log, UploadImageMenu;
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:string_validator/string_validator.dart';
class VideoPlaceholder extends StatefulWidget {
const VideoPlaceholder({super.key, required this.node});
final Node node;
@override
State<VideoPlaceholder> createState() => VideoPlaceholderState();
}
class VideoPlaceholderState extends State<VideoPlaceholder> {
final controller = PopoverController();
final documentService = DocumentService();
late final editorState = context.read<EditorState>();
bool showLoading = false;
@override
Widget build(BuildContext context) {
final Widget child = DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: FlowyHover(
style: HoverStyle(borderRadius: BorderRadius.circular(4)),
child: SizedBox(
height: 52,
child: Row(
children: [
const HSpace(10),
const Icon(Icons.featured_video_outlined, size: 24),
const HSpace(10),
FlowyText(LocaleKeys.document_plugins_video_emptyLabel.tr()),
],
),
),
),
);
if (PlatformExtension.isDesktopOrWeb) {
return AppFlowyPopover(
controller: controller,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: const BoxConstraints(
maxWidth: 540,
maxHeight: 360,
minHeight: 80,
),
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (_) => UploadVideoMenu(
onUrlSubmitted: (url) {
controller.close();
WidgetsBinding.instance.addPostFrameCallback(
(_) async => updateSrc(url),
);
},
),
child: child,
);
} else {
return MobileBlockActionButtons(
node: widget.node,
editorState: editorState,
child: GestureDetector(
onTap: () {
editorState.updateSelectionWithReason(null, extraInfo: {});
showUploadVideoMenu();
},
child: child,
),
);
}
}
void showUploadVideoMenu() {
if (PlatformExtension.isDesktopOrWeb) {
controller.show();
} else {
showMobileBottomSheet(
context,
title: LocaleKeys.document_plugins_video_label.tr(),
showHeader: true,
showCloseButton: true,
showDragHandle: true,
builder: (context) => Container(
margin: const EdgeInsets.only(top: 12.0),
constraints: const BoxConstraints(
maxHeight: 340,
minHeight: 80,
),
child: UploadVideoMenu(
onUrlSubmitted: (url) async {
context.pop();
await updateSrc(url);
},
),
),
);
}
}
Future<void> updateSrc(String url) async {
if (url.isEmpty || !isURL(url)) {
// show error
showSnackBarMessage(
context,
LocaleKeys.document_imageBlock_error_invalidImage.tr(),
);
return;
}
final transaction = editorState.transaction;
transaction.updateNode(widget.node, {
VideoBlockKeys.url: url,
});
await editorState.apply(transaction);
}
}
@@ -58,8 +58,9 @@ class EditorStyleCustomizer {
DefaultAppearanceSettings.getDefaultSelectionColor(context),
defaultTextDirection: appearance.defaultTextDirection,
textStyleConfiguration: TextStyleConfiguration(
// applyHeightToFirstAscent: true,
// applyHeightToLastDescent: true,
lineHeight: 1.4,
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
text: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
color: afThemeExtension.onBackground,
@@ -210,7 +211,7 @@ class EditorStyleCustomizer {
selectionMenuItemIconColor: afThemeExtension.onBackground,
selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface,
selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface,
selectionMenuItemSelectedColor: theme.hoverColor,
selectionMenuItemSelectedColor: afThemeExtension.greyHover,
);
}
@@ -0,0 +1,246 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PublishTab extends StatelessWidget {
const PublishTab({super.key});
@override
Widget build(BuildContext context) {
return BlocConsumer<DocumentShareBloc, DocumentShareState>(
listener: (context, state) {
if (state.publishResult != null) {
state.publishResult!.fold(
(value) => showSnackBarMessage(context, 'Published successfully'),
(error) =>
showSnackBarMessage(context, 'Failed to publish: $error'),
);
}
},
builder: (context, state) {
return state.isPublished
? _PublishedWidget(
url: state.url,
onVisitSite: () {},
onUnPublish: () {
context
.read<DocumentShareBloc>()
.add(const DocumentShareEvent.unPublish());
},
)
: _UnPublishWidget(
onPublish: () => context
.read<DocumentShareBloc>()
.add(const DocumentShareEvent.publish('')),
);
},
);
}
}
class _PublishedWidget extends StatefulWidget {
const _PublishedWidget({
required this.url,
required this.onVisitSite,
required this.onUnPublish,
});
final String url;
final VoidCallback onVisitSite;
final VoidCallback onUnPublish;
@override
State<_PublishedWidget> createState() => _PublishedWidgetState();
}
class _PublishedWidgetState extends State<_PublishedWidget> {
final controller = TextEditingController();
@override
void initState() {
super.initState();
controller.text = widget.url;
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(16),
const _PublishTabHeader(),
const VSpace(16),
_PublishUrl(
controller: controller,
onCopy: (url) {},
onSubmitted: (url) {},
),
const VSpace(16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildButton(
context,
name: LocaleKeys.shareAction_unPublish.tr(),
borderColor: const Color(0x1E14171B),
onTap: widget.onUnPublish,
),
const Spacer(),
_buildButton(
context,
name: LocaleKeys.shareAction_visitSite.tr(),
backgroundColor: Theme.of(context).colorScheme.primary,
textColor: Colors.white,
onTap: () {
safeLaunchUrl(controller.text);
},
),
],
),
],
);
}
Widget _buildButton(
BuildContext context, {
required String name,
Color? backgroundColor,
Color borderColor = Colors.transparent,
Color? textColor,
required VoidCallback onTap,
}) {
return SizedBox(
height: 36,
width: 189,
child: FlowyButton(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: borderColor),
),
text: FlowyText.regular(
name,
textAlign: TextAlign.center,
color: textColor,
),
onTap: onTap,
),
);
}
}
class _UnPublishWidget extends StatelessWidget {
const _UnPublishWidget({
required this.onPublish,
});
final VoidCallback onPublish;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(16),
const _PublishTabHeader(),
const VSpace(16),
RoundedTextButton(
height: 36,
title: LocaleKeys.shareAction_publish.tr(),
padding: const EdgeInsets.symmetric(vertical: 9.0),
fontSize: 14.0,
textColor: Theme.of(context).colorScheme.onPrimary,
onPressed: onPublish,
),
],
);
}
}
class _PublishTabHeader extends StatelessWidget {
const _PublishTabHeader();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const FlowySvg(FlowySvgs.share_publish_s),
const HSpace(6),
FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()),
],
),
const VSpace(4),
FlowyText.regular(
LocaleKeys.shareAction_publishToTheWebHint.tr(),
fontSize: 12,
maxLines: 3,
color: Theme.of(context).hintColor,
),
],
);
}
}
class _PublishUrl extends StatelessWidget {
const _PublishUrl({
required this.controller,
required this.onCopy,
required this.onSubmitted,
});
final TextEditingController controller;
final void Function(String url) onCopy;
final void Function(String url) onSubmitted;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 36,
child: FlowyTextField(
autoFocus: false,
controller: controller,
suffixIcon: _buildCopyLinkIcon(),
),
);
}
Widget _buildCopyLinkIcon() {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => onCopy(controller.text),
child: Container(
width: 36,
height: 36,
alignment: Alignment.center,
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(
border: Border(left: BorderSide(color: Color(0x141F2329))),
),
child: const FlowySvg(
FlowySvgs.m_toolbar_link_m,
),
),
),
);
}
}
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/share/share_menu.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/export/document_exporter.dart';
@@ -13,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -28,23 +30,44 @@ class DocumentShareButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<DocumentShareBloc>(param1: view),
create: (context) => getIt<DocumentShareBloc>(param1: view)
..add(const DocumentShareEvent.initial()),
child: BlocListener<DocumentShareBloc, DocumentShareState>(
listener: (context, state) {
state.mapOrNull(
finish: (state) {
state.successOrFail.fold(
(data) => _handleExportData(context, data),
_handleExportError,
);
},
);
if (state.isLoading == false && state.exportResult != null) {
state.exportResult!.fold(
(data) => _handleExportData(context, data),
_handleExportError,
);
}
},
child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
builder: (context, state) => SizedBox(
height: 32.0,
child: IntrinsicWidth(child: ShareActionList(view: view)),
),
builder: (context, state) {
final shareBloc = context.read<DocumentShareBloc>();
return SizedBox(
height: 32.0,
child: IntrinsicWidth(
child: AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned,
constraints: const BoxConstraints(
maxWidth: 422,
),
margin: const EdgeInsets.all(16),
offset: const Offset(0, 8),
popupBuilder: (context) => BlocProvider.value(
value: shareBloc,
child: const ShareMenu(),
),
child: RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
fontSize: 14.0,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
),
);
},
),
),
);
@@ -0,0 +1,121 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'publish_tab.dart';
enum ShareMenuTab {
share,
publish,
exportAs;
static List<ShareMenuTab> supportedTabs = [
ShareMenuTab.share,
ShareMenuTab.publish,
ShareMenuTab.exportAs,
];
String get i18n {
switch (this) {
case ShareMenuTab.share:
return 'Share';
case ShareMenuTab.publish:
return LocaleKeys.shareAction_publish;
case ShareMenuTab.exportAs:
return 'Export as';
}
}
}
class ShareMenu extends StatefulWidget {
const ShareMenu({super.key});
@override
State<ShareMenu> createState() => _ShareMenuState();
}
class _ShareMenuState extends State<ShareMenu> {
ShareMenuTab selectedTab = ShareMenuTab.publish;
@override
Widget build(BuildContext context) {
final children = {
for (final tab in ShareMenuTab.supportedTabs)
tab: _Segment(
title: tab.i18n.tr(),
isSelected: selectedTab == tab,
),
};
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 34,
child: CustomSlidingSegmentedControl<ShareMenuTab>(
initialValue: selectedTab,
curve: Curves.linear,
padding: 0,
fixedWidth: 128,
innerPadding: const EdgeInsets.all(3.0),
children: children,
decoration: BoxDecoration(
color: const Color(0xFFEEF0F3),
borderRadius: BorderRadius.circular(8),
),
thumbDecoration: BoxDecoration(
color: Colors.white,
boxShadow: const [
BoxShadow(
color: Color(0x141F2225),
blurRadius: 8,
offset: Offset(0, 2),
),
],
borderRadius: BorderRadius.circular(8),
),
onValueChanged: (v) {
setState(() {
selectedTab = v;
});
},
),
),
_buildTab(context),
],
);
}
Widget _buildTab(BuildContext context) {
switch (selectedTab) {
case ShareMenuTab.publish:
return const PublishTab();
default:
return const Center(
child: FlowyText('coming soon'),
);
}
}
}
class _Segment extends StatelessWidget {
const _Segment({
required this.title,
required this.isSelected,
});
final String title;
final bool isSelected;
@override
Widget build(BuildContext context) {
final textColor = isSelected ? null : Theme.of(context).hintColor;
return FlowyText(
title,
textAlign: TextAlign.center,
color: textColor,
);
}
}
@@ -1,10 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
@@ -22,8 +24,6 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -164,9 +164,6 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
),
BlocProvider.value(value: getIt<RenameViewBloc>()),
BlocProvider.value(value: getIt<ActionNavigationBloc>()),
BlocProvider.value(
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
),
],
child: BlocListener<ActionNavigationBloc, ActionNavigationState>(
listenWhen: (_, curr) => curr.action != null,
@@ -64,11 +64,6 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
await windowManager.show();
await windowManager.focus();
if (PlatformExtension.isWindows) {
// Hide title bar on Windows, we implement a custom solution elsewhere
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
if (position != null) {
await windowManager.setPosition(position);
}
@@ -9,7 +9,15 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart';
class UserBackendService {
abstract class IUserBackendService {
Future<FlowyResult<void, FlowyError>> cancelSubscription(String workspaceId);
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
String workspaceId,
SubscriptionPlanPB plan,
);
}
class UserBackendService implements IUserBackendService {
UserBackendService({required this.userId});
final Int64 userId;
@@ -164,7 +172,7 @@ class UserBackendService {
String workspaceId,
) async {
final data = QueryWorkspacePB()..workspaceId = workspaceId;
return UserEventGetWorkspaceMember(data).send();
return UserEventGetWorkspaceMembers(data).send();
}
Future<FlowyResult<void, FlowyError>> addWorkspaceMember(
@@ -225,20 +233,29 @@ class UserBackendService {
return UserEventGetWorkspaceSubscriptions().send();
}
static Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
getWorkspaceMember() async {
final data = WorkspaceMemberIdPB.create()..uid = userId;
return UserEventGetMemberInfo(data).send();
}
@override
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
String workspaceId,
SubscriptionPlanPB plan,
) {
final request = SubscribeWorkspacePB()
..workspaceId = workspaceId
..recurringInterval = RecurringIntervalPB.Month
..recurringInterval = RecurringIntervalPB.Year
..workspaceSubscriptionPlan = plan
..successUrl =
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success';
return UserEventSubscribeWorkspace(request).send();
}
static Future<FlowyResult<void, FlowyError>> cancelSubscription(
@override
Future<FlowyResult<void, FlowyError>> cancelSubscription(
String workspaceId,
) {
final request = UserWorkspaceIdPB()..workspaceId = workspaceId;
@@ -156,7 +156,7 @@ class CommandPaletteBloc
add(CommandPaletteEvent.performSearch(search: value));
void _onResultsChanged(RepeatedSearchResultPB results) =>
add(CommandPaletteEvent.resultsChanged(results: results, max: 2));
add(CommandPaletteEvent.resultsChanged(results: results));
}
@freezed
@@ -10,7 +10,19 @@ import 'package:collection/collection.dart';
class FavoriteService {
Future<FlowyResult<RepeatedFavoriteViewPB, FlowyError>> readFavorites() {
return FolderEventReadFavorites().send();
final result = FolderEventReadFavorites().send();
return result.then((result) {
return result.fold(
(favoriteViews) {
return FlowyResult.success(
RepeatedFavoriteViewPB(
items: favoriteViews.items.where((e) => !e.item.isSpace),
),
);
},
(error) => FlowyResult.failure(error),
);
});
}
Future<FlowyResult<void, FlowyError>> toggleFavorite(
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/recent_listener.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@@ -65,8 +66,20 @@ class CachedRecentService {
).send();
}
Future<FlowyResult<RepeatedRecentViewPB, FlowyError>> _readRecentViews() =>
FolderEventReadRecentViews().send();
Future<FlowyResult<RepeatedRecentViewPB, FlowyError>>
_readRecentViews() async {
final result = await FolderEventReadRecentViews().send();
return result.fold(
(recentViews) {
return FlowyResult.success(
RepeatedRecentViewPB(
items: recentViews.items.where((e) => !e.item.isSpace),
),
);
},
(error) => FlowyResult.failure(error),
);
}
bool _isInitialized = false;
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
class DesktopAppearance extends BaseAppearance {
@override
@@ -76,7 +77,7 @@ class DesktopAppearance extends BaseAppearance {
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.any(scrollbarInteractiveStates.contains)) {
return theme.shader7;
return theme.shader3;
}
return theme.shader5;
}),
@@ -102,6 +103,7 @@ class DesktopAppearance extends BaseAppearance {
indicatorColor: theme.main1,
cardColor: theme.input,
colorScheme: colorScheme,
extensions: [
AFThemeExtension(
warning: theme.yellow,
@@ -117,6 +119,7 @@ class DesktopAppearance extends BaseAppearance {
tint9: theme.tint9,
textColor: theme.text,
secondaryTextColor: theme.secondaryText,
strongText: theme.strongText,
greyHover: theme.hoverBG1,
greySelect: theme.bg3,
lightGreyHover: theme.hoverBG3,
@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
// ThemeData in mobile
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
class MobileAppearance extends BaseAppearance {
static const _primaryColor = Color(0xFF00BCF0); //primary 100
@@ -250,6 +251,7 @@ class MobileAppearance extends BaseAppearance {
tint9: theme.tint9,
textColor: theme.text,
secondaryTextColor: theme.secondaryText,
strongText: theme.strongText,
greyHover: theme.hoverBG1,
greySelect: theme.bg3,
lightGreyHover: theme.hoverBG3,
@@ -13,6 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:fixnum/fixnum.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'settings_plan_bloc.freezed.dart';
@@ -20,8 +21,10 @@ part 'settings_plan_bloc.freezed.dart';
class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
SettingsPlanBloc({
required this.workspaceId,
required Int64 userId,
}) : super(const _Initial()) {
_service = WorkspaceService(workspaceId: workspaceId);
_userService = UserBackendService(userId: userId);
_successListenable = getIt<SubscriptionSuccessListenable>();
_successListenable.addListener(_onPaymentSuccessful);
@@ -103,7 +106,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
}
},
addSubscription: (plan) async {
final result = await UserBackendService.createSubscription(
final result = await _userService.createSubscription(
workspaceId,
SubscriptionPlanPB.Pro,
);
@@ -114,7 +117,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
);
},
cancelSubscription: () async {
await UserBackendService.cancelSubscription(workspaceId);
await _userService.cancelSubscription(workspaceId);
add(const SettingsPlanEvent.started());
},
paymentSuccessful: () {
@@ -131,6 +134,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
late final String workspaceId;
late final WorkspaceService _service;
late final IUserBackendService _userService;
late final SubscriptionSuccessListenable _successListenable;
void _onPaymentSuccessful() {
@@ -13,4 +13,14 @@ extension SubscriptionLabels on WorkspaceSubscriptionPB {
LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(),
_ => 'N/A',
};
String get info => switch (subscriptionPlan) {
SubscriptionPlanPB.None =>
LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(),
SubscriptionPlanPB.Pro =>
LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(),
SubscriptionPlanPB.Team =>
LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),
_ => 'N/A',
};
}
@@ -18,7 +18,27 @@ class ShortcutsState with _$ShortcutsState {
}) = _ShortcutsState;
}
enum ShortcutsStatus { initial, updating, success, failure }
enum ShortcutsStatus {
initial,
updating,
success,
failure;
/// Helper getter for when the [ShortcutsStatus] signifies
/// that the shortcuts have not been loaded yet.
///
bool get isLoading => [initial, updating].contains(this);
/// Helper getter for when the [ShortcutsStatus] signifies
/// a failure by itself being [ShortcutsStatus.failure]
///
bool get isFailure => this == ShortcutsStatus.failure;
/// Helper getter for when the [ShortcutsStatus] signifies
/// a success by itself being [ShortcutsStatus.success]
///
bool get isSuccess => this == ShortcutsStatus.success;
}
class ShortcutsCubit extends Cubit<ShortcutsState> {
ShortcutsCubit(this.service) : super(const ShortcutsState());
@@ -56,44 +76,31 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
emit(
state.copyWith(
status: ShortcutsStatus.failure,
error: LocaleKeys.settings_shortcuts_couldNotLoadErrorMsg.tr(),
error: LocaleKeys.settings_shortcutsPage_couldNotLoadErrorMsg.tr(),
),
);
}
}
Future<void> updateAllShortcuts() async {
emit(
state.copyWith(
status: ShortcutsStatus.updating,
error: '',
),
);
emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));
try {
await service.saveAllShortcuts(state.commandShortcutEvents);
emit(
state.copyWith(
status: ShortcutsStatus.success,
error: '',
),
);
emit(state.copyWith(status: ShortcutsStatus.success, error: ''));
} catch (e) {
emit(
state.copyWith(
status: ShortcutsStatus.failure,
error: LocaleKeys.settings_shortcuts_couldNotSaveErrorMsg.tr(),
error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(),
),
);
}
}
Future<void> resetToDefault() async {
emit(
state.copyWith(
status: ShortcutsStatus.updating,
error: '',
),
);
emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));
try {
await service.saveAllShortcuts(defaultCommandShortcutEvents);
await fetchShortcuts();
@@ -101,7 +108,7 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
emit(
state.copyWith(
status: ShortcutsStatus.failure,
error: LocaleKeys.settings_shortcuts_couldNotSaveErrorMsg.tr(),
error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(),
),
);
}
@@ -110,16 +117,20 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
/// Checks if the new command is conflicting with other shortcut
/// We also check using the key, whether this command is a codeblock
/// shortcut, if so we only check a conflict with other codeblock shortcut.
String getConflict(CommandShortcutEvent currentShortcut, String command) {
CommandShortcutEvent? getConflict(
CommandShortcutEvent currentShortcut,
String command,
) {
// check if currentShortcut is a codeblock shortcut.
final isCodeBlockCommand = currentShortcut.isCodeBlockCommand;
for (final e in state.commandShortcutEvents) {
if (e.command == command && e.isCodeBlockCommand == isCodeBlockCommand) {
return e.key;
return e;
}
}
return '';
return null;
}
}
@@ -51,16 +51,10 @@ class WorkspaceSettingsBloc
currentWorkspaceInList.workspaceId,
);
final role = members
.firstWhereOrNull((e) => e.email == userProfile.email)
?.role ??
AFRolePB.Guest;
emit(
state.copyWith(
workspace: currentWorkspaceInList,
members: members,
myRole: role,
),
);
} catch (e) {
@@ -118,7 +112,7 @@ class WorkspaceSettingsBloc
String workspaceId,
) async {
final data = QueryWorkspacePB()..workspaceId = workspaceId;
final result = await UserEventGetWorkspaceMember(data).send();
final result = await UserEventGetWorkspaceMembers(data).send();
return result.fold(
(s) => s.items,
(e) {
@@ -150,7 +144,6 @@ class WorkspaceSettingsState with _$WorkspaceSettingsState {
const factory WorkspaceSettingsState({
@Default(null) UserWorkspacePB? workspace,
@Default([]) List<WorkspaceMemberPB> members,
@Default(AFRolePB.Guest) AFRolePB myRole,
@Default(false) bool deleteWorkspace,
@Default(false) bool leaveWorkspace,
}) = _WorkspaceSettingsState;
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -14,8 +15,11 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.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' hide Log;
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
@@ -60,7 +64,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
on<SpaceEvent>(
(event, emit) async {
await event.when(
initial: (userProfile, workspaceId) async {
initial: (userProfile, workspaceId, openFirstPage) async {
_initial(userProfile, workspaceId);
final (spaces, publicViews, privateViews) = await _getSpaces();
@@ -69,6 +73,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
publicViews: publicViews,
privateViews: privateViews,
);
final currentSpace = await _getLastOpenedSpace(spaces);
final isExpanded = await _getSpaceExpandStatus(currentSpace);
emit(
@@ -79,8 +84,26 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
shouldShowUpgradeDialog: shouldShowUpgradeDialog,
),
);
if (shouldShowUpgradeDialog) {
if (!integrationMode().isTest) {
add(const SpaceEvent.migrate());
}
}
if (openFirstPage) {
if (currentSpace != null) {
add(SpaceEvent.open(currentSpace));
}
}
},
create: (name, icon, iconColor, permission) async {
create: (
name,
icon,
iconColor,
permission,
createNewPageByDefault,
) async {
final space = await _createSpace(
name: name,
icon: icon,
@@ -88,8 +111,22 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
permission: permission,
);
if (space != null) {
emit(state.copyWith(spaces: [...state.spaces, space]));
emit(
state.copyWith(
spaces: [...state.spaces, space],
currentSpace: space,
),
);
add(SpaceEvent.open(space));
if (createNewPageByDefault) {
add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
),
);
}
}
},
delete: (space) async {
@@ -155,12 +192,31 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
await _openSpace(space);
final isExpanded = await _getSpaceExpandStatus(space);
emit(state.copyWith(currentSpace: space, isExpanded: isExpanded));
// don't open the page automatically on mobile
if (PlatformExtension.isDesktop) {
// open the first page by default
if (space.childViews.isNotEmpty) {
final firstPage = space.childViews.first;
emit(
state.copyWith(
lastCreatedPage: firstPage,
),
);
} else {
emit(
state.copyWith(
lastCreatedPage: ViewPB(),
),
);
}
}
},
expand: (space, isExpanded) async {
await _setSpaceExpandStatus(space, isExpanded);
emit(state.copyWith(isExpanded: isExpanded));
},
createPage: (name, section, index) async {
createPage: (name, index) async {
final parentViewId = state.currentSpace?.id;
if (parentViewId == null) {
return;
@@ -202,14 +258,39 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
);
},
reset: (userProfile, workspaceId) async {
if (workspaceId == _workspaceId) {
return;
}
_reset(userProfile, workspaceId);
add(SpaceEvent.initial(userProfile, workspaceId));
add(
SpaceEvent.initial(
userProfile,
workspaceId,
openFirstPage: true,
),
);
},
migrate: () async {
final result = await migrate();
emit(state.copyWith(shouldShowUpgradeDialog: !result));
},
switchToNextSpace: () async {
final spaces = state.spaces;
if (spaces.isEmpty) {
return;
}
final currentSpace = state.currentSpace;
if (currentSpace == null) {
return;
}
final currentIndex = spaces.indexOf(currentSpace);
final nextIndex = (currentIndex + 1) % spaces.length;
final nextSpace = spaces[nextIndex];
add(SpaceEvent.open(nextSpace));
},
);
},
);
@@ -245,6 +326,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
required String icon,
required String iconColor,
required SpacePermission permission,
String? viewId,
}) async {
final section = switch (permission) {
SpacePermission.publicToAll => ViewSectionPB.Public,
@@ -255,6 +337,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
name: name,
viewSection: section,
setAsCurrent: false,
viewId: viewId,
);
return await result.fold((space) async {
Log.info('Space created: $space');
@@ -378,7 +461,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
});
}
Future<bool> migrate() async {
Future<bool> migrate({bool auto = true}) async {
if (_workspaceId == null) {
return false;
}
@@ -391,20 +474,37 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
final isOwner = members.items
.any((e) => e.role == AFRolePB.Owner && e.email == user.email);
if (members.items.isEmpty) {
return true;
}
// only one member in the workspace, migrate it immediately
// only the owner can migrate the public space
if (members.items.length == 1 || isOwner) {
// create a new public space and a new private space
// move all the views in the workspace to the new public/private space
final publicViews =
await _workspaceService.getPublicViews().getOrThrow();
var publicViews = await _workspaceService.getPublicViews().getOrThrow();
final containsPublicSpace = publicViews.any(
(e) => e.isSpace && e.spacePermission == SpacePermission.publicToAll,
);
publicViews = publicViews.where((e) => !e.isSpace).toList();
// if there is already a public space, don't migrate the public space
// only migrate the public space if there are any public views
if (publicViews.isEmpty || containsPublicSpace) {
return true;
}
final viewId = fixedUuid(user.id.toInt(), UuidType.publicSpace);
final publicSpace = await _createSpace(
name: 'shared',
name: 'Shared',
icon: builtInSpaceIcons.first,
iconColor: builtInSpaceColors.first,
permission: SpacePermission.publicToAll,
viewId: viewId,
);
Log.info('migrating: created a new public space: ${publicSpace?.id}');
if (publicSpace != null) {
for (final view in publicViews.reversed) {
if (view.isSpace) {
@@ -413,20 +513,36 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
await ViewBackendService.moveViewV2(
viewId: view.id,
newParentId: publicSpace.id,
prevViewId: view.parentViewId,
prevViewId: null,
);
Log.info(
'migrating: migrate ${view.name}(${view.id}) to public space(${publicSpace.id})',
);
}
}
}
// create a new private space
final privateViews =
await _workspaceService.getPrivateViews().getOrThrow();
final viewId = fixedUuid(user.id.toInt(), UuidType.privateSpace);
var privateViews = await _workspaceService.getPrivateViews().getOrThrow();
// if there is already a private space, don't migrate the private space
final containsPrivateSpace = privateViews.any(
(e) => e.isSpace && e.spacePermission == SpacePermission.private,
);
privateViews = privateViews.where((e) => !e.isSpace).toList();
if (privateViews.isEmpty || containsPrivateSpace) {
return true;
}
// only migrate the private space if there are any private views
final privateSpace = await _createSpace(
name: 'private',
name: 'Private',
icon: builtInSpaceIcons.last,
iconColor: builtInSpaceColors.last,
permission: SpacePermission.private,
viewId: viewId,
);
Log.info('migrating: created a new private space: ${privateSpace?.id}');
if (privateSpace != null) {
for (final view in privateViews.reversed) {
if (view.isSpace) {
@@ -435,7 +551,10 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
await ViewBackendService.moveViewV2(
viewId: view.id,
newParentId: privateSpace.id,
prevViewId: view.parentViewId,
prevViewId: null,
);
Log.info(
'migrating: migrate ${view.name}(${view.id}) to private space(${privateSpace.id})',
);
}
}
@@ -452,14 +571,16 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
required List<ViewPB> publicViews,
required List<ViewPB> privateViews,
}) async {
final publicSpaces =
spaces.where((e) => e.spacePermission == SpacePermission.publicToAll);
final publicSpaces = spaces.where(
(e) => e.spacePermission == SpacePermission.publicToAll,
);
if (publicSpaces.isEmpty && publicViews.isNotEmpty) {
return true;
}
final privateSpaces =
spaces.where((e) => e.spacePermission == SpacePermission.private);
final privateSpaces = spaces.where(
(e) => e.spacePermission == SpacePermission.private,
);
if (privateSpaces.isEmpty && privateViews.isNotEmpty) {
return true;
}
@@ -472,13 +593,15 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
class SpaceEvent with _$SpaceEvent {
const factory SpaceEvent.initial(
UserProfilePB userProfile,
String workspaceId,
) = _Initial;
String workspaceId, {
required bool openFirstPage,
}) = _Initial;
const factory SpaceEvent.create({
required String name,
required String icon,
required String iconColor,
required SpacePermission permission,
required bool createNewPageByDefault,
}) = _Create;
const factory SpaceEvent.rename(ViewPB space, String name) = _Rename;
const factory SpaceEvent.changeIcon(String icon, String iconColor) =
@@ -493,7 +616,6 @@ class SpaceEvent with _$SpaceEvent {
const factory SpaceEvent.expand(ViewPB space, bool isExpanded) = _Expand;
const factory SpaceEvent.createPage({
required String name,
required ViewSectionPB viewSection,
int? index,
}) = _CreatePage;
const factory SpaceEvent.delete(ViewPB? space) = _Delete;
@@ -503,6 +625,7 @@ class SpaceEvent with _$SpaceEvent {
String workspaceId,
) = _Reset;
const factory SpaceEvent.migrate() = _Migrate;
const factory SpaceEvent.switchToNextSpace() = _SwitchToNextSpace;
}
@freezed
@@ -1,3 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/user/application/user_listener.dart';
@@ -10,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
@@ -54,11 +55,21 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
Log.info('init open workspace: ${currentWorkspace.workspaceId}');
await _userService.openWorkspace(currentWorkspace.workspaceId);
}
WorkspaceMemberPB? currentWorkspaceMember;
final workspaceMemberResult =
await _userService.getWorkspaceMember();
currentWorkspaceMember = workspaceMemberResult.fold(
(s) => s,
(e) => null,
);
emit(
state.copyWith(
currentWorkspace: currentWorkspace,
workspaces: workspaces,
isCollabWorkspaceOn: isCollabWorkspaceOn,
currentWorkspaceMember: currentWorkspaceMember,
actionResult: null,
),
);
@@ -198,6 +209,14 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
(e) => state.currentWorkspace,
);
WorkspaceMemberPB? currentWorkspaceMember;
final workspaceMemberResult =
await _userService.getWorkspaceMember();
currentWorkspaceMember = workspaceMemberResult.fold(
(s) => s,
(e) => null,
);
result
..onSuccess((s) {
Log.info(
@@ -211,6 +230,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
emit(
state.copyWith(
currentWorkspace: currentWorkspace,
currentWorkspaceMember: currentWorkspaceMember,
actionResult: UserWorkspaceActionResult(
actionType: UserWorkspaceActionType.open,
isLoading: false,
@@ -474,6 +494,7 @@ class UserWorkspaceState with _$UserWorkspaceState {
const factory UserWorkspaceState({
@Default(null) UserWorkspacePB? currentWorkspace,
@Default([]) List<UserWorkspacePB> workspaces,
@Default(null) WorkspaceMemberPB? currentWorkspaceMember,
@Default(null) UserWorkspaceActionResult? actionResult,
@Default(false) bool isCollabWorkspaceOn,
}) = _UserWorkspaceState;
@@ -38,6 +38,7 @@ class ViewBackendService {
/// If the index is null, the view will be added to the end of the list.
int? index,
ViewSectionPB? section,
final String? viewId,
}) {
final payload = CreateViewPayloadPB.create()
..parentViewId = parentViewId
@@ -63,6 +64,10 @@ class ViewBackendService {
payload.section = section;
}
if (viewId != null) {
payload.viewId = viewId;
}
return FolderEventCreateView(payload).send();
}
@@ -266,4 +271,42 @@ class ViewBackendService {
);
return FolderEventUpdateViewVisibilityStatus(payload).send();
}
static Future<FlowyResult<PublishInfoResponsePB, FlowyError>> getPublishInfo(
ViewPB view,
) async {
final payload = ViewIdPB()..value = view.id;
return FolderEventGetPublishInfo(payload).send();
}
static Future<FlowyResult<void, FlowyError>> publish(
ViewPB view, {
String? name,
}) async {
final payload = PublishViewParamsPB()..viewId = view.id;
if (name != null) {
payload.publishName = name;
}
return FolderEventPublishView(payload).send();
}
static Future<FlowyResult<void, FlowyError>> unpublish(
ViewPB view,
) async {
final payload = UnpublishViewsPayloadPB(viewIds: [view.id]);
return FolderEventUnpublishViews(payload).send();
}
static Future<FlowyResult<void, FlowyError>> setPublishNameSpace(
String name,
) async {
final payload = SetPublishNamespacePayloadPB()..newNamespace = name;
return FolderEventSetPublishNamespace(payload).send();
}
static Future<FlowyResult<PublishNamespacePB, FlowyError>>
getPublishNameSpace() async {
return FolderEventGetPublishNamespace().send();
}
}
@@ -18,6 +18,7 @@ class ViewTitleBloc extends Bloc<ViewTitleEvent, ViewTitleState> {
state.copyWith(
name: view.name,
icon: view.icon.value,
view: view,
),
);
@@ -27,16 +28,18 @@ class ViewTitleBloc extends Bloc<ViewTitleEvent, ViewTitleState> {
ViewTitleEvent.updateNameOrIcon(
view.name,
view.icon.value,
view,
),
);
},
);
},
updateNameOrIcon: (name, icon) async {
updateNameOrIcon: (name, icon, view) async {
emit(
state.copyWith(
name: name,
icon: icon,
view: view,
),
);
},
@@ -58,8 +61,11 @@ class ViewTitleBloc extends Bloc<ViewTitleEvent, ViewTitleState> {
@freezed
class ViewTitleEvent with _$ViewTitleEvent {
const factory ViewTitleEvent.initial() = Initial;
const factory ViewTitleEvent.updateNameOrIcon(String name, String icon) =
UpdateNameOrIcon;
const factory ViewTitleEvent.updateNameOrIcon(
String name,
String icon,
ViewPB? view,
) = UpdateNameOrIcon;
}
@freezed
@@ -67,6 +73,7 @@ class ViewTitleState with _$ViewTitleState {
const factory ViewTitleState({
required String name,
required String icon,
@Default(null) ViewPB? view,
}) = _ViewTitleState;
factory ViewTitleState.initial() => const ViewTitleState(name: '', icon: '');
@@ -18,6 +18,7 @@ class WorkspaceService {
int? index,
ViewLayoutPB? layout,
bool? setAsCurrent,
String? viewId,
}) {
final payload = CreateViewPayloadPB.create()
..parentViewId = workspaceId
@@ -37,6 +38,10 @@ class WorkspaceService {
payload.setAsCurrent = setAsCurrent;
}
if (viewId != null) {
payload.viewId = viewId;
}
return FolderEventCreateView(payload).send();
}
@@ -72,7 +72,10 @@ class DesktopHomeScreen extends StatelessWidget {
child: MultiBlocProvider(
key: ValueKey(userProfile.id),
providers: [
BlocProvider<ReminderBloc>.value(value: getIt<ReminderBloc>()),
BlocProvider.value(
value: getIt<ReminderBloc>()
..add(const ReminderEvent.started()),
),
BlocProvider<TabsBloc>.value(value: getIt<TabsBloc>()),
BlocProvider<HomeBloc>(
create: (_) =>
@@ -1,7 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
@@ -11,12 +9,15 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:provider/provider.dart';
import 'package:scaled_app/scaled_app.dart';
typedef KeyDownHandler = void Function(HotKey hotKey);
ValueNotifier<int> switchToTheNextSpace = ValueNotifier(0);
/// Helper class that utilizes the global [HotKeyManager] to easily
/// add a [HotKey] with different handlers.
///
@@ -163,6 +164,16 @@ class _HomeHotKeysState extends State<HomeHotKeys> {
keyDownHandler: (_) => _scaleToSize(1),
),
// Switch to the next space
HotKeyItem(
hotKey: HotKey(
KeyCode.keyO,
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
scope: HotKeyScope.inapp,
),
keyDownHandler: (_) => switchToTheNextSpace.value++,
),
// Open settings dialog
openSettingsHotKey(context, widget.userProfile),
];
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart';
@@ -36,9 +37,6 @@ class FavoriteMenu extends StatelessWidget {
FavoriteMenuBloc()..add(const FavoriteMenuEvent.initial()),
child: BlocBuilder<FavoriteMenuBloc, FavoriteMenuState>(
builder: (context, state) {
if (state.views.isEmpty) {
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -52,7 +50,10 @@ class FavoriteMenu extends StatelessWidget {
},
),
const VSpace(12),
_buildViews(context, state),
_FavoriteGroups(
minWidth: minWidth,
state: state,
),
],
);
},
@@ -60,64 +61,17 @@ class FavoriteMenu extends StatelessWidget {
),
);
}
}
Widget _buildViews(BuildContext context, FavoriteMenuState state) {
return Container(
width: minWidth - 2 * _kHorizontalPadding,
constraints: const BoxConstraints(
maxHeight: 300,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildGroups(
context,
state.todayViews,
LocaleKeys.sideBar_today.tr(),
),
..._buildGroups(
context,
state.thisWeekViews,
LocaleKeys.sideBar_thisWeek.tr(),
),
..._buildGroups(
context,
state.otherViews,
LocaleKeys.sideBar_others.tr(),
),
],
),
),
);
}
class _FavoriteGroupedViews extends StatelessWidget {
const _FavoriteGroupedViews({
required this.views,
});
List<Widget> _buildGroups(
BuildContext context,
List<ViewPB> views,
String title,
) {
return [
if (views.isNotEmpty) ...[
SizedBox(
height: 24,
child: FlowyText(
title,
fontSize: 12.0,
color: Theme.of(context).hintColor,
),
),
const VSpace(2),
_buildGroupedViews(context, views),
const VSpace(8),
const Divider(height: 1),
const VSpace(8),
],
];
}
final List<ViewPB> views;
Widget _buildGroupedViews(BuildContext context, List<ViewPB> views) {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: views
@@ -134,6 +88,15 @@ class FavoriteMenu extends StatelessWidget {
isFeedback: false,
isDraggable: false,
shouldRenderChildren: false,
extendBuilder: (view) => view.isPinned
? [
const HSpace(4.0),
const FlowySvg(
FlowySvgs.favorite_pin_s,
blendMode: null,
),
]
: [],
leftIconBuilder: (_, __) => const HSpace(4.0),
rightIconsBuilder: (_, view) => [
FavoriteMoreActions(view: view),
@@ -148,6 +111,93 @@ class FavoriteMenu extends StatelessWidget {
}
}
class _FavoriteGroups extends StatelessWidget {
const _FavoriteGroups({
required this.minWidth,
required this.state,
});
final double minWidth;
final FavoriteMenuState state;
@override
Widget build(BuildContext context) {
final today = _buildGroups(
context,
state.todayViews,
LocaleKeys.sideBar_today.tr(),
);
final thisWeek = _buildGroups(
context,
state.thisWeekViews,
LocaleKeys.sideBar_thisWeek.tr(),
);
final others = _buildGroups(
context,
state.otherViews,
LocaleKeys.sideBar_others.tr(),
);
return Container(
width: minWidth - 2 * _kHorizontalPadding,
constraints: const BoxConstraints(
maxHeight: 300,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (today.isNotEmpty) ...[
...today,
const VSpace(8),
const Divider(height: 1),
const VSpace(8),
],
if (thisWeek.isNotEmpty) ...[
...thisWeek,
const VSpace(8),
const Divider(height: 1),
const VSpace(8),
],
...others.isNotEmpty && (today.isNotEmpty || thisWeek.isNotEmpty)
? others
: _buildGroups(
context,
state.otherViews,
LocaleKeys.sideBar_others.tr(),
showHeader: false,
),
],
),
),
);
}
List<Widget> _buildGroups(
BuildContext context,
List<ViewPB> views,
String title, {
bool showHeader = true,
}) {
return [
if (views.isNotEmpty) ...[
if (showHeader)
SizedBox(
height: 24,
child: FlowyText(
title,
fontSize: 12.0,
color: Theme.of(context).hintColor,
),
),
const VSpace(2),
_FavoriteGroupedViews(views: views),
const VSpace(8),
],
];
}
}
class _FavoriteSearchField extends StatefulWidget {
const _FavoriteSearchField({
required this.width,
@@ -70,13 +70,14 @@ class SidebarTopMenu extends StatelessWidget {
children: [
TextSpan(
text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: color),
style:
Theme.of(context).tooltipTheme.textStyle!.copyWith(color: color),
),
TextSpan(
text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\',
style: Theme.of(context)
.textTheme
.bodyMedium!
.tooltipTheme
.textStyle!
.copyWith(color: Theme.of(context).hintColor),
),
],
@@ -91,6 +92,7 @@ class SidebarTopMenu extends StatelessWidget {
child: FlowyTooltip(
richMessage: textSpan,
child: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => context
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu()),
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_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/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart';
@@ -48,13 +49,23 @@ class SidebarNewPageButton extends StatelessWidget {
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn
? ViewSectionPB.Private
: ViewSectionPB.Public;
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: viewName,
viewSection: section,
index: 0,
),
);
final spaceState = context.read<SpaceBloc>().state;
if (spaceState.spaces.isNotEmpty) {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: viewName,
index: 0,
),
);
} else {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: viewName,
viewSection: section,
index: 0,
),
);
}
}
},
);
@@ -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/document/application/document_appearance_cubit.dart';
@@ -13,7 +15,6 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
@@ -41,11 +42,30 @@ HotKeyItem openSettingsHotKey(
},
);
class UserSettingButton extends StatelessWidget {
class UserSettingButton extends StatefulWidget {
const UserSettingButton({required this.userProfile, super.key});
final UserProfilePB userProfile;
@override
State<UserSettingButton> createState() => _UserSettingButtonState();
}
class _UserSettingButtonState extends State<UserSettingButton> {
late UserWorkspaceBloc _userWorkspaceBloc;
@override
void initState() {
super.initState();
_userWorkspaceBloc = context.read<UserWorkspaceBloc>();
}
@override
void didChangeDependencies() {
_userWorkspaceBloc = context.read<UserWorkspaceBloc>();
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return SizedBox.square(
@@ -53,7 +73,11 @@ class UserSettingButton extends StatelessWidget {
child: FlowyTooltip(
message: LocaleKeys.settings_menu_open.tr(),
child: FlowyButton(
onTap: () => showSettingsDialog(context, userProfile),
onTap: () => showSettingsDialog(
context,
widget.userProfile,
_userWorkspaceBloc,
),
margin: EdgeInsets.zero,
text: const FlowySvg(
FlowySvgs.settings_s,
@@ -65,7 +89,11 @@ class UserSettingButton extends StatelessWidget {
}
}
void showSettingsDialog(BuildContext context, UserProfilePB userProfile) {
void showSettingsDialog(
BuildContext context,
UserProfilePB userProfile, [
UserWorkspaceBloc? bloc,
]) {
AFFocusManager.of(context).notifyLoseFocus();
showDialog(
context: context,
@@ -75,15 +103,10 @@ void showSettingsDialog(BuildContext context, UserProfilePB userProfile) {
BlocProvider<DocumentAppearanceCubit>.value(
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
),
BlocProvider.value(value: context.read<UserWorkspaceBloc>()),
BlocProvider.value(value: bloc ?? context.read<UserWorkspaceBloc>()),
],
child: SettingsDialog(
userProfile,
workspaceId: context
.read<UserWorkspaceBloc>()
.state
.currentWorkspace!
.workspaceId,
didLogout: () async {
// Pop the dialog using the dialog context
Navigator.of(dialogContext).pop();
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/blank/blank.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
@@ -23,6 +24,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_migration.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
@@ -115,6 +117,7 @@ class HomeSideBar extends StatelessWidget {
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
openFirstPage: false,
),
),
),
@@ -133,11 +136,23 @@ class HomeSideBar extends StatelessWidget {
BlocListener<SpaceBloc, SpaceState>(
listenWhen: (p, c) =>
p.lastCreatedPage?.id != c.lastCreatedPage?.id,
listener: (context, state) => context.read<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: state.lastCreatedPage!.plugin(),
),
),
listener: (context, state) {
final page = state.lastCreatedPage;
if (page == null || page.id.isEmpty) {
// open the blank page
context.read<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: BlankPagePlugin(),
),
);
} else {
context.read<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: state.lastCreatedPage!.plugin(),
),
);
}
},
),
BlocListener<ActionNavigationBloc, ActionNavigationState>(
listenWhen: (_, curr) => curr.action != null,
@@ -150,23 +165,27 @@ class HomeSideBar extends StatelessWidget {
if (actionType == UserWorkspaceActionType.create ||
actionType == UserWorkspaceActionType.delete ||
actionType == UserWorkspaceActionType.open) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.reload(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
);
if (context.read<SpaceBloc>().state.spaces.isEmpty) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.reload(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
);
} else {
context.read<SpaceBloc>().add(
SpaceEvent.reset(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
);
}
context
.read<FavoriteBloc>()
.add(const FavoriteEvent.fetchFavorites());
context.read<SpaceBloc>().add(
SpaceEvent.reset(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
);
}
},
),
@@ -304,8 +323,6 @@ class _SidebarState extends State<_Sidebar> {
_renderFolderOrSpace(menuHorizontalInset),
_renderUpgradeSpaceButton(menuHorizontalInset),
// trash
Padding(
padding: menuHorizontalInset +
@@ -313,6 +330,8 @@ class _SidebarState extends State<_Sidebar> {
child: const Divider(height: 0.5, color: Color(0x141F2329)),
),
const VSpace(8),
_renderUpgradeSpaceButton(menuHorizontalInset),
const VSpace(8),
Padding(
padding: menuHorizontalInset +
const EdgeInsets.symmetric(horizontal: 4.0),
@@ -326,11 +345,12 @@ class _SidebarState extends State<_Sidebar> {
}
Widget _renderFolderOrSpace(EdgeInsets menuHorizontalInset) {
final spaceState = context.read<SpaceBloc>().state;
final workspaceState = context.read<UserWorkspaceBloc>().state;
// there's no space or the workspace is not collaborative,
// show the folder section (Workspace, Private, Personal)
// otherwise, show the space
return context.watch<SpaceBloc>().state.spaces.isEmpty ||
!context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn
return spaceState.spaces.isEmpty || !workspaceState.isCollabWorkspaceOn
? Expanded(
child: Padding(
padding: menuHorizontalInset - const EdgeInsets.only(right: 6),
@@ -362,26 +382,19 @@ class _SidebarState extends State<_Sidebar> {
}
Widget _renderUpgradeSpaceButton(EdgeInsets menuHorizontalInset) {
return !context.watch<SpaceBloc>().state.shouldShowUpgradeDialog
final spaceState = context.watch<SpaceBloc>().state;
final workspaceState = context.read<UserWorkspaceBloc>().state;
return !spaceState.shouldShowUpgradeDialog ||
!workspaceState.isCollabWorkspaceOn
? const SizedBox.shrink()
: Container(
height: 40,
padding: menuHorizontalInset,
child: FlowyButton(
onTap: () {
context.read<SpaceBloc>().add(const SpaceEvent.migrate());
},
leftIcon: const Icon(
Icons.upgrade_rounded,
color: Colors.red,
),
leftIconSize: const Size.square(20),
iconPadding: 12.0,
text: FlowyText.regular(
LocaleKeys.space_enableSpacesForYourWorkspace.tr(),
overflow: TextOverflow.ellipsis,
),
),
: Padding(
padding: menuHorizontalInset +
const EdgeInsets.only(
left: 4.0,
right: 4.0,
top: 8.0,
),
child: const SpaceMigration(),
);
}
@@ -15,9 +15,9 @@ class CreateSpacePopup extends StatefulWidget {
}
class _CreateSpacePopupState extends State<CreateSpacePopup> {
String spaceName = '';
String spaceIcon = '';
String spaceIconColor = '';
String spaceName = LocaleKeys.space_defaultSpaceName.tr();
String spaceIcon = builtInSpaceIcons.first;
String spaceIconColor = builtInSpaceColors.first;
SpacePermission spacePermission = SpacePermission.publicToAll;
@override
@@ -50,7 +50,13 @@ class _CreateSpacePopupState extends State<CreateSpacePopup> {
),
),
const VSpace(8.0),
_SpaceNameTextField(onChanged: (value) => spaceName = value),
_SpaceNameTextField(
onChanged: (value) => spaceName = value,
onSubmitted: (value) {
spaceName = value;
_createSpace();
},
),
const VSpace(20.0),
SpacePermissionSwitch(
onPermissionChanged: (value) => spacePermission = value,
@@ -59,29 +65,36 @@ class _CreateSpacePopupState extends State<CreateSpacePopup> {
SpaceCancelOrConfirmButton(
confirmButtonName: LocaleKeys.button_create.tr(),
onCancel: () => Navigator.of(context).pop(),
onConfirm: () {
context.read<SpaceBloc>().add(
SpaceEvent.create(
name: spaceName,
icon: spaceIcon,
iconColor: spaceIconColor,
permission: spacePermission,
),
);
Navigator.of(context).pop();
},
onConfirm: () => _createSpace(),
),
],
),
);
}
void _createSpace() {
context.read<SpaceBloc>().add(
SpaceEvent.create(
name: spaceName,
icon: spaceIcon,
iconColor: spaceIconColor,
permission: spacePermission,
createNewPageByDefault: true,
),
);
Navigator.of(context).pop();
}
}
class _SpaceNameTextField extends StatelessWidget {
const _SpaceNameTextField({required this.onChanged});
const _SpaceNameTextField({
required this.onChanged,
required this.onSubmitted,
});
final void Function(String name) onChanged;
final void Function(String name) onSubmitted;
@override
Widget build(BuildContext context) {
@@ -98,8 +111,9 @@ class _SpaceNameTextField extends StatelessWidget {
SizedBox(
height: 40,
child: FlowyTextField(
hintText: 'Untitled space',
hintText: LocaleKeys.space_spaceName.tr(),
onChanged: onChanged,
onSubmitted: onSubmitted,
),
),
],
@@ -99,6 +99,7 @@ class _SpaceNameTextField extends StatelessWidget {
SizedBox.square(
dimension: 40,
child: SpaceIconPopup(
cornerRadius: 12,
icon: space?.spaceIcon,
iconColor: space?.spaceIconColor,
onIconChanged: onIconChanged,
Loaded 100 of 421 files, more files were not shown because too many files have changed in this diff. Show more