Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2991be276 | ||
|
|
3645dd1b8b | ||
|
|
4237a73810 | ||
|
|
80a2ebc216 | ||
|
|
02a9f0dfbb | ||
|
|
4a8536d403 | ||
|
|
9c3f083684 | ||
|
|
628eace341 | ||
|
|
77a53ef67a | ||
|
|
5dec6ea545 | ||
|
|
dbcf8ba86f | ||
|
|
63fe1a6ef3 | ||
|
|
a2c52508fa | ||
|
|
0709a2d2ee | ||
|
|
e73b310076 | ||
|
|
7586a0ed48 | ||
|
|
d8b32168a9 | ||
|
|
8ca541bfa1 | ||
|
|
b64da2c02f | ||
|
|
fdaca36b87 | ||
|
|
8557383a89 | ||
|
|
fa86480458 | ||
|
|
d75eb05707 | ||
|
|
a2b32585a1 | ||
|
|
75cea400d2 | ||
|
|
ed82ec8eef | ||
|
|
b6debd6c71 | ||
|
|
a05ed63a3e | ||
|
|
3e75f1f24a | ||
|
|
e607694729 | ||
|
|
91d870415e | ||
|
|
21a497924c | ||
|
|
4a126e17ce | ||
|
|
8bf97ad5c6 | ||
|
|
a85d11fb6d | ||
|
|
1fd34bc58a | ||
|
|
70a7b856a2 | ||
|
|
49683e6079 | ||
|
|
13107b63e9 | ||
|
|
ce1a6e4fca | ||
|
|
1d23e28eaf | ||
|
|
2a4d78b688 | ||
|
|
cbe452a73d | ||
|
|
27899ee993 | ||
|
|
785597f53e | ||
|
|
e2ce274718 |
No files matched your search
@@ -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
|
||||
@@ -1,4 +1,20 @@
|
||||
# Release Notes
|
||||
## Version 0.6.0 - 19/06/2024
|
||||
### New Features
|
||||
- Introduced the "Space" feature to help you organize your pages more efficiently.
|
||||
### Bug Fixes
|
||||
- Resolved shortcut conflicts on the board page.
|
||||
- Resolved an issue where underscores could cause the editor to freeze.
|
||||
|
||||
## Version 0.5.9 - 06/06/2024
|
||||
### New Features
|
||||
- Revamped the sidebar for both Desktop and Mobile.
|
||||
- Added support for embedding videos in documents.
|
||||
- Introduced a hotkey (Cmd/Ctrl + 0) to reset the app scale.
|
||||
- Supported searching the workspace by page title.
|
||||
### Bug Fixes
|
||||
- Fixed the issue preventing the use of Backspace to delete words in Kanban boards.
|
||||
|
||||
## Version 0.5.8 - 05/20/2024
|
||||
### New Features
|
||||
- Improvement to the Callout block to insert new lines
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.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',
|
||||
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -17,25 +17,25 @@ void main() {
|
||||
await tester.openSettingsPage(SettingsPage.notifications);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final switchFinder = find.byType(Switch).first;
|
||||
final toggleFinder = find.byType(Toggle).first;
|
||||
|
||||
// Defaults to enabled
|
||||
Switch switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, true);
|
||||
Toggle toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, true);
|
||||
|
||||
// Disable
|
||||
await tester.tap(switchFinder);
|
||||
await tester.tap(toggleFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, false);
|
||||
toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, false);
|
||||
|
||||
// Enable again
|
||||
await tester.tap(switchFinder);
|
||||
await tester.tap(toggleFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, true);
|
||||
toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/auth_operation.dart';
|
||||
import '../../shared/base.dart';
|
||||
import '../../shared/expectation.dart';
|
||||
import '../../shared/settings.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Settings Billing', () {
|
||||
testWidgets('Local auth cannot see plan+billing', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapSignInAsGuest();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// We check that another settings page is present to ensure
|
||||
// it's not a fluke
|
||||
expect(
|
||||
find.text(
|
||||
LocaleKeys.settings_workspacePage_menuLabel.tr(),
|
||||
skipOffstage: false,
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
LocaleKeys.settings_planPage_menuLabel.tr(),
|
||||
skipOffstage: false,
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
LocaleKeys.settings_billingPage_menuLabel.tr(),
|
||||
skipOffstage: false,
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'notifications_settings_test.dart' as notifications_settings_test;
|
||||
import 'settings_billing_test.dart' as settings_billing_test;
|
||||
import 'shortcuts_settings_test.dart' as shortcuts_settings_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
notifications_settings_test.main();
|
||||
settings_billing_test.main();
|
||||
shortcuts_settings_test.main();
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/keyboard.dart';
|
||||
import '../../shared/util.dart';
|
||||
import '../board/board_hide_groups_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('shortcuts test', () {
|
||||
testWidgets('can change and overwrite shortcut', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.shortcuts);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final backspaceCmd =
|
||||
LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr();
|
||||
|
||||
// Input "Delete" into the search field
|
||||
await tester.enterText(find.byType(TextField), backspaceCmd);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
),
|
||||
onHover: () async {
|
||||
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
LogicalKeyboardKey.delete,
|
||||
LogicalKeyboardKey.enter,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
// We expect to see conflict dialog
|
||||
expect(
|
||||
find.text(
|
||||
LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Press on confirm label
|
||||
await tester.tap(
|
||||
find.text(
|
||||
LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// We expect the first ShortcutSettingTile to have one
|
||||
// [KeyBadge] with `delete` label
|
||||
final first = tester.widget(find.byType(ShortcutSettingTile).first)
|
||||
as ShortcutSettingTile;
|
||||
expect(
|
||||
first.command.command,
|
||||
'delete',
|
||||
);
|
||||
|
||||
// And the second one which is `Delete left character` to have none
|
||||
// as it will have been overwritten
|
||||
final second = tester.widget(find.byType(ShortcutSettingTile).at(1))
|
||||
as ShortcutSettingTile;
|
||||
expect(
|
||||
second.command.command,
|
||||
'',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -44,6 +45,12 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
assert(isSwitched == value);
|
||||
}
|
||||
|
||||
void assertToggleValue(Finder finder, bool value) {
|
||||
final Toggle switchWidget = widget(finder);
|
||||
final isSwitched = switchWidget.value;
|
||||
assert(isSwitched == value);
|
||||
}
|
||||
|
||||
void assertEnableEncryptSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
find.descendant(
|
||||
@@ -65,10 +72,10 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
}
|
||||
|
||||
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
assertToggleValue(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyCloudEnableSync),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Toggle),
|
||||
),
|
||||
value,
|
||||
);
|
||||
@@ -86,7 +93,7 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
Future<void> toggleEnableSync(Type syncButton) async {
|
||||
final finder = find.descendant(
|
||||
of: find.byType(syncButton),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Toggle),
|
||||
);
|
||||
|
||||
await tapButton(finder);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -37,8 +36,6 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
AuthenticatorType? cloudType,
|
||||
String? email,
|
||||
}) async {
|
||||
VideoBlockKit.ensureInitialized();
|
||||
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
// Set the window size
|
||||
await binding.setSurfaceSize(windowSize);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@@ -22,6 +27,7 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
@@ -29,10 +35,6 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'emoji.dart';
|
||||
@@ -619,6 +621,18 @@ extension SettingsFinder on CommonFinders {
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first;
|
||||
|
||||
Finder findSettingsMenuScrollable() => find
|
||||
.descendant(
|
||||
of: find
|
||||
.descendant(
|
||||
of: find.byType(SettingsMenu),
|
||||
matching: find.byType(SingleChildScrollView),
|
||||
)
|
||||
.first,
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first;
|
||||
}
|
||||
|
||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
|
||||
@@ -130,12 +130,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await openPage('v020', layout: ViewLayoutPB.Grid);
|
||||
}
|
||||
|
||||
Future<void> hoverOnFirstRowOfGrid() async {
|
||||
Future<void> hoverOnFirstRowOfGrid([Future<void> Function()? onHover]) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
expect(findRow, findsWidgets);
|
||||
|
||||
final firstRow = findRow.first;
|
||||
await hoverOnWidget(firstRow);
|
||||
await hoverOnWidget(firstRow, onHover: onHover);
|
||||
}
|
||||
|
||||
Future<void> editCell({
|
||||
@@ -876,11 +876,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> tapRowMenuButtonInGrid() async {
|
||||
expect(find.byType(RowMenuButton), findsOneWidget);
|
||||
await tapButton(find.byType(RowMenuButton));
|
||||
}
|
||||
|
||||
/// Should call [tapRowMenuButtonInGrid] first.
|
||||
Future<void> tapDeleteOnRowMenu() async {
|
||||
expect(find.text(LocaleKeys.grid_row_delete.tr()), findsOneWidget);
|
||||
await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../desktop/board/board_hide_groups_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'common_operations.dart';
|
||||
|
||||
@@ -31,6 +32,14 @@ extension AppFlowySettings on WidgetTester {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is SettingsMenuElement && widget.page == page,
|
||||
);
|
||||
|
||||
await scrollUntilVisible(
|
||||
button,
|
||||
0,
|
||||
scrollable: find.findSettingsMenuScrollable(),
|
||||
);
|
||||
await pump();
|
||||
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
return;
|
||||
|
||||
@@ -58,12 +58,6 @@ PODS:
|
||||
- Flutter
|
||||
- keyboard_height_plugin (0.0.1):
|
||||
- Flutter
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
@@ -72,8 +66,6 @@ PODS:
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.14.2):
|
||||
- SDWebImage/Core (= 5.14.2)
|
||||
- SDWebImage/Core (5.14.2)
|
||||
@@ -91,10 +83,6 @@ PODS:
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
@@ -110,20 +98,14 @@ DEPENDENCIES:
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -161,20 +143,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||
keyboard_height_plugin:
|
||||
:path: ".symlinks/plugins/keyboard_height_plugin/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
||||
media_kit_video:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@@ -185,10 +159,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
||||
@@ -206,14 +176,10 @@ SPEC CHECKSUMS:
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||
@@ -222,8 +188,6 @@ SPEC CHECKSUMS:
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/window_title_bar.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CocoaWindowChannel {
|
||||
@@ -122,15 +123,21 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
|
||||
|
||||
return FlowyTooltip(
|
||||
richMessage: textSpan,
|
||||
child: FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context
|
||||
child: Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerDown: (_) => context
|
||||
.read<HomeSettingBloc>()
|
||||
.add(const HomeSettingEvent.collapseMenu()),
|
||||
iconPadding: const EdgeInsets.all(4.0),
|
||||
icon: context.read<HomeSettingBloc>().state.isMenuCollapsed
|
||||
? const FlowySvg(FlowySvgs.show_menu_s)
|
||||
: const FlowySvg(FlowySvgs.hide_menu_m),
|
||||
child: FlowyHover(
|
||||
child: Container(
|
||||
width: 24,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: const RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: FlowySvg(FlowySvgs.hide_menu_s),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:scaled_app/scaled_app.dart';
|
||||
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
|
||||
import 'startup/startup.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0);
|
||||
VideoBlockKit.ensureInitialized();
|
||||
|
||||
await runAppFlowy();
|
||||
}
|
||||
+2
-1
@@ -33,6 +33,7 @@ class BlockActionBottomSheet extends StatelessWidget {
|
||||
FlowySvgs.arrow_up_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
showTopBorder: false,
|
||||
onTap: () => onAction(BlockActionBottomSheetType.insertAbove),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
@@ -48,7 +49,7 @@ class BlockActionBottomSheet extends StatelessWidget {
|
||||
FlowyOptionTile.text(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),
|
||||
leftIcon: const FlowySvg(FlowySvgs.m_duplicate_s),
|
||||
onTap: () => onAction(BlockActionBottomSheetType.duplicate),
|
||||
),
|
||||
|
||||
|
||||
+6
-6
@@ -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(
|
||||
|
||||
+3
-2
@@ -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(
|
||||
|
||||
+14
-7
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
@@ -68,7 +69,7 @@ class _MobileFavoriteSpaceState extends State<MobileFavoriteSpace>
|
||||
|
||||
if (favoriteState.views.isEmpty) {
|
||||
return const EmptySpacePlaceholder(
|
||||
type: MobileViewCardType.favorite,
|
||||
type: MobilePageCardType.favorite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,11 +93,17 @@ class _FavoriteViews extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderColor = Theme.of(context).isLightMode
|
||||
? const Color(0xFFE9E9EC)
|
||||
: const Color(0x1AFFFFFF);
|
||||
return Scrollbar(
|
||||
child: ListView.separated(
|
||||
key: const PageStorageKey('favorite_views_page_storage_key'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final view = favoriteViews[index];
|
||||
@@ -105,16 +112,16 @@ class _FavoriteViews extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
color: borderColor,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: MobileViewCard(
|
||||
child: MobileViewPage(
|
||||
key: ValueKey(view.item.id),
|
||||
view: view.item,
|
||||
timestamp: view.timestamp,
|
||||
type: MobileViewCardType.favorite,
|
||||
type: MobilePageCardType.favorite,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -28,9 +28,12 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
|
||||
return Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
vertical: HomeSpaceViewSizes.mVerticalPadding,
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
top: HomeSpaceViewSizes.mVerticalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: MobileFolders(
|
||||
user: widget.userProfile,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
+14
-7
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/recent/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
@@ -36,7 +37,7 @@ class _MobileRecentSpaceState extends State<MobileRecentSpace>
|
||||
|
||||
if (recentViews.isEmpty) {
|
||||
return const Center(
|
||||
child: EmptySpacePlaceholder(type: MobileViewCardType.recent),
|
||||
child: EmptySpacePlaceholder(type: MobilePageCardType.recent),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,12 +64,18 @@ class _RecentViews extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderColor = Theme.of(context).isLightMode
|
||||
? const Color(0xFFE9E9EC)
|
||||
: const Color(0x1AFFFFFF);
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
key: const PageStorageKey('recent_views_page_storage_key'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final sectionView = recentViews[index];
|
||||
@@ -77,16 +84,16 @@ class _RecentViews extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
color: borderColor,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: MobileViewCard(
|
||||
child: MobileViewPage(
|
||||
key: ValueKey(sectionView.item.id),
|
||||
view: sectionView.item,
|
||||
timestamp: sectionView.timestamp,
|
||||
type: MobileViewCardType.recent,
|
||||
type: MobilePageCardType.recent,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
|
||||
class EmptySpacePlaceholder extends StatelessWidget {
|
||||
const EmptySpacePlaceholder({super.key, required this.type});
|
||||
|
||||
final MobileViewCardType type;
|
||||
final MobilePageCardType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -42,14 +42,14 @@ class EmptySpacePlaceholder extends StatelessWidget {
|
||||
}
|
||||
|
||||
String get _emptyPageText => switch (type) {
|
||||
MobileViewCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(),
|
||||
MobileViewCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(),
|
||||
MobilePageCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(),
|
||||
MobilePageCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(),
|
||||
};
|
||||
|
||||
String get _emptyPageSubText => switch (type) {
|
||||
MobileViewCardType.recent =>
|
||||
MobilePageCardType.recent =>
|
||||
LocaleKeys.sideBar_emptyRecentDescription.tr(),
|
||||
MobileViewCardType.favorite =>
|
||||
MobilePageCardType.favorite =>
|
||||
LocaleKeys.sideBar_emptyFavoriteDescription.tr(),
|
||||
};
|
||||
}
|
||||
+72
-21
@@ -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,
|
||||
|
||||
+40
-58
@@ -1,15 +1,15 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final PropertyValueNotifier<ViewLayoutPB?> createNewPageNotifier =
|
||||
PropertyValueNotifier(null);
|
||||
|
||||
const _homeLabel = 'home';
|
||||
const _addLabel = 'add';
|
||||
const _notificationLabel = 'notification';
|
||||
@@ -46,22 +46,40 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLightMode = Theme.of(context).isLightMode;
|
||||
final backgroundColor = isLightMode
|
||||
? Colors.white.withOpacity(0.95)
|
||||
: const Color(0xFF23262B).withOpacity(0.95);
|
||||
return Scaffold(
|
||||
body: navigationShell,
|
||||
bottomNavigationBar: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
enableFeedback: false,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
items: _items,
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
|
||||
extendBody: true,
|
||||
bottomNavigationBar: ClipRRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 3,
|
||||
sigmaY: 3,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: isLightMode
|
||||
? Border(
|
||||
top: BorderSide(color: Theme.of(context).dividerColor),
|
||||
)
|
||||
: null,
|
||||
color: backgroundColor,
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
enableFeedback: false,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
items: _items,
|
||||
backgroundColor: Colors.transparent,
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -72,7 +90,7 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
void _onTap(BuildContext context, int bottomBarIndex) {
|
||||
if (_items[bottomBarIndex].label == _addLabel) {
|
||||
// show an add dialog
|
||||
_showCreatePageBottomSheet(context);
|
||||
createNewPageNotifier.value = ViewLayoutPB.Document;
|
||||
return;
|
||||
}
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
@@ -87,40 +105,4 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
initialLocation: bottomBarIndex == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreatePageBottomSheet(BuildContext context) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: LocaleKeys.sideBar_addAPage.tr(),
|
||||
showDragHandle: true,
|
||||
showCloseButton: true,
|
||||
useRootNavigator: true,
|
||||
builder: (sheetContext) {
|
||||
return AddNewPageWidgetBottomSheet(
|
||||
view: ViewPB(),
|
||||
onAction: (layout) async {
|
||||
Navigator.of(sheetContext).pop();
|
||||
final currentWorkspaceId =
|
||||
await FolderEventReadCurrentWorkspace().send();
|
||||
await currentWorkspaceId.fold((s) async {
|
||||
final workspaceService = WorkspaceService(workspaceId: s.id);
|
||||
final result = await workspaceService.createView(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
viewSection: ViewSectionPB.Private,
|
||||
layout: layout,
|
||||
);
|
||||
result.fold((s) {
|
||||
context.pushView(s);
|
||||
}, (e) {
|
||||
Log.error('Failed to create new page: $e');
|
||||
});
|
||||
}, (e) {
|
||||
Log.error('Failed to read current workspace: $e');
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-9
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -13,9 +15,23 @@ class ChatUserMessageBloc
|
||||
}) : super(ChatUserMessageState.initial(message)) {
|
||||
on<ChatUserMessageEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {},
|
||||
update: (userProfile, deviceId, states) {},
|
||||
event.when(
|
||||
initial: () {
|
||||
final payload =
|
||||
WorkspaceMemberIdPB(uid: Int64.parseInt(message.author.id));
|
||||
UserEventGetMemberInfo(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold((member) {
|
||||
add(ChatUserMessageEvent.didReceiveMemberInfo(member));
|
||||
}, (err) {
|
||||
Log.error("Error getting member info: $err");
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
didReceiveMemberInfo: (WorkspaceMemberPB memberInfo) {
|
||||
emit(state.copyWith(member: memberInfo));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -25,11 +41,9 @@ class ChatUserMessageBloc
|
||||
@freezed
|
||||
class ChatUserMessageEvent with _$ChatUserMessageEvent {
|
||||
const factory ChatUserMessageEvent.initial() = Initial;
|
||||
const factory ChatUserMessageEvent.update(
|
||||
UserProfilePB userProfile,
|
||||
String deviceId,
|
||||
DocumentAwarenessStatesPB states,
|
||||
) = Update;
|
||||
const factory ChatUserMessageEvent.didReceiveMemberInfo(
|
||||
WorkspaceMemberPB memberInfo,
|
||||
) = _MemberInfo;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+74
-8
@@ -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(
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:highlight/highlight.dart';
|
||||
|
||||
/// Highlight Flutter Widget
|
||||
class SelectableHighlightView extends StatelessWidget {
|
||||
SelectableHighlightView(
|
||||
String input, {
|
||||
super.key,
|
||||
this.language,
|
||||
this.theme = const {},
|
||||
this.padding,
|
||||
this.textStyle,
|
||||
int tabSize = 8,
|
||||
}) : source = input.replaceAll('\t', ' ' * tabSize);
|
||||
|
||||
/// The original code to be highlighted
|
||||
final String source;
|
||||
|
||||
/// Highlight language
|
||||
///
|
||||
/// It is recommended to give it a value for performance
|
||||
///
|
||||
/// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages)
|
||||
final String? language;
|
||||
|
||||
/// Highlight theme
|
||||
///
|
||||
/// [All available themes](https://github.com/pd4d10/highlight/blob/master/flutter_highlight/lib/themes)
|
||||
final Map<String, TextStyle> theme;
|
||||
|
||||
/// Padding
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// Text styles
|
||||
///
|
||||
/// Specify text styles such as font family and font size
|
||||
final TextStyle? textStyle;
|
||||
|
||||
List<TextSpan> _convert(List<Node> nodes) {
|
||||
final List<TextSpan> spans = [];
|
||||
var currentSpans = spans;
|
||||
final List<List<TextSpan>> stack = [];
|
||||
|
||||
// ignore: always_declare_return_types
|
||||
traverse(Node node) {
|
||||
if (node.value != null) {
|
||||
currentSpans.add(
|
||||
node.className == null
|
||||
? TextSpan(text: node.value)
|
||||
: TextSpan(text: node.value, style: theme[node.className!]),
|
||||
);
|
||||
} else if (node.children != null) {
|
||||
final List<TextSpan> tmp = [];
|
||||
currentSpans
|
||||
.add(TextSpan(children: tmp, style: theme[node.className!]));
|
||||
stack.add(currentSpans);
|
||||
currentSpans = tmp;
|
||||
|
||||
for (final n in node.children!) {
|
||||
traverse(n);
|
||||
if (n == node.children!.last) {
|
||||
currentSpans = stack.isEmpty ? spans : stack.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final node in nodes) {
|
||||
traverse(node);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
static const _rootKey = 'root';
|
||||
static const _defaultBackgroundColor = Color(0xffffffff);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor,
|
||||
padding: padding,
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: textStyle,
|
||||
children:
|
||||
_convert(highlight.parse(source, language: language).nodes!),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+11
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
@@ -5,12 +7,10 @@ import 'package:appflowy/plugins/database/application/setting/property_bloc.dart
|
||||
import 'package:appflowy/plugins/database/calendar/application/calendar_setting_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Widget that displays a list of settings that alters the appearance of the
|
||||
@@ -325,7 +325,6 @@ Widget _toggleItem({
|
||||
Toggle(
|
||||
value: value,
|
||||
onChanged: (value) => onToggle(!value),
|
||||
style: ToggleStyle.big,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
|
||||
+5
-6
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
+12
-1
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -52,7 +53,17 @@ class RowActionMenu extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(action.text, overflow: TextOverflow.ellipsis),
|
||||
onTap: () {
|
||||
action.performAction(context, viewId, rowId);
|
||||
if (action == RowAction.delete) {
|
||||
NavigatorOkCancelDialog(
|
||||
message: LocaleKeys.grid_row_deleteRowPrompt.tr(),
|
||||
onOkPressed: () {
|
||||
action.performAction(context, viewId, rowId);
|
||||
},
|
||||
).show(context);
|
||||
} else {
|
||||
action.performAction(context, viewId, rowId);
|
||||
}
|
||||
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
leftIcon: icon,
|
||||
|
||||
+5
-3
@@ -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,
|
||||
),
|
||||
|
||||
+2
-1
@@ -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,
|
||||
),
|
||||
|
||||
+2
-1
@@ -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,
|
||||
),
|
||||
|
||||
+55
-18
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -62,22 +62,24 @@ class _ChecklistItemsState extends State<ChecklistItems> {
|
||||
if (showIncompleteOnly) {
|
||||
tasks.removeWhere((task) => task.isSelected);
|
||||
}
|
||||
final children = tasks
|
||||
.mapIndexed(
|
||||
(index, task) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: ChecklistItem(
|
||||
key: ValueKey(task.data.id),
|
||||
task: task,
|
||||
autofocus: widget.state.newTask && index == tasks.length - 1,
|
||||
onSubmitted: index == tasks.length - 1
|
||||
? () => widget.bloc
|
||||
.add(const ChecklistCellEvent.createNewTask(""))
|
||||
: null,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
// final children = tasks
|
||||
// .mapIndexed(
|
||||
// (index, task) => Padding(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
// child: ChecklistItem(
|
||||
// key: ValueKey('${task.data.id}$index'),
|
||||
// task: task,
|
||||
// autofocus: widget.state.newTask && index == tasks.length - 1,
|
||||
// onSubmitted: () {
|
||||
// if (index == tasks.length - 1) {
|
||||
// // create a new task under the last task if the users press enter
|
||||
// widget.bloc.add(const ChecklistCellEvent.createNewTask(''));
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Column(
|
||||
@@ -115,7 +117,7 @@ class _ChecklistItemsState extends State<ChecklistItems> {
|
||||
),
|
||||
),
|
||||
const VSpace(2.0),
|
||||
...children,
|
||||
_ChecklistCellEditors(tasks: tasks),
|
||||
ChecklistItemControl(cellNotifer: widget.cellContainerNotifier),
|
||||
],
|
||||
),
|
||||
@@ -123,6 +125,41 @@ class _ChecklistItemsState extends State<ChecklistItems> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistCellEditors extends StatelessWidget {
|
||||
const _ChecklistCellEditors({
|
||||
required this.tasks,
|
||||
});
|
||||
|
||||
final List<ChecklistSelectOption> tasks;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<ChecklistCellBloc>();
|
||||
final state = bloc.state;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...tasks.mapIndexed(
|
||||
(index, task) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: ChecklistItem(
|
||||
key: ValueKey('${task.data.id}$index'),
|
||||
task: task,
|
||||
autofocus: state.newTask && index == tasks.length - 1,
|
||||
onSubmitted: () {
|
||||
if (index == tasks.length - 1) {
|
||||
// create a new task under the last task if the users press enter
|
||||
bloc.add(const ChecklistCellEvent.createNewTask(''));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistItemControl extends StatelessWidget {
|
||||
const ChecklistItemControl({super.key, required this.cellNotifer});
|
||||
|
||||
|
||||
+115
-188
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+129
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
+117
-29
@@ -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: '',
|
||||
);
|
||||
}
|
||||
+4
-5
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
+59
-15
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-14
@@ -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()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -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),
|
||||
);
|
||||
|
||||
|
||||
-7
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
-11
@@ -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),
|
||||
|
||||
+21
-26
@@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+6
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
-102
@@ -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);
|
||||
}
|
||||
-314
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
-136
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Whitespace-only changes.
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+36
-13
@@ -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;
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+15
-2
@@ -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;
|
||||
|
||||
|
||||
+5
-2
@@ -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,
|
||||
|
||||
+3
-1
@@ -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,
|
||||
|
||||
+6
-2
@@ -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() {
|
||||
|
||||
+10
@@ -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',
|
||||
};
|
||||
}
|
||||
+36
-25
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-8
@@ -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;
|
||||
|
||||
+144
-21
@@ -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();
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -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),
|
||||
];
|
||||
|
||||
+109
-59
@@ -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,
|
||||
|
||||
+5
-3
@@ -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()),
|
||||
|
||||
+18
-7
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
+33
-10
@@ -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();
|
||||
|
||||
+55
-42
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+32
-18
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+1
@@ -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
Reference in new issue
Block a user