mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-22 05:37:15 -04:00
Compare commits
89 commits
Author | SHA1 | Date | |
---|---|---|---|
|
6bdaee3a00 | ||
|
7f74543125 | ||
|
514eeb8466 | ||
|
403f343371 | ||
|
8c5547da64 | ||
|
eaac387c8d | ||
|
00aad4da47 | ||
|
c10c844fa9 | ||
|
40c1ae1d38 | ||
|
3ae6888fee | ||
|
14b5e4e184 | ||
|
530e076838 | ||
|
1be51d6679 | ||
|
b0c2b04a2d | ||
|
4e2990e599 | ||
|
9cd49c2447 | ||
|
0cdecee771 | ||
|
1bcf4e6e8d | ||
|
343f7e4fd5 | ||
|
4634b51edf | ||
|
8cd0442a1f | ||
|
10a536b1a2 | ||
|
65b7916a6a | ||
|
2cf96a2ce3 | ||
|
7885cb80f4 | ||
|
1356382524 | ||
|
04407fe8ff | ||
|
3451100b80 | ||
|
f8927b1843 | ||
|
c7bf8bb1ba | ||
|
c6010a6734 | ||
|
cf46213e00 | ||
|
2ee786f351 | ||
|
92d5690bba | ||
|
791a79a234 | ||
|
fa798f3ecd | ||
|
f72739d98d | ||
|
fd581b4453 | ||
|
747a63d452 | ||
|
833a2bf5d6 | ||
|
607b7ecd1f | ||
|
bb5d36402a | ||
|
ccd1f5f8e9 | ||
|
2c5f41b580 | ||
|
2f5b494885 | ||
|
58f87b39aa | ||
|
d478ecfd41 | ||
|
72fc0cce07 | ||
|
81f63bebe6 | ||
|
102087537a | ||
|
6dac45172e | ||
|
84952b9056 | ||
|
e851fba71b | ||
|
3a05a4851f | ||
|
edc5710e32 | ||
|
1802792795 | ||
|
28e89beb43 | ||
|
889756ebb0 | ||
|
54b5e248e3 | ||
|
d24383b6ea | ||
|
2dc22004a1 | ||
|
0906febe95 | ||
|
b12bd8ee85 | ||
|
e1bfb7095b | ||
|
068f93c258 | ||
|
d8401e09c9 | ||
|
394ac85c32 | ||
|
f6e3290aa4 | ||
|
4925e99166 | ||
|
3bb5075a98 | ||
|
59efb7d9e5 | ||
|
165e95c480 | ||
|
31d8653ba6 | ||
|
80df4955e2 | ||
|
5436277ada | ||
|
ed64719560 | ||
|
ac659066c6 | ||
|
3a4d17f054 | ||
|
57e4d269eb | ||
|
c2339c3522 | ||
|
13065ac726 | ||
|
af91a72187 | ||
|
98b835227e | ||
|
c633dd0919 | ||
|
e2896b2911 | ||
|
77fbf0f8a3 | ||
|
c89f33e2f8 | ||
|
e5b6393257 | ||
|
f62686fdeb |
385 changed files with 14342 additions and 9816 deletions
|
@ -1,44 +1,63 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/emoji/emoji_handler.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/keyboard.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Future<void> prepare(WidgetTester tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
}
|
||||
|
||||
// May be better to move this to an existing test but unsure what it fits with
|
||||
group('Keyboard shortcuts related to emojis', () {
|
||||
testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await prepare(tester);
|
||||
|
||||
final Finder editor = find.byType(AppFlowyEditor);
|
||||
await tester.tap(editor);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(EmojiHandler), findsNothing);
|
||||
|
||||
expect(find.byType(EmojiSelectionMenu), findsNothing);
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
Platform.isMacOS
|
||||
? LogicalKeyboardKey.meta
|
||||
: LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.alt,
|
||||
LogicalKeyboardKey.keyE,
|
||||
],
|
||||
tester: tester,
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyE,
|
||||
isAltPressed: true,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isControlPressed: !Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
expect(find.byType(EmojiHandler), findsOneWidget);
|
||||
|
||||
expect(find.byType(EmojiSelectionMenu), findsOneWidget);
|
||||
/// press backspace to hide the emoji picker
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
|
||||
expect(find.byType(EmojiHandler), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('insert emoji by slash menu', (tester) async {
|
||||
await prepare(tester);
|
||||
await tester.editor.showSlashMenu();
|
||||
|
||||
/// show emoji picler
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_emoji.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
expect(find.byType(EmojiHandler), findsOneWidget);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
final firstNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
|
||||
/// except the emoji is in document
|
||||
expect(firstNode.delta!.toPlainText().contains('😀'), true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -47,10 +66,7 @@ void main() {
|
|||
WidgetTester tester, {
|
||||
String? search,
|
||||
}) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await prepare(tester);
|
||||
await tester.ime.insertText(':${search ?? 'a'}');
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
|
@ -38,7 +37,7 @@ void main() {
|
|||
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.light);
|
||||
|
@ -48,7 +47,7 @@ void main() {
|
|||
LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.dark);
|
||||
|
@ -66,10 +65,11 @@ void main() {
|
|||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.light);
|
||||
// disable it temporarily. It works on macOS but not on Linux.
|
||||
// themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
// expect(themeMode, ThemeMode.light);
|
||||
});
|
||||
|
||||
testWidgets('show or hide home menu', (tester) async {
|
||||
|
|
|
@ -79,7 +79,7 @@ extension AppFlowySettings on WidgetTester {
|
|||
// Enable editing username
|
||||
final editUsernameFinder = find.descendant(
|
||||
of: find.byType(AccountUserProfile),
|
||||
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
||||
matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m),
|
||||
);
|
||||
await tap(editUsernameFinder, warnIfMissed: false);
|
||||
await pumpAndSettle();
|
||||
|
|
|
@ -31,9 +31,6 @@ class _SelectModelMenuState extends State<SelectModelMenu> {
|
|||
),
|
||||
child: BlocBuilder<SelectModelBloc, SelectModelState>(
|
||||
builder: (context, state) {
|
||||
if (state.selectedModel == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AppFlowyPopover(
|
||||
offset: Offset(-12.0, 0.0),
|
||||
constraints: BoxConstraints(maxWidth: 250, maxHeight: 600),
|
||||
|
@ -55,8 +52,12 @@ class _SelectModelMenuState extends State<SelectModelMenu> {
|
|||
);
|
||||
},
|
||||
child: _CurrentModelButton(
|
||||
model: state.selectedModel!,
|
||||
onTap: () => popoverController.show(),
|
||||
model: state.selectedModel,
|
||||
onTap: () {
|
||||
if (state.selectedModel != null) {
|
||||
popoverController.show();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -202,7 +203,7 @@ class _CurrentModelButton extends StatelessWidget {
|
|||
required this.onTap,
|
||||
});
|
||||
|
||||
final AIModelPB model;
|
||||
final AIModelPB? model;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
|
@ -214,40 +215,45 @@ class _CurrentModelButton extends StatelessWidget {
|
|||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: DesktopAIPromptSizes.actionBarButtonSize,
|
||||
child: FlowyHover(
|
||||
style: const HoverStyle(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
// TODO: remove this after change icon to 20px
|
||||
padding: EdgeInsets.all(2),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.ai_sparks_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: Size.square(16),
|
||||
),
|
||||
),
|
||||
if (!model.isDefault)
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 50),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: FlowyHover(
|
||||
style: const HoverStyle(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 2.0),
|
||||
child: FlowyText(
|
||||
model.i18n,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
// TODO: remove this after change icon to 20px
|
||||
padding: EdgeInsets.all(2),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.ai_sparks_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
size: Size.square(16),
|
||||
),
|
||||
),
|
||||
FlowySvg(
|
||||
FlowySvgs.ai_source_drop_down_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(8),
|
||||
),
|
||||
],
|
||||
if (model != null && !model!.isDefault)
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 2.0),
|
||||
child: FlowyText(
|
||||
model!.i18n,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
FlowySvg(
|
||||
FlowySvgs.ai_source_drop_down_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -44,10 +44,18 @@ Future<bool> afLaunchUri(
|
|||
uri = Uri.parse('https://$url');
|
||||
}
|
||||
|
||||
// try to launch the uri directly
|
||||
bool result = await launcher.canLaunchUrl(uri);
|
||||
/// opening an incorrect link will cause a system error dialog to pop up on macOS
|
||||
/// only use [canLaunchUrl] on macOS
|
||||
/// and there is an known issue with url_launcher on Linux where it fails to launch
|
||||
/// see https://github.com/flutter/flutter/issues/88463
|
||||
bool result = true;
|
||||
if (UniversalPlatform.isMacOS) {
|
||||
result = await launcher.canLaunchUrl(uri);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
try {
|
||||
// try to launch the uri directly
|
||||
result = await launcher.launchUrl(
|
||||
uri,
|
||||
mode: mode,
|
||||
|
|
|
@ -19,14 +19,13 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
|
|||
|
||||
Future<void> _initialize(Emitter<UserProfileState> emit) async {
|
||||
emit(const UserProfileState.loading());
|
||||
|
||||
final workspaceOrFailure =
|
||||
final latestOrFailure =
|
||||
await FolderEventGetCurrentWorkspaceSetting().send();
|
||||
|
||||
final userOrFailure = await getIt<AuthService>().getUser();
|
||||
|
||||
final workspaceSetting = workspaceOrFailure.fold(
|
||||
(workspaceSettingPB) => workspaceSettingPB,
|
||||
final latest = latestOrFailure.fold(
|
||||
(latestPB) => latestPB,
|
||||
(error) => null,
|
||||
);
|
||||
|
||||
|
@ -35,13 +34,13 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
|
|||
(error) => null,
|
||||
);
|
||||
|
||||
if (workspaceSetting == null || userProfile == null) {
|
||||
if (latest == null || userProfile == null) {
|
||||
return emit(const UserProfileState.workspaceFailure());
|
||||
}
|
||||
|
||||
emit(
|
||||
UserProfileState.success(
|
||||
workspaceSettings: workspaceSetting,
|
||||
workspaceSettings: latest,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
);
|
||||
|
@ -59,7 +58,7 @@ class UserProfileState with _$UserProfileState {
|
|||
const factory UserProfileState.loading() = _Loading;
|
||||
const factory UserProfileState.workspaceFailure() = _WorkspaceFailure;
|
||||
const factory UserProfileState.success({
|
||||
required WorkspaceSettingPB workspaceSettings,
|
||||
required WorkspaceLatestPB workspaceSettings,
|
||||
required UserProfilePB userProfile,
|
||||
}) = _Success;
|
||||
}
|
||||
|
|
|
@ -203,7 +203,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
|||
final userProfile = context.read<MobileViewPageBloc>().state.userProfilePB;
|
||||
// the publish feature is only available for AppFlowy Cloud
|
||||
if (userProfile == null ||
|
||||
userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) {
|
||||
userProfile.workspaceAuthType != AuthTypePB.Server) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
@ -31,9 +31,9 @@ class MobileFavoriteScreen extends StatelessWidget {
|
|||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final workspaceSetting = snapshots.data?[0].fold(
|
||||
(workspaceSettingPB) {
|
||||
return workspaceSettingPB as WorkspaceSettingPB?;
|
||||
final latest = snapshots.data?[0].fold(
|
||||
(latest) {
|
||||
return latest as WorkspaceLatestPB?;
|
||||
},
|
||||
(error) => null,
|
||||
);
|
||||
|
@ -46,7 +46,7 @@ class MobileFavoriteScreen extends StatelessWidget {
|
|||
|
||||
// In the unlikely case either of the above is null, eg.
|
||||
// when a workspace is already open this can happen.
|
||||
if (workspaceSetting == null || userProfile == null) {
|
||||
if (latest == null || userProfile == null) {
|
||||
return const WorkspaceFailedScreen();
|
||||
}
|
||||
|
||||
|
|
|
@ -44,9 +44,9 @@ class MobileHomeScreen extends StatelessWidget {
|
|||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final workspaceSetting = snapshots.data?[0].fold(
|
||||
(workspaceSettingPB) {
|
||||
return workspaceSettingPB as WorkspaceSettingPB?;
|
||||
final workspaceLatest = snapshots.data?[0].fold(
|
||||
(workspaceLatestPB) {
|
||||
return workspaceLatestPB as WorkspaceLatestPB?;
|
||||
},
|
||||
(error) => null,
|
||||
);
|
||||
|
@ -59,7 +59,7 @@ class MobileHomeScreen extends StatelessWidget {
|
|||
|
||||
// In the unlikely case either of the above is null, eg.
|
||||
// when a workspace is already open this can happen.
|
||||
if (workspaceSetting == null || userProfile == null) {
|
||||
if (workspaceLatest == null || userProfile == null) {
|
||||
return const WorkspaceFailedScreen();
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ class MobileHomeScreen extends StatelessWidget {
|
|||
value: userProfile,
|
||||
child: MobileHomePage(
|
||||
userProfile: userProfile,
|
||||
workspaceSetting: workspaceSetting,
|
||||
workspaceLatest: workspaceLatest,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -95,11 +95,11 @@ class MobileHomePage extends StatefulWidget {
|
|||
const MobileHomePage({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
required this.workspaceSetting,
|
||||
required this.workspaceLatest,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
final WorkspaceLatestPB workspaceLatest;
|
||||
|
||||
@override
|
||||
State<MobileHomePage> createState() => _MobileHomePageState();
|
||||
|
@ -145,7 +145,7 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
|||
|
||||
void _onLatestViewChange() async {
|
||||
final id = getIt<MenuSharedState>().latestOpenView?.id;
|
||||
if (id == null) {
|
||||
if (id == null || id.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
|
||||
|
|
|
@ -194,6 +194,7 @@ class _MobileWorkspace extends StatelessWidget {
|
|||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspace.workspaceId,
|
||||
workspace.workspaceAuthType,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget {
|
|||
text: LocaleKeys.settings_popupMenuItem_settings.tr(),
|
||||
),
|
||||
// only show the member items in cloud mode
|
||||
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[
|
||||
if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[
|
||||
const PopupMenuDivider(height: 0.5),
|
||||
_buildItem(
|
||||
value: _MobileSettingsPopupMenuItem.members,
|
||||
|
|
|
@ -167,8 +167,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
|||
children: [
|
||||
MobileHomeSpace(userProfile: widget.userProfile),
|
||||
// only show ai chat button for cloud user
|
||||
if (widget.userProfile.authenticator ==
|
||||
AuthenticatorPB.AppFlowyCloud)
|
||||
if (widget.userProfile.workspaceAuthType == AuthTypePB.Server)
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 20,
|
||||
|
|
|
@ -123,6 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget {
|
|||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.createWorkspace(
|
||||
name,
|
||||
AuthTypePB.Server,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -50,9 +50,9 @@ class _MobileNotificationsScreenState extends State<MobileNotificationsScreen>
|
|||
orElse: () =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
workspaceFailure: () => const WorkspaceFailedScreen(),
|
||||
success: (workspaceSetting, userProfile) =>
|
||||
success: (workspaceLatest, userProfile) =>
|
||||
_NotificationScreenContent(
|
||||
workspaceSetting: workspaceSetting,
|
||||
workspaceLatest: workspaceLatest,
|
||||
userProfile: userProfile,
|
||||
controller: controller,
|
||||
reminderBloc: reminderBloc,
|
||||
|
@ -66,13 +66,13 @@ class _MobileNotificationsScreenState extends State<MobileNotificationsScreen>
|
|||
|
||||
class _NotificationScreenContent extends StatelessWidget {
|
||||
const _NotificationScreenContent({
|
||||
required this.workspaceSetting,
|
||||
required this.workspaceLatest,
|
||||
required this.userProfile,
|
||||
required this.controller,
|
||||
required this.reminderBloc,
|
||||
});
|
||||
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
final WorkspaceLatestPB workspaceLatest;
|
||||
final UserProfilePB userProfile;
|
||||
final TabController controller;
|
||||
final ReminderBloc reminderBloc;
|
||||
|
@ -84,7 +84,7 @@ class _NotificationScreenContent extends StatelessWidget {
|
|||
..add(
|
||||
SidebarSectionsEvent.initial(
|
||||
userProfile,
|
||||
workspaceSetting.workspaceId,
|
||||
workspaceLatest.workspaceId,
|
||||
),
|
||||
),
|
||||
child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
|
|
|
@ -58,7 +58,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
|
|||
userName: userName,
|
||||
onSubmitted: (value) => context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserName(value)),
|
||||
.add(SettingsUserEvent.updateUserName(name: value)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -40,7 +40,7 @@ class UserSessionSettingGroup extends StatelessWidget {
|
|||
|
||||
// delete account button
|
||||
// only show the delete account button in cloud mode
|
||||
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[
|
||||
if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[
|
||||
const VSpace(16.0),
|
||||
MobileLogoutButton(
|
||||
text: LocaleKeys.button_deleteAccount.tr(),
|
||||
|
|
|
@ -197,7 +197,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
// only show the result dialog when the action is WorkspaceMemberActionType.add
|
||||
if (actionType == WorkspaceMemberActionType.add) {
|
||||
if (actionType == WorkspaceMemberActionType.addByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
|
@ -223,7 +223,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.invite) {
|
||||
} else if (actionType == WorkspaceMemberActionType.inviteByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
|
@ -250,7 +250,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.remove) {
|
||||
} else if (actionType == WorkspaceMemberActionType.removeByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
|
@ -284,7 +284,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
}
|
||||
context
|
||||
.read<WorkspaceMemberBloc>()
|
||||
.add(WorkspaceMemberEvent.inviteWorkspaceMember(email));
|
||||
.add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email));
|
||||
// clear the email field after inviting
|
||||
emailController.clear();
|
||||
}
|
||||
|
|
|
@ -178,7 +178,7 @@ class _MemberItem extends StatelessWidget {
|
|||
showBottomBorder: false,
|
||||
onTap: () {
|
||||
workspaceMemberBloc.add(
|
||||
WorkspaceMemberEvent.removeWorkspaceMember(
|
||||
WorkspaceMemberEvent.removeWorkspaceMemberByEmail(
|
||||
member.email,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -435,7 +435,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||
messageType: ChatMessageTypePB.User,
|
||||
questionStreamPort: Int64(questionStream.nativePort),
|
||||
answerStreamPort: Int64(answerStream!.nativePort),
|
||||
metadata: await metadataPBFromMetadata(metadata),
|
||||
//metadata: await metadataPBFromMetadata(metadata),
|
||||
);
|
||||
if (format != null) {
|
||||
payload.format = format.toPB();
|
||||
|
|
|
@ -27,6 +27,7 @@ class ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {
|
|||
final payload = WorkspaceMemberIdPB(
|
||||
uid: Int64.parseInt(userId),
|
||||
);
|
||||
|
||||
await UserEventGetMemberInfo(payload).send().then((result) {
|
||||
result.fold(
|
||||
(member) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
|
@ -9,8 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:desktop_drop/desktop_drop.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/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
@ -51,14 +48,14 @@ class AIChatPage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) {
|
||||
return Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
||||
fontSize: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
// if (userProfile.authenticator != AuthTypePB.Server) {
|
||||
// return Center(
|
||||
// child: FlowyText(
|
||||
// LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
||||
// fontSize: 20,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
|
|
|
@ -36,7 +36,7 @@ class BlankPagePlugin extends Plugin {
|
|||
PluginWidgetBuilder get widgetBuilder => BlankPagePluginWidgetBuilder();
|
||||
|
||||
@override
|
||||
PluginId get id => "BlankStack";
|
||||
PluginId get id => "";
|
||||
|
||||
@override
|
||||
PluginType get pluginType => PluginType.blank;
|
||||
|
|
|
@ -143,12 +143,11 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
|
|||
(f) => null,
|
||||
);
|
||||
if (databaseMeta != null) {
|
||||
final result =
|
||||
await ViewBackendService.getView(databaseMeta.inlineViewId);
|
||||
final result = await ViewBackendService.getView(databaseMeta.viewId);
|
||||
return result.fold(
|
||||
(s) => DatabaseMeta(
|
||||
databaseId: databaseId,
|
||||
inlineViewId: databaseMeta.inlineViewId,
|
||||
viewId: databaseMeta.viewId,
|
||||
databaseName: s.name,
|
||||
),
|
||||
(f) => null,
|
||||
|
|
|
@ -17,11 +17,11 @@ class RelationDatabaseListCubit extends Cubit<RelationDatabaseListState> {
|
|||
.send()
|
||||
.fold<List<DatabaseMetaPB>>((s) => s.items, (f) => []);
|
||||
final futures = metaPBs.map((meta) {
|
||||
return ViewBackendService.getView(meta.inlineViewId).then(
|
||||
return ViewBackendService.getView(meta.viewId).then(
|
||||
(result) => result.fold(
|
||||
(s) => DatabaseMeta(
|
||||
databaseId: meta.databaseId,
|
||||
inlineViewId: meta.inlineViewId,
|
||||
viewId: meta.viewId,
|
||||
databaseName: s.name,
|
||||
),
|
||||
(f) => null,
|
||||
|
@ -43,10 +43,10 @@ class DatabaseMeta with _$DatabaseMeta {
|
|||
/// id of the database
|
||||
required String databaseId,
|
||||
|
||||
/// id of the inline view
|
||||
required String inlineViewId,
|
||||
/// id of the view
|
||||
required String viewId,
|
||||
|
||||
/// name of the database, currently identical to the name of the inline view
|
||||
/// name of the database
|
||||
required String databaseName,
|
||||
}) = _DatabaseMeta;
|
||||
}
|
||||
|
|
|
@ -30,9 +30,9 @@ class DatabaseSyncBloc extends Bloc<DatabaseSyncEvent, DatabaseSyncBlocState> {
|
|||
.then((value) => value.fold((s) => s, (f) => null));
|
||||
emit(
|
||||
state.copyWith(
|
||||
shouldShowIndicator: userProfile?.authenticator ==
|
||||
AuthenticatorPB.AppFlowyCloud &&
|
||||
databaseId != null,
|
||||
shouldShowIndicator:
|
||||
userProfile?.workspaceAuthType == AuthTypePB.Server &&
|
||||
databaseId != null,
|
||||
),
|
||||
);
|
||||
if (databaseId != null) {
|
||||
|
|
|
@ -256,7 +256,7 @@ class _CellEditorTitle extends StatelessWidget {
|
|||
}
|
||||
|
||||
void _openRelatedDatbase(BuildContext context) {
|
||||
FolderEventGetView(ViewIdPB(value: databaseMeta.inlineViewId))
|
||||
FolderEventGetView(ViewIdPB(value: databaseMeta.viewId))
|
||||
.send()
|
||||
.then((result) {
|
||||
result.fold(
|
||||
|
|
|
@ -21,8 +21,8 @@ import 'package:appflowy/shared/af_image.dart';
|
|||
import 'package:appflowy/shared/flowy_gradient_colors.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
@ -69,8 +69,8 @@ class RowBanner extends StatefulWidget {
|
|||
class _RowBannerState extends State<RowBanner> {
|
||||
final _isHovering = ValueNotifier(false);
|
||||
late final isLocalMode =
|
||||
(widget.userProfile?.authenticator ?? AuthenticatorPB.Local) ==
|
||||
AuthenticatorPB.Local;
|
||||
(widget.userProfile?.workspaceAuthType ?? AuthTypePB.Local) ==
|
||||
AuthTypePB.Local;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
|
@ -101,8 +101,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||
|
||||
bool get isLocalMode {
|
||||
final userProfilePB = state.userProfilePB;
|
||||
final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local;
|
||||
return type == AuthenticatorPB.Local;
|
||||
final type = userProfilePB?.workspaceAuthType ?? AuthTypePB.Local;
|
||||
return type == AuthTypePB.Local;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -32,7 +32,7 @@ class DocumentCollaboratorsBloc
|
|||
emit(
|
||||
state.copyWith(
|
||||
shouldShowIndicator:
|
||||
userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud,
|
||||
userProfile?.workspaceAuthType == AuthTypePB.Server,
|
||||
),
|
||||
);
|
||||
final deviceId = ApplicationInfo.deviceId;
|
||||
|
|
|
@ -31,7 +31,7 @@ class DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {
|
|||
emit(
|
||||
state.copyWith(
|
||||
shouldShowIndicator:
|
||||
userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud,
|
||||
userProfile?.workspaceAuthType == AuthTypePB.Server,
|
||||
),
|
||||
);
|
||||
_syncStateListener.start(
|
||||
|
|
|
@ -445,7 +445,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
|||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(appTheme.borderRadius.l),
|
||||
color: appTheme.surfaceColorScheme.primary,
|
||||
boxShadow: [appTheme.shadow.small],
|
||||
boxShadow: appTheme.shadow.small,
|
||||
),
|
||||
toolbarBuilder: (_, child, onDismiss, isMetricsChanged) =>
|
||||
BlocProvider.value(
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.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_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
|
@ -9,7 +8,6 @@ import 'package:appflowy_ui/appflowy_ui.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'operations/ai_writer_entities.dart';
|
||||
|
||||
|
@ -119,7 +117,7 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
|
|||
}
|
||||
|
||||
Widget buildChild(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme;
|
||||
final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme;
|
||||
final child = FlowyIconButton(
|
||||
width: 48,
|
||||
height: 32,
|
||||
|
@ -142,7 +140,7 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
|
|||
],
|
||||
),
|
||||
onPressed: () {
|
||||
if (_isAIEnabled(widget.editorState)) {
|
||||
if (_isAIWriterEnabled(widget.editorState)) {
|
||||
keepEditorFocusNotifier.increase();
|
||||
popoverController.show();
|
||||
setState(() {
|
||||
|
@ -159,7 +157,7 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
|
|||
return widget.tooltipBuilder?.call(
|
||||
context,
|
||||
_aiWriterToolbarItemId,
|
||||
_isAIEnabled(widget.editorState)
|
||||
_isAIWriterEnabled(widget.editorState)
|
||||
? LocaleKeys.document_plugins_aiWriter_userQuestion.tr()
|
||||
: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||
child,
|
||||
|
@ -188,10 +186,10 @@ class ImproveWritingButton extends StatelessWidget {
|
|||
icon: FlowySvg(
|
||||
FlowySvgs.toolbar_ai_improve_writing_m,
|
||||
size: Size.square(20.0),
|
||||
color: theme.iconColorTheme.primary,
|
||||
color: theme.iconColorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
if (_isAIEnabled(editorState)) {
|
||||
if (_isAIWriterEnabled(editorState)) {
|
||||
keepEditorFocusNotifier.increase();
|
||||
_insertAiNode(editorState, AiWriterCommand.improveWriting);
|
||||
} else {
|
||||
|
@ -205,7 +203,7 @@ class ImproveWritingButton extends StatelessWidget {
|
|||
return tooltipBuilder?.call(
|
||||
context,
|
||||
_aiWriterToolbarItemId,
|
||||
_isAIEnabled(editorState)
|
||||
_isAIWriterEnabled(editorState)
|
||||
? LocaleKeys.document_plugins_aiWriter_improveWriting.tr()
|
||||
: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||
child,
|
||||
|
@ -240,10 +238,8 @@ void _insertAiNode(EditorState editorState, AiWriterCommand command) async {
|
|||
);
|
||||
}
|
||||
|
||||
bool _isAIEnabled(EditorState editorState) {
|
||||
final documentContext = editorState.document.root.context;
|
||||
return documentContext == null ||
|
||||
!documentContext.read<DocumentBloc>().isLocalMode;
|
||||
bool _isAIWriterEnabled(EditorState editorState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool onlyShowInTextTypeAndExcludeTable(
|
||||
|
|
|
@ -315,6 +315,6 @@ ShapeDecoration buildToolbarLinkDecoration(
|
|||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
shadows: [theme.shadow.small],
|
||||
shadows: theme.shadow.small,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ import 'package:appflowy_backend/dispatch/error.dart';
|
|||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||
|
@ -184,8 +184,8 @@ Future<void> insertLocalFile(
|
|||
final fileType = file.fileType.toMediaFileTypePB();
|
||||
|
||||
// Check upload type
|
||||
final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) ==
|
||||
AuthenticatorPB.Local;
|
||||
final isLocalMode =
|
||||
(userProfile?.workspaceAuthType ?? AuthTypePB.Local) == AuthTypePB.Local;
|
||||
|
||||
String? path;
|
||||
String? errorMsg;
|
||||
|
@ -229,8 +229,8 @@ Future<void> insertLocalFiles(
|
|||
if (files.every((f) => f.path.isEmpty)) return;
|
||||
|
||||
// Check upload type
|
||||
final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) ==
|
||||
AuthenticatorPB.Local;
|
||||
final isLocalMode =
|
||||
(userProfile?.workspaceAuthType ?? AuthTypePB.Local) == AuthTypePB.Local;
|
||||
|
||||
for (final file in files) {
|
||||
final fileType = file.fileType.toMediaFileTypePB();
|
||||
|
|
|
@ -63,7 +63,7 @@ class _LinkEmbedMenuState extends State<LinkEmbedMenu> {
|
|||
|
||||
Widget buildChild() {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
iconScheme = theme.iconColorTheme,
|
||||
iconScheme = theme.iconColorScheme,
|
||||
fillScheme = theme.fillColorScheme;
|
||||
|
||||
return Container(
|
||||
|
@ -102,7 +102,7 @@ class _LinkEmbedMenuState extends State<LinkEmbedMenu> {
|
|||
}
|
||||
|
||||
Widget buildconvertBotton() {
|
||||
final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme;
|
||||
final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme;
|
||||
return AppFlowyPopover(
|
||||
offset: Offset(0, 6),
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
|
@ -170,7 +170,7 @@ class _LinkEmbedMenuState extends State<LinkEmbedMenu> {
|
|||
}
|
||||
|
||||
Widget buildMoreOptionBotton() {
|
||||
final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme;
|
||||
final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme;
|
||||
return AppFlowyPopover(
|
||||
offset: Offset(0, 6),
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
|
|
|
@ -161,25 +161,13 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget buildImage(BuildContext context) {
|
||||
if (imageUrl?.isEmpty ?? true) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
fillScheme = theme.fillColorScheme,
|
||||
iconScheme = theme.iconColorTheme;
|
||||
iconScheme = theme.iconColorScheme;
|
||||
final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0;
|
||||
Widget child;
|
||||
if (imageUrl?.isNotEmpty ?? false) {
|
||||
child = FlowyNetworkImage(
|
||||
url: imageUrl!,
|
||||
width: width,
|
||||
);
|
||||
} else {
|
||||
child = Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toolbar_link_earth_m,
|
||||
color: iconScheme.secondary,
|
||||
size: Size.square(30),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
|
@ -188,7 +176,17 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
|||
child: Container(
|
||||
width: width,
|
||||
color: fillScheme.quaternary,
|
||||
child: child,
|
||||
child: FlowyNetworkImage(
|
||||
url: imageUrl!,
|
||||
width: width,
|
||||
errorWidgetBuilder: (_, __, ___) => Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toolbar_link_earth_m,
|
||||
color: iconScheme.secondary,
|
||||
size: Size.square(30),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:appflowy_backend/log.dart';
|
|||
// ignore: depend_on_referenced_packages
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
|
||||
abstract class LinkInfoParser {
|
||||
Future<LinkInfo?> parse(
|
||||
|
@ -38,12 +39,19 @@ class DefaultParser implements LinkInfoParser {
|
|||
if (code != 200 && isHome) {
|
||||
throw Exception('Http request error: $code');
|
||||
}
|
||||
// else if (!isHome && code == 403) {
|
||||
// uri = Uri.parse('${uri.scheme}://${uri.host}/');
|
||||
// response = await http.get(uri).timeout(timeout);
|
||||
// }
|
||||
|
||||
final document = html_parser.parse(response.body);
|
||||
final contentType = response.headers['content-type'];
|
||||
final charset = contentType?.split('charset=').lastOrNull;
|
||||
String body = '';
|
||||
if (charset == null ||
|
||||
charset.toLowerCase() == 'latin-1' ||
|
||||
charset.toLowerCase() == 'iso-8859-1') {
|
||||
body = latin1.decode(response.bodyBytes);
|
||||
} else {
|
||||
body = utf8.decode(response.bodyBytes, allowMalformed: true);
|
||||
}
|
||||
|
||||
final document = html_parser.parse(body);
|
||||
|
||||
final siteName = document
|
||||
.querySelector('meta[property="og:site_name"]')
|
||||
|
|
|
@ -148,7 +148,7 @@ class _PasteAsMenuState extends State<PasteAsMenu> {
|
|||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: theme.surfaceColorScheme.primary,
|
||||
boxShadow: [theme.shadow.medium],
|
||||
boxShadow: theme.shadow.medium,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
|
@ -226,7 +226,7 @@ class PageStyleCoverImage extends StatelessWidget {
|
|||
(f) => null,
|
||||
);
|
||||
final isAppFlowyCloud =
|
||||
userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud;
|
||||
userProfile?.workspaceAuthType == AuthTypePB.Server;
|
||||
final PageStyleCoverImageType type;
|
||||
if (!isAppFlowyCloud) {
|
||||
result = await saveImageToLocalStorage(path);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart';
|
||||
import 'package:appflowy/plugins/emoji/emoji_actions_command.dart';
|
||||
import 'package:appflowy/plugins/emoji/emoji_menu.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'slash_menu_item_builder.dart';
|
||||
|
||||
|
@ -37,11 +40,16 @@ extension on EditorState {
|
|||
}) async {
|
||||
final container = Overlay.of(context);
|
||||
menuService.dismiss();
|
||||
showEmojiPickerMenu(
|
||||
container,
|
||||
this,
|
||||
menuService.alignment,
|
||||
menuService.offset,
|
||||
);
|
||||
if (UniversalPlatform.isMobile || selection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final node = getNodeAtPath(selection!.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || node.type == CodeBlockKeys.type) {
|
||||
return;
|
||||
}
|
||||
emojiMenuService = EmojiMenu(editorState: this, overlay: container);
|
||||
emojiMenuService?.show('');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ class _FormatToolbarItem extends ToolbarItem {
|
|||
size: Size.square(20.0),
|
||||
color: (isDark && isHighlight)
|
||||
? Color(0xFF282E3A)
|
||||
: theme.iconColorTheme.primary,
|
||||
: theme.iconColorScheme.primary,
|
||||
),
|
||||
onPressed: () => editorState.toggleAttribute(
|
||||
name,
|
||||
|
|
|
@ -83,7 +83,7 @@ class _HighlightColorPickerWidgetState
|
|||
|
||||
Widget buildChild(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
iconColor = theme.iconColorTheme.primary;
|
||||
iconColor = theme.iconColorScheme.primary;
|
||||
|
||||
final child = FlowyIconButton(
|
||||
width: 36,
|
||||
|
|
|
@ -45,7 +45,7 @@ final customLinkItem = ToolbarItem(
|
|||
size: Size.square(20.0),
|
||||
color: (isDark && isHref)
|
||||
? Color(0xFF282E3A)
|
||||
: theme.iconColorTheme.primary,
|
||||
: theme.iconColorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
getIt<FloatingToolbarController>().hideToolbar();
|
||||
|
|
|
@ -97,7 +97,7 @@ class _TextAlignActionListState extends State<TextAlignActionList> {
|
|||
|
||||
Widget buildChild(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
iconColor = theme.iconColorTheme.primary;
|
||||
iconColor = theme.iconColorScheme.primary;
|
||||
final child = FlowyIconButton(
|
||||
width: 48,
|
||||
height: 32,
|
||||
|
|
|
@ -82,7 +82,7 @@ class _TextColorPickerWidgetState extends State<TextColorPickerWidget> {
|
|||
|
||||
Widget buildChild(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
iconColor = theme.iconColorTheme.primary;
|
||||
iconColor = theme.iconColorScheme.primary;
|
||||
final child = FlowyIconButton(
|
||||
width: 36,
|
||||
height: 32,
|
||||
|
|
|
@ -79,7 +79,7 @@ class _TextHeadingActionListState extends State<TextHeadingActionList> {
|
|||
|
||||
Widget buildChild(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
iconColor = theme.iconColorTheme.primary;
|
||||
iconColor = theme.iconColorScheme.primary;
|
||||
final child = FlowyIconButton(
|
||||
width: 48,
|
||||
height: 32,
|
||||
|
|
|
@ -120,7 +120,7 @@ class _SuggestionsActionListState extends State<SuggestionsActionList> {
|
|||
|
||||
Widget buildChild(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
iconColor = theme.iconColorTheme.primary;
|
||||
iconColor = theme.iconColorScheme.primary;
|
||||
final child = FlowyHover(
|
||||
isSelected: () => isSelected,
|
||||
style: HoverStyle(
|
||||
|
|
|
@ -316,9 +316,12 @@ class EditorStyleCustomizer {
|
|||
}
|
||||
|
||||
TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) {
|
||||
if (fontFamily == null || fontFamily == defaultFontFamily) {
|
||||
if (fontFamily == null) {
|
||||
return TextStyle(fontWeight: fontWeight);
|
||||
} else if (fontFamily == defaultFontFamily) {
|
||||
return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight);
|
||||
}
|
||||
|
||||
try {
|
||||
return getGoogleFontSafely(fontFamily, fontWeight: fontWeight);
|
||||
} on Exception {
|
||||
|
|
|
@ -18,7 +18,7 @@ CharacterShortcutEvent emojiCommand(BuildContext context) =>
|
|||
},
|
||||
handlerWithCharacter: (editorState, character) {
|
||||
emojiMenuService = EmojiMenu(
|
||||
context: context,
|
||||
overlay: Overlay.of(context),
|
||||
editorState: editorState,
|
||||
);
|
||||
return emojiCommandHandler(editorState, context, character);
|
||||
|
@ -40,10 +40,7 @@ Future<bool> emojiCommandHandler(
|
|||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null ||
|
||||
delta == null ||
|
||||
delta.isEmpty ||
|
||||
node.type == CodeBlockKeys.type) {
|
||||
if (node == null || delta == null || node.type == CodeBlockKeys.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ class EmojiHandler extends StatefulWidget {
|
|||
required this.onDismiss,
|
||||
required this.onSelectionUpdate,
|
||||
required this.onEmojiSelect,
|
||||
this.startCharAmount = 1,
|
||||
this.cancelBySpaceHandler,
|
||||
this.initialSearchText = '',
|
||||
});
|
||||
|
@ -32,7 +31,6 @@ class EmojiHandler extends StatefulWidget {
|
|||
final VoidCallback onDismiss;
|
||||
final VoidCallback onSelectionUpdate;
|
||||
final SelectEmojiItemHandler onEmojiSelect;
|
||||
final int startCharAmount;
|
||||
final String initialSearchText;
|
||||
final bool Function()? cancelBySpaceHandler;
|
||||
|
||||
|
@ -54,6 +52,8 @@ class _EmojiHandlerState extends State<EmojiHandler> {
|
|||
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
|
||||
);
|
||||
|
||||
int get startCharAmount => widget.initialSearchText.length;
|
||||
|
||||
set search(String search) {
|
||||
_search = search;
|
||||
_doSearch();
|
||||
|
@ -68,7 +68,8 @@ class _EmojiHandlerState extends State<EmojiHandler> {
|
|||
(_) => focusNode.requestFocus(),
|
||||
);
|
||||
|
||||
startOffset = (widget.editorState.selection?.endIndex ?? 0) - 1;
|
||||
startOffset =
|
||||
(widget.editorState.selection?.endIndex ?? 0) - startCharAmount;
|
||||
|
||||
if (kCachedEmojiData != null) {
|
||||
loadEmojis(kCachedEmojiData!);
|
||||
|
@ -194,7 +195,8 @@ class _EmojiHandlerState extends State<EmojiHandler> {
|
|||
|
||||
void _doSearch() {
|
||||
if (!loaded || !mounted) return;
|
||||
if (_search.startsWith(' ') || _search.isEmpty) {
|
||||
final enableEmptySearch = widget.initialSearchText.isEmpty;
|
||||
if ((_search.startsWith(' ') || _search.isEmpty) && !enableEmptySearch) {
|
||||
widget.onDismiss.call();
|
||||
return;
|
||||
}
|
||||
|
@ -232,6 +234,10 @@ class _EmojiHandlerState extends State<EmojiHandler> {
|
|||
widget.onDismiss.call();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
if (_search.isEmpty) {
|
||||
if (widget.initialSearchText.isEmpty) {
|
||||
widget.onDismiss.call();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (_canDeleteLastCharacter()) {
|
||||
widget.editorState.deleteBackward();
|
||||
} else {
|
||||
|
@ -276,7 +282,7 @@ class _EmojiHandlerState extends State<EmojiHandler> {
|
|||
void onSelect(int index) {
|
||||
widget.onEmojiSelect.call(
|
||||
context,
|
||||
(startOffset - widget.startCharAmount, startOffset + _search.length),
|
||||
(startOffset - startCharAmount, startOffset + _search.length),
|
||||
emojiData.getEmojiById(searchedEmojis[index].id),
|
||||
);
|
||||
widget.onDismiss.call();
|
||||
|
|
|
@ -12,21 +12,19 @@ abstract class EmojiMenuService {
|
|||
|
||||
class EmojiMenu extends EmojiMenuService {
|
||||
EmojiMenu({
|
||||
required this.context,
|
||||
required this.overlay,
|
||||
required this.editorState,
|
||||
this.startCharAmount = 1,
|
||||
this.cancelBySpaceHandler,
|
||||
this.menuHeight = 400,
|
||||
this.menuWidth = 300,
|
||||
});
|
||||
|
||||
final BuildContext context;
|
||||
final EditorState editorState;
|
||||
final double menuHeight;
|
||||
final double menuWidth;
|
||||
final OverlayState overlay;
|
||||
final bool Function()? cancelBySpaceHandler;
|
||||
|
||||
final int startCharAmount;
|
||||
Offset _offset = Offset.zero;
|
||||
Alignment _alignment = Alignment.topLeft;
|
||||
OverlayEntry? _menuEntry;
|
||||
|
@ -97,7 +95,6 @@ class EmojiMenu extends EmojiMenuService {
|
|||
menuService: this,
|
||||
onDismiss: dismiss,
|
||||
onSelectionUpdate: _onSelectionUpdate,
|
||||
startCharAmount: startCharAmount,
|
||||
cancelBySpaceHandler: cancelBySpaceHandler,
|
||||
initialSearchText: initialCharacter,
|
||||
onEmojiSelect: (
|
||||
|
@ -132,8 +129,9 @@ class EmojiMenu extends EmojiMenuService {
|
|||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_menuEntry!);
|
||||
overlay.insert(_menuEntry!);
|
||||
|
||||
keepEditorFocusNotifier.increase();
|
||||
editorState.service.keyboardService?.disable(showCursor: true);
|
||||
editorState.service.scrollService?.disable();
|
||||
selectionService.currentSelection.addListener(_onSelectionChange);
|
||||
|
|
|
@ -193,7 +193,7 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
|
|||
Future<void> _updatePublishStatus(Emitter<ShareState> emit) async {
|
||||
final publishInfo = await ViewBackendService.getPublishInfo(view);
|
||||
final enablePublish = await UserBackendService.getCurrentUserProfile().fold(
|
||||
(v) => v.authenticator == AuthenticatorPB.AppFlowyCloud,
|
||||
(v) => v.workspaceAuthType == AuthTypePB.Server,
|
||||
(p) => false,
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
|
||||
extension UserProfilePBExtension on UserProfilePB {
|
||||
String? get authToken {
|
||||
try {
|
||||
final map = jsonDecode(token) as Map<String, dynamic>;
|
||||
return map['access_token'] as String?;
|
||||
} catch (e) {
|
||||
Log.error('Failed to decode auth token: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import 'package:appflowy/startup/startup.dart';
|
|||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/util/default_extensions.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
@ -294,8 +294,8 @@ class _IconUploaderState extends State<IconUploader> {
|
|||
(userProfile) => userProfile,
|
||||
(l) => null,
|
||||
);
|
||||
final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) ==
|
||||
AuthenticatorPB.Local;
|
||||
final isLocalMode = (userProfile?.workspaceAuthType ?? AuthTypePB.Local) ==
|
||||
AuthTypePB.Local;
|
||||
if (isLocalMode) {
|
||||
result = await pickedImages.first.saveToLocal();
|
||||
} else {
|
||||
|
|
|
@ -102,7 +102,7 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) {
|
|||
case AuthenticatorType.local:
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => BackendAuthService(
|
||||
AuthenticatorPB.Local,
|
||||
AuthTypePB.Local,
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
|
|
@ -7,11 +7,13 @@ import 'package:appflowy/shared/feature_flags.dart';
|
|||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/notification/notification_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
|
@ -138,6 +140,8 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
|
|||
|
||||
final _commandPaletteNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
final themeBuilder = AppFlowyDefaultTheme();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -234,27 +238,35 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
|
|||
supportedLocales: context.supportedLocales,
|
||||
locale: state.locale,
|
||||
routerConfig: routerConfig,
|
||||
builder: (context, child) => AppFlowyTheme(
|
||||
data: Theme.of(context).brightness == Brightness.light
|
||||
? AppFlowyThemeData.light()
|
||||
: AppFlowyThemeData.dark(),
|
||||
child: MediaQuery(
|
||||
// use the 1.0 as the textScaleFactor to avoid the text size
|
||||
// affected by the system setting.
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(state.textScaleFactor),
|
||||
builder: (context, child) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
final fontFamily =
|
||||
state.font.orDefault(defaultFontFamily);
|
||||
|
||||
return AppFlowyTheme(
|
||||
data: brightness == Brightness.light
|
||||
? themeBuilder.light(fontFamily: fontFamily)
|
||||
: themeBuilder.dark(fontFamily: fontFamily),
|
||||
child: MediaQuery(
|
||||
// use the 1.0 as the textScaleFactor to avoid the text size
|
||||
// affected by the system setting.
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler:
|
||||
TextScaler.linear(state.textScaleFactor),
|
||||
),
|
||||
child: overlayManagerBuilder(
|
||||
context,
|
||||
!UniversalPlatform.isMobile &&
|
||||
FeatureFlag.search.isOn
|
||||
? CommandPalette(
|
||||
notifier: _commandPaletteNotifier,
|
||||
child: child,
|
||||
)
|
||||
: child,
|
||||
),
|
||||
),
|
||||
child: overlayManagerBuilder(
|
||||
context,
|
||||
!UniversalPlatform.isMobile && FeatureFlag.search.isOn
|
||||
? CommandPalette(
|
||||
notifier: _commandPaletteNotifier,
|
||||
child: child,
|
||||
)
|
||||
: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -112,7 +112,7 @@ class AppFlowyCloudDeepLink {
|
|||
(_) async {
|
||||
final deviceId = await getDeviceId();
|
||||
final payload = OauthSignInPB(
|
||||
authenticator: AuthenticatorPB.AppFlowyCloud,
|
||||
authenticator: AuthTypePB.Server,
|
||||
map: {
|
||||
AuthServiceMapKeys.signInURL: uri.toString(),
|
||||
AuthServiceMapKeys.deviceId: deviceId,
|
||||
|
|
|
@ -51,7 +51,6 @@ GoRouter generateRouter(Widget child) {
|
|||
// Routes in both desktop and mobile
|
||||
_signInScreenRoute(),
|
||||
_skipLogInScreenRoute(),
|
||||
_encryptSecretScreenRoute(),
|
||||
_workspaceErrorScreenRoute(),
|
||||
// Desktop only
|
||||
if (UniversalPlatform.isDesktop) _desktopHomeScreenRoute(),
|
||||
|
@ -120,18 +119,6 @@ GoRouter generateRouter(Widget child) {
|
|||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: SignUpScreen.routeName,
|
||||
pageBuilder: (context, state) {
|
||||
return CustomTransitionPage(
|
||||
child: SignUpScreen(
|
||||
router: getIt<AuthRouter>(),
|
||||
),
|
||||
transitionsBuilder: _buildFadeTransition,
|
||||
transitionDuration: _slowDuration,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -471,23 +458,6 @@ GoRoute _workspaceErrorScreenRoute() {
|
|||
);
|
||||
}
|
||||
|
||||
GoRoute _encryptSecretScreenRoute() {
|
||||
return GoRoute(
|
||||
path: EncryptSecretScreen.routeName,
|
||||
pageBuilder: (context, state) {
|
||||
final args = state.extra as Map<String, dynamic>;
|
||||
return CustomTransitionPage(
|
||||
child: EncryptSecretScreen(
|
||||
user: args[EncryptSecretScreen.argUser],
|
||||
key: args[EncryptSecretScreen.argKey],
|
||||
),
|
||||
transitionsBuilder: _buildFadeTransition,
|
||||
transitionDuration: _slowDuration,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GoRoute _skipLogInScreenRoute() {
|
||||
return GoRoute(
|
||||
path: SkipLogInScreen.routeName,
|
||||
|
|
|
@ -18,7 +18,7 @@ class AppFlowyCloudAuthService implements AuthService {
|
|||
AppFlowyCloudAuthService();
|
||||
|
||||
final BackendAuthService _backendAuthService = BackendAuthService(
|
||||
AuthenticatorPB.AppFlowyCloud,
|
||||
AuthTypePB.Server,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
|
@ -20,7 +20,7 @@ class AppFlowyCloudMockAuthService implements AuthService {
|
|||
final String userEmail;
|
||||
|
||||
final BackendAuthService _appFlowyAuthService =
|
||||
BackendAuthService(AuthenticatorPB.AppFlowyCloud);
|
||||
BackendAuthService(AuthTypePB.Server);
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserProfilePB, FlowyError>> signUp({
|
||||
|
@ -48,7 +48,7 @@ class AppFlowyCloudMockAuthService implements AuthService {
|
|||
Map<String, String> params = const {},
|
||||
}) async {
|
||||
final payload = SignInUrlPayloadPB.create()
|
||||
..authenticator = AuthenticatorPB.AppFlowyCloud
|
||||
..authenticator = AuthTypePB.Server
|
||||
// don't use nanoid here, the gotrue server will transform the email
|
||||
..email = userEmail;
|
||||
|
||||
|
@ -58,7 +58,7 @@ class AppFlowyCloudMockAuthService implements AuthService {
|
|||
return getSignInURLResult.fold(
|
||||
(urlPB) async {
|
||||
final payload = OauthSignInPB(
|
||||
authenticator: AuthenticatorPB.AppFlowyCloud,
|
||||
authenticator: AuthTypePB.Server,
|
||||
map: {
|
||||
AuthServiceMapKeys.signInURL: urlPB.signInUrl,
|
||||
AuthServiceMapKeys.deviceId: deviceId,
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|||
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||
show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
|
@ -15,7 +16,7 @@ import 'device_id.dart';
|
|||
class BackendAuthService implements AuthService {
|
||||
BackendAuthService(this.authType);
|
||||
|
||||
final AuthenticatorPB authType;
|
||||
final AuthTypePB authType;
|
||||
|
||||
@override
|
||||
Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>
|
||||
|
@ -71,7 +72,7 @@ class BackendAuthService implements AuthService {
|
|||
..email = userEmail
|
||||
..password = password
|
||||
// When sign up as guest, the auth type is always local.
|
||||
..authType = AuthenticatorPB.Local
|
||||
..authType = AuthTypePB.Local
|
||||
..deviceId = await getDeviceId();
|
||||
final response = await UserEventSignUp(request).send().then(
|
||||
(value) => value,
|
||||
|
@ -82,7 +83,7 @@ class BackendAuthService implements AuthService {
|
|||
@override
|
||||
Future<FlowyResult<UserProfilePB, FlowyError>> signUpWithOAuth({
|
||||
required String platform,
|
||||
AuthenticatorPB authType = AuthenticatorPB.Local,
|
||||
AuthTypePB authType = AuthTypePB.Local,
|
||||
Map<String, String> params = const {},
|
||||
}) async {
|
||||
return FlowyResult.failure(
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import 'package:appflowy/plugins/database/application/defines.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'auth/auth_service.dart';
|
||||
|
||||
part 'encrypt_secret_bloc.freezed.dart';
|
||||
|
||||
class EncryptSecretBloc extends Bloc<EncryptSecretEvent, EncryptSecretState> {
|
||||
EncryptSecretBloc({required this.user})
|
||||
: super(EncryptSecretState.initial()) {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final UserProfilePB user;
|
||||
|
||||
void _dispatch() {
|
||||
on<EncryptSecretEvent>((event, emit) async {
|
||||
await event.when(
|
||||
setEncryptSecret: (secret) async {
|
||||
if (isLoading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final payload = UserSecretPB.create()
|
||||
..encryptionSecret = secret
|
||||
..encryptionSign = user.encryptionSign
|
||||
..encryptionType = user.encryptionType
|
||||
..userId = user.id;
|
||||
final result = await UserEventSetEncryptionSecret(payload).send();
|
||||
if (!isClosed) {
|
||||
add(EncryptSecretEvent.didFinishCheck(result));
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: const LoadingState.loading(),
|
||||
successOrFail: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
cancelInputSecret: () async {
|
||||
await getIt<AuthService>().signOut();
|
||||
emit(
|
||||
state.copyWith(
|
||||
successOrFail: null,
|
||||
isSignOut: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
didFinishCheck: (result) {
|
||||
result.fold(
|
||||
(unit) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: const LoadingState.loading(),
|
||||
successOrFail: result,
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: LoadingState.finish(FlowyResult.failure(err)),
|
||||
successOrFail: result,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
bool isLoading() {
|
||||
final loadingState = state.loadingState;
|
||||
if (loadingState != null) {
|
||||
return loadingState.when(
|
||||
loading: () => true,
|
||||
finish: (_) => false,
|
||||
idle: () => false,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class EncryptSecretEvent with _$EncryptSecretEvent {
|
||||
const factory EncryptSecretEvent.setEncryptSecret(String secret) =
|
||||
_SetEncryptSecret;
|
||||
const factory EncryptSecretEvent.didFinishCheck(
|
||||
FlowyResult<void, FlowyError> result,
|
||||
) = _DidFinishCheck;
|
||||
const factory EncryptSecretEvent.cancelInputSecret() = _CancelInputSecret;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class EncryptSecretState with _$EncryptSecretState {
|
||||
const factory EncryptSecretState({
|
||||
required FlowyResult<void, FlowyError>? successOrFail,
|
||||
required bool isSignOut,
|
||||
LoadingState? loadingState,
|
||||
}) = _EncryptSecretState;
|
||||
|
||||
factory EncryptSecretState.initial() => const EncryptSecretState(
|
||||
successOrFail: null,
|
||||
isSignOut: false,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/user/application/password/password_http_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'password_bloc.freezed.dart';
|
||||
|
||||
class PasswordBloc extends Bloc<PasswordEvent, PasswordState> {
|
||||
PasswordBloc(this.userProfile) : super(PasswordState.initial()) {
|
||||
on<PasswordEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
init: () async => _init(),
|
||||
changePassword: (oldPassword, newPassword) async => _onChangePassword(
|
||||
emit,
|
||||
oldPassword: oldPassword,
|
||||
newPassword: newPassword,
|
||||
),
|
||||
setupPassword: (newPassword) async => _onSetupPassword(
|
||||
emit,
|
||||
newPassword: newPassword,
|
||||
),
|
||||
forgotPassword: (email) async => _onForgotPassword(
|
||||
emit,
|
||||
email: email,
|
||||
),
|
||||
checkHasPassword: () async => _onCheckHasPassword(
|
||||
emit,
|
||||
),
|
||||
cancel: () {},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
late final PasswordHttpService passwordHttpService;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
Future<void> _init() async {
|
||||
if (userProfile.workspaceAuthType == AuthTypePB.Local) {
|
||||
Log.debug('PasswordBloc: skip init because user is local authenticator');
|
||||
return;
|
||||
}
|
||||
|
||||
final baseUrl = await getAppFlowyCloudUrl();
|
||||
try {
|
||||
final authToken = jsonDecode(userProfile.token)['access_token'];
|
||||
passwordHttpService = PasswordHttpService(
|
||||
baseUrl: baseUrl,
|
||||
authToken: authToken,
|
||||
);
|
||||
_isInitialized = true;
|
||||
} catch (e) {
|
||||
Log.error('PasswordBloc: _init: error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChangePassword(
|
||||
Emitter<PasswordState> emit, {
|
||||
required String oldPassword,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
Log.info('changePassword: not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isSubmitting) {
|
||||
Log.info('changePassword: already submitting');
|
||||
return;
|
||||
}
|
||||
|
||||
_clearState(emit, true);
|
||||
|
||||
final result = await passwordHttpService.changePassword(
|
||||
currentPassword: oldPassword,
|
||||
newPassword: newPassword,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
changePasswordResult: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSetupPassword(
|
||||
Emitter<PasswordState> emit, {
|
||||
required String newPassword,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
Log.info('setupPassword: not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isSubmitting) {
|
||||
Log.info('setupPassword: already submitting');
|
||||
return;
|
||||
}
|
||||
|
||||
_clearState(emit, true);
|
||||
|
||||
final result = await passwordHttpService.setupPassword(
|
||||
newPassword: newPassword,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
hasPassword: result.fold(
|
||||
(success) => true,
|
||||
(error) => false,
|
||||
),
|
||||
setupPasswordResult: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onForgotPassword(
|
||||
Emitter<PasswordState> emit, {
|
||||
required String email,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
Log.info('forgotPassword: not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isSubmitting) {
|
||||
Log.info('forgotPassword: already submitting');
|
||||
return;
|
||||
}
|
||||
|
||||
_clearState(emit, true);
|
||||
|
||||
final result = await passwordHttpService.forgotPassword(email: email);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
forgotPasswordResult: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onCheckHasPassword(Emitter<PasswordState> emit) async {
|
||||
if (!_isInitialized) {
|
||||
Log.info('checkHasPassword: not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isSubmitting) {
|
||||
Log.info('checkHasPassword: already submitting');
|
||||
return;
|
||||
}
|
||||
|
||||
_clearState(emit, true);
|
||||
|
||||
final result = await passwordHttpService.checkHasPassword();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: false,
|
||||
hasPassword: result.fold(
|
||||
(success) => success,
|
||||
(error) => false,
|
||||
),
|
||||
checkHasPasswordResult: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _clearState(Emitter<PasswordState> emit, bool isSubmitting) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSubmitting: isSubmitting,
|
||||
changePasswordResult: null,
|
||||
setupPasswordResult: null,
|
||||
forgotPasswordResult: null,
|
||||
checkHasPasswordResult: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class PasswordEvent with _$PasswordEvent {
|
||||
const factory PasswordEvent.init() = Init;
|
||||
|
||||
// Change password
|
||||
const factory PasswordEvent.changePassword({
|
||||
required String oldPassword,
|
||||
required String newPassword,
|
||||
}) = ChangePassword;
|
||||
|
||||
// Setup password
|
||||
const factory PasswordEvent.setupPassword({
|
||||
required String newPassword,
|
||||
}) = SetupPassword;
|
||||
|
||||
// Forgot password
|
||||
const factory PasswordEvent.forgotPassword({
|
||||
required String email,
|
||||
}) = ForgotPassword;
|
||||
|
||||
// Check has password
|
||||
const factory PasswordEvent.checkHasPassword() = CheckHasPassword;
|
||||
|
||||
// Cancel operation
|
||||
const factory PasswordEvent.cancel() = Cancel;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class PasswordState with _$PasswordState {
|
||||
const factory PasswordState({
|
||||
required bool isSubmitting,
|
||||
required bool hasPassword,
|
||||
required FlowyResult<bool, FlowyError>? changePasswordResult,
|
||||
required FlowyResult<bool, FlowyError>? setupPasswordResult,
|
||||
required FlowyResult<bool, FlowyError>? forgotPasswordResult,
|
||||
required FlowyResult<bool, FlowyError>? checkHasPasswordResult,
|
||||
}) = _PasswordState;
|
||||
|
||||
factory PasswordState.initial() => const PasswordState(
|
||||
isSubmitting: false,
|
||||
hasPassword: false,
|
||||
changePasswordResult: null,
|
||||
setupPasswordResult: null,
|
||||
forgotPasswordResult: null,
|
||||
checkHasPasswordResult: null,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
enum PasswordEndpoint {
|
||||
changePassword,
|
||||
forgotPassword,
|
||||
setupPassword,
|
||||
checkHasPassword;
|
||||
|
||||
String get path {
|
||||
switch (this) {
|
||||
case PasswordEndpoint.changePassword:
|
||||
return '/gotrue/user/change-password';
|
||||
case PasswordEndpoint.forgotPassword:
|
||||
return '/gotrue/user/recover';
|
||||
case PasswordEndpoint.setupPassword:
|
||||
return '/gotrue/user/change-password';
|
||||
case PasswordEndpoint.checkHasPassword:
|
||||
return '/gotrue/user/auth-info';
|
||||
}
|
||||
}
|
||||
|
||||
String get method {
|
||||
switch (this) {
|
||||
case PasswordEndpoint.changePassword:
|
||||
case PasswordEndpoint.setupPassword:
|
||||
case PasswordEndpoint.forgotPassword:
|
||||
return 'POST';
|
||||
case PasswordEndpoint.checkHasPassword:
|
||||
return 'GET';
|
||||
}
|
||||
}
|
||||
|
||||
Uri uri(String baseUrl) => Uri.parse('$baseUrl$path');
|
||||
}
|
||||
|
||||
class PasswordHttpService {
|
||||
PasswordHttpService({
|
||||
required this.baseUrl,
|
||||
required this.authToken,
|
||||
});
|
||||
|
||||
final String baseUrl;
|
||||
final String authToken;
|
||||
|
||||
final http.Client client = http.Client();
|
||||
|
||||
Map<String, String> get headers => {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $authToken',
|
||||
};
|
||||
|
||||
/// Changes the user's password
|
||||
///
|
||||
/// [currentPassword] - The user's current password
|
||||
/// [newPassword] - The new password to set
|
||||
Future<FlowyResult<bool, FlowyError>> changePassword({
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
final result = await _makeRequest(
|
||||
endpoint: PasswordEndpoint.changePassword,
|
||||
body: {
|
||||
'current_password': currentPassword,
|
||||
'password': newPassword,
|
||||
},
|
||||
errorMessage: 'Failed to change password',
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(data) => FlowyResult.success(true),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends a password reset email to the user
|
||||
///
|
||||
/// [email] - The email address of the user
|
||||
Future<FlowyResult<bool, FlowyError>> forgotPassword({
|
||||
required String email,
|
||||
}) async {
|
||||
final result = await _makeRequest(
|
||||
endpoint: PasswordEndpoint.forgotPassword,
|
||||
body: {'email': email},
|
||||
errorMessage: 'Failed to send password reset email',
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(data) => FlowyResult.success(true),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets up a password for a user that doesn't have one
|
||||
///
|
||||
/// [newPassword] - The new password to set
|
||||
Future<FlowyResult<bool, FlowyError>> setupPassword({
|
||||
required String newPassword,
|
||||
}) async {
|
||||
final result = await _makeRequest(
|
||||
endpoint: PasswordEndpoint.setupPassword,
|
||||
body: {'password': newPassword},
|
||||
errorMessage: 'Failed to setup password',
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(data) => FlowyResult.success(true),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks if the user has a password set
|
||||
Future<FlowyResult<bool, FlowyError>> checkHasPassword() async {
|
||||
final result = await _makeRequest(
|
||||
endpoint: PasswordEndpoint.checkHasPassword,
|
||||
errorMessage: 'Failed to check password status',
|
||||
);
|
||||
|
||||
try {
|
||||
return result.fold(
|
||||
(data) => FlowyResult.success(data['has_password'] ?? false),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
} catch (e) {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(msg: 'Failed to check password status: $e'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a request to the specified endpoint with the given body
|
||||
Future<FlowyResult<dynamic, FlowyError>> _makeRequest({
|
||||
required PasswordEndpoint endpoint,
|
||||
Map<String, dynamic>? body,
|
||||
String errorMessage = 'Request failed',
|
||||
}) async {
|
||||
try {
|
||||
final uri = endpoint.uri(baseUrl);
|
||||
http.Response response;
|
||||
|
||||
if (endpoint.method == 'POST') {
|
||||
response = await client.post(
|
||||
uri,
|
||||
headers: headers,
|
||||
body: body != null ? jsonEncode(body) : null,
|
||||
);
|
||||
} else if (endpoint.method == 'GET') {
|
||||
response = await client.get(
|
||||
uri,
|
||||
headers: headers,
|
||||
);
|
||||
} else {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(msg: 'Invalid request method: ${endpoint.method}'),
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
if (response.body.isNotEmpty) {
|
||||
return FlowyResult.success(jsonDecode(response.body));
|
||||
}
|
||||
return FlowyResult.success(true);
|
||||
} else {
|
||||
final errorBody =
|
||||
response.body.isNotEmpty ? jsonDecode(response.body) : {};
|
||||
|
||||
Log.info(
|
||||
'${endpoint.name} request failed: ${response.statusCode}, $errorBody ',
|
||||
);
|
||||
|
||||
return FlowyResult.failure(
|
||||
FlowyError(
|
||||
msg: errorBody['msg'] ?? errorMessage,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('${endpoint.name} request failed: error: $e');
|
||||
|
||||
return FlowyResult.failure(
|
||||
FlowyError(msg: 'Network error: ${e.toString()}'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -303,6 +303,8 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
|||
msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr();
|
||||
} else if (errorMsg.contains('invalid')) {
|
||||
msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr();
|
||||
} else if (errorMsg.contains('Invalid login credentials')) {
|
||||
msg = LocaleKeys.signIn_invalidLoginCredentials.tr();
|
||||
}
|
||||
return state.copyWith(
|
||||
isSubmitting: false,
|
||||
|
|
|
@ -24,7 +24,7 @@ typedef DidUpdateUserWorkspacesCallback = void Function(
|
|||
);
|
||||
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
||||
typedef DidUpdateUserWorkspaceSetting = void Function(
|
||||
UseAISettingPB settings,
|
||||
WorkspaceSettingsPB settings,
|
||||
);
|
||||
|
||||
class UserListener {
|
||||
|
@ -101,10 +101,10 @@ class UserListener {
|
|||
result.map(
|
||||
(r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)),
|
||||
);
|
||||
case user.UserNotification.DidUpdateAISetting:
|
||||
case user.UserNotification.DidUpdateWorkspaceSetting:
|
||||
result.map(
|
||||
(r) =>
|
||||
onUserWorkspaceSettingUpdated?.call(UseAISettingPB.fromBuffer(r)),
|
||||
(r) => onUserWorkspaceSettingUpdated
|
||||
?.call(WorkspaceSettingsPB.fromBuffer(r)),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
|
@ -113,22 +113,21 @@ class UserListener {
|
|||
}
|
||||
}
|
||||
|
||||
typedef WorkspaceSettingNotifyValue
|
||||
= FlowyResult<WorkspaceSettingPB, FlowyError>;
|
||||
typedef WorkspaceLatestNotifyValue = FlowyResult<WorkspaceLatestPB, FlowyError>;
|
||||
|
||||
class FolderListener {
|
||||
FolderListener();
|
||||
|
||||
final PublishNotifier<WorkspaceSettingNotifyValue> _settingChangedNotifier =
|
||||
final PublishNotifier<WorkspaceLatestNotifyValue> _latestChangedNotifier =
|
||||
PublishNotifier();
|
||||
|
||||
FolderNotificationListener? _listener;
|
||||
|
||||
void start({
|
||||
void Function(WorkspaceSettingNotifyValue)? onSettingUpdated,
|
||||
void Function(WorkspaceLatestNotifyValue)? onLatestUpdated,
|
||||
}) {
|
||||
if (onSettingUpdated != null) {
|
||||
_settingChangedNotifier.addPublishListener(onSettingUpdated);
|
||||
if (onLatestUpdated != null) {
|
||||
_latestChangedNotifier.addPublishListener(onLatestUpdated);
|
||||
}
|
||||
|
||||
// The "current-workspace" is predefined in the backend. Do not try to
|
||||
|
@ -146,9 +145,9 @@ class FolderListener {
|
|||
switch (ty) {
|
||||
case FolderNotification.DidUpdateWorkspaceSetting:
|
||||
result.fold(
|
||||
(payload) => _settingChangedNotifier.value =
|
||||
FlowyResult.success(WorkspaceSettingPB.fromBuffer(payload)),
|
||||
(error) => _settingChangedNotifier.value = FlowyResult.failure(error),
|
||||
(payload) => _latestChangedNotifier.value =
|
||||
FlowyResult.success(WorkspaceLatestPB.fromBuffer(payload)),
|
||||
(error) => _latestChangedNotifier.value = FlowyResult.failure(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
|
@ -158,6 +157,6 @@ class FolderListener {
|
|||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_settingChangedNotifier.dispose();
|
||||
_latestChangedNotifier.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,8 +40,6 @@ class UserBackendService implements IUserBackendService {
|
|||
String? password,
|
||||
String? email,
|
||||
String? iconUrl,
|
||||
String? openAIKey,
|
||||
String? stabilityAiKey,
|
||||
}) {
|
||||
final payload = UpdateUserProfilePayloadPB.create()..id = userId;
|
||||
|
||||
|
@ -61,14 +59,6 @@ class UserBackendService implements IUserBackendService {
|
|||
payload.iconUrl = iconUrl;
|
||||
}
|
||||
|
||||
if (openAIKey != null) {
|
||||
payload.openaiKey = openAIKey;
|
||||
}
|
||||
|
||||
if (stabilityAiKey != null) {
|
||||
payload.stabilityAiKey = stabilityAiKey;
|
||||
}
|
||||
|
||||
return UserEventUpdateUserProfile(payload).send();
|
||||
}
|
||||
|
||||
|
@ -95,6 +85,17 @@ class UserBackendService implements IUserBackendService {
|
|||
return UserEventPasscodeSignIn(payload).send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> signInWithPassword(
|
||||
String email,
|
||||
String password,
|
||||
) {
|
||||
final payload = SignInPayloadPB(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
return UserEventSignInWithEmailPassword(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> signOut() {
|
||||
return UserEventSignOut().send();
|
||||
}
|
||||
|
@ -120,8 +121,13 @@ class UserBackendService implements IUserBackendService {
|
|||
});
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> openWorkspace(String workspaceId) {
|
||||
final payload = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
|
||||
Future<FlowyResult<void, FlowyError>> openWorkspace(
|
||||
String workspaceId,
|
||||
AuthTypePB authType,
|
||||
) {
|
||||
final payload = OpenUserWorkspacePB()
|
||||
..workspaceId = workspaceId
|
||||
..workspaceAuthType = authType;
|
||||
return UserEventOpenWorkspace(payload).send();
|
||||
}
|
||||
|
||||
|
@ -134,25 +140,13 @@ class UserBackendService implements IUserBackendService {
|
|||
});
|
||||
}
|
||||
|
||||
Future<FlowyResult<WorkspacePB, FlowyError>> createWorkspace(
|
||||
String name,
|
||||
String desc,
|
||||
) {
|
||||
final request = CreateWorkspacePayloadPB.create()
|
||||
..name = name
|
||||
..desc = desc;
|
||||
return FolderEventCreateFolderWorkspace(request).send().then((result) {
|
||||
return result.fold(
|
||||
(workspace) => FlowyResult.success(workspace),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<FlowyResult<UserWorkspacePB, FlowyError>> createUserWorkspace(
|
||||
String name,
|
||||
AuthTypePB authType,
|
||||
) {
|
||||
final request = CreateWorkspacePB.create()..name = name;
|
||||
final request = CreateWorkspacePB.create()
|
||||
..name = name
|
||||
..authType = authType;
|
||||
return UserEventCreateWorkspace(request).send();
|
||||
}
|
||||
|
||||
|
@ -250,13 +244,6 @@ class UserBackendService implements IUserBackendService {
|
|||
return UserEventGetWorkspaceSubscriptionInfo(params).send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
|
||||
getWorkspaceMember() async {
|
||||
final data = WorkspaceMemberIdPB.create()..uid = userId;
|
||||
|
||||
return UserEventGetMemberInfo(data).send();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
||||
String workspaceId,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:appflowy/plugins/database/application/defines.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/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
@ -22,20 +20,10 @@ class WorkspaceErrorBloc
|
|||
void _dispatch() {
|
||||
on<WorkspaceErrorEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
event.when(
|
||||
init: () {
|
||||
// _loadSnapshots();
|
||||
},
|
||||
resetWorkspace: () async {
|
||||
emit(state.copyWith(loadingState: const LoadingState.loading()));
|
||||
final payload = ResetWorkspacePB.create()
|
||||
..workspaceId = userFolder.workspaceId
|
||||
..uid = userFolder.uid;
|
||||
final result = await UserEventResetWorkspace(payload).send();
|
||||
if (!isClosed) {
|
||||
add(WorkspaceErrorEvent.didResetWorkspace(result));
|
||||
}
|
||||
},
|
||||
didResetWorkspace: (result) {
|
||||
result.fold(
|
||||
(_) {
|
||||
|
@ -68,7 +56,6 @@ class WorkspaceErrorBloc
|
|||
class WorkspaceErrorEvent with _$WorkspaceErrorEvent {
|
||||
const factory WorkspaceErrorEvent.init() = _Init;
|
||||
const factory WorkspaceErrorEvent.logout() = _DidLogout;
|
||||
const factory WorkspaceErrorEvent.resetWorkspace() = _ResetWorkspace;
|
||||
const factory WorkspaceErrorEvent.didResetWorkspace(
|
||||
FlowyResult<void, FlowyError> result,
|
||||
) = _DidResetWorkspace;
|
||||
|
|
|
@ -74,9 +74,8 @@ class AnonUserItem extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
|
||||
final isDisabled =
|
||||
isSelected || user.authenticator != AuthenticatorPB.Local;
|
||||
final desc = "${user.name}\t ${user.authenticator}\t";
|
||||
final isDisabled = isSelected || user.workspaceAuthType != AuthTypePB.Local;
|
||||
final desc = "${user.name}\t ${user.workspaceAuthType}\t";
|
||||
final child = SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import 'package:appflowy/user/presentation/helpers/helpers.dart';
|
||||
import 'package:appflowy/user/presentation/presentation.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void handleUserProfileResult(
|
||||
FlowyResult<UserProfilePB, FlowyError> userProfileResult,
|
||||
BuildContext context,
|
||||
AuthRouter authRouter,
|
||||
) {
|
||||
userProfileResult.fold(
|
||||
(userProfile) {
|
||||
if (userProfile.encryptionType == EncryptionTypePB.Symmetric) {
|
||||
authRouter.pushEncryptionScreen(context, userProfile);
|
||||
} else {
|
||||
authRouter.goHomeScreen(context, userProfile);
|
||||
}
|
||||
},
|
||||
(error) {
|
||||
handleOpenWorkspaceError(context, error);
|
||||
},
|
||||
);
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
export 'handle_open_workspace_error.dart';
|
||||
export 'handle_user_profile_result.dart';
|
||||
|
|
|
@ -21,10 +21,6 @@ class AuthRouter {
|
|||
getIt<SplashRouter>().pushWorkspaceStartScreen(context, userProfile);
|
||||
}
|
||||
|
||||
void pushSignUpScreen(BuildContext context) {
|
||||
context.push(SignUpScreen.routeName);
|
||||
}
|
||||
|
||||
/// Navigates to the home screen based on the current workspace and platform.
|
||||
///
|
||||
/// This function takes in a [BuildContext] and a [UserProfilePB] object to
|
||||
|
@ -61,20 +57,6 @@ class AuthRouter {
|
|||
);
|
||||
}
|
||||
|
||||
void pushEncryptionScreen(
|
||||
BuildContext context,
|
||||
UserProfilePB userProfile,
|
||||
) {
|
||||
// After log in,push EncryptionScreen on the top SignInScreen
|
||||
context.push(
|
||||
EncryptSecretScreen.routeName,
|
||||
extra: {
|
||||
EncryptSecretScreen.argUser: userProfile,
|
||||
EncryptSecretScreen.argKey: ValueKey(userProfile.id),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pushWorkspaceErrorScreen(
|
||||
BuildContext context,
|
||||
UserFolderPB userFolder,
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/helpers/helpers.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../application/encrypt_secret_bloc.dart';
|
||||
|
||||
class EncryptSecretScreen extends StatefulWidget {
|
||||
const EncryptSecretScreen({required this.user, super.key});
|
||||
|
||||
final UserProfilePB user;
|
||||
|
||||
static const routeName = '/EncryptSecretScreen';
|
||||
|
||||
// arguments used in GoRouter
|
||||
static const argUser = 'user';
|
||||
static const argKey = 'key';
|
||||
|
||||
@override
|
||||
State<EncryptSecretScreen> createState() => _EncryptSecretScreenState();
|
||||
}
|
||||
|
||||
class _EncryptSecretScreenState extends State<EncryptSecretScreen> {
|
||||
final TextEditingController _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: BlocProvider(
|
||||
create: (context) => EncryptSecretBloc(user: widget.user),
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<EncryptSecretBloc, EncryptSecretState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isSignOut != current.isSignOut,
|
||||
listener: (context, state) async {
|
||||
if (state.isSignOut) {
|
||||
await runAppFlowy();
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<EncryptSecretBloc, EncryptSecretState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.successOrFail != current.successOrFail,
|
||||
listener: (context, state) async {
|
||||
await state.successOrFail?.fold(
|
||||
(unit) async {
|
||||
await runAppFlowy();
|
||||
},
|
||||
(error) {
|
||||
handleOpenWorkspaceError(context, error);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<EncryptSecretBloc, EncryptSecretState>(
|
||||
builder: (context, state) {
|
||||
final indicator = state.loadingState?.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
finish: (result) => const SizedBox.shrink(),
|
||||
idle: () => const SizedBox.shrink(),
|
||||
) ??
|
||||
const SizedBox.shrink();
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
height: 160,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText.medium(
|
||||
"${LocaleKeys.settings_menu_inputEncryptPrompt.tr()} ${widget.user.email}",
|
||||
fontSize: 14,
|
||||
maxLines: 10,
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: FlowyTextField(
|
||||
controller: _textEditingController,
|
||||
hintText:
|
||||
LocaleKeys.settings_menu_inputTextFieldHint.tr(),
|
||||
onChanged: (_) {},
|
||||
),
|
||||
),
|
||||
OkCancelButton(
|
||||
alignment: MainAxisAlignment.end,
|
||||
onOkPressed: () =>
|
||||
context.read<EncryptSecretBloc>().add(
|
||||
EncryptSecretEvent.setEncryptSecret(
|
||||
_textEditingController.text,
|
||||
),
|
||||
),
|
||||
onCancelPressed: () => context
|
||||
.read<EncryptSecretBloc>()
|
||||
.add(const EncryptSecretEvent.cancelInputSecret()),
|
||||
mode: TextButtonMode.normal,
|
||||
),
|
||||
const VSpace(6),
|
||||
indicator,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
export 'sign_in_screen/sign_in_screen.dart';
|
||||
export 'skip_log_in_screen.dart';
|
||||
export 'splash_screen.dart';
|
||||
export 'sign_up_screen.dart';
|
||||
export 'encrypt_secret_screen.dart';
|
||||
export 'workspace_error_screen.dart';
|
||||
export 'workspace_start_screen/workspace_start_screen.dart';
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:appflowy/user/presentation/router.dart';
|
|||
import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
@ -34,11 +33,7 @@ class SignInScreen extends StatelessWidget {
|
|||
if (successOrFail != null) {
|
||||
successOrFail.fold(
|
||||
(userProfile) {
|
||||
if (userProfile.encryptionType == EncryptionTypePB.Symmetric) {
|
||||
getIt<AuthRouter>().pushEncryptionScreen(context, userProfile);
|
||||
} else {
|
||||
getIt<AuthRouter>().goHomeScreen(context, userProfile);
|
||||
}
|
||||
getIt<AuthRouter>().goHomeScreen(context, userProfile);
|
||||
},
|
||||
(error) {
|
||||
Log.error('Sign in error: $error');
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||
import 'package:appflowy/user/application/sign_in_bloc.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
@ -50,11 +52,6 @@ class _ContinueWithEmailAndPasswordState
|
|||
);
|
||||
} else if (successOrFail == null && !state.isSubmitting) {
|
||||
emailKey.currentState?.clearError();
|
||||
|
||||
// _pushContinueWithMagicLinkOrPasscodePage(
|
||||
// context,
|
||||
// controller.text,
|
||||
// );
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
|
@ -63,7 +60,6 @@ class _ContinueWithEmailAndPasswordState
|
|||
key: emailKey,
|
||||
controller: controller,
|
||||
hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),
|
||||
radius: 10,
|
||||
onSubmitted: (value) => _signInWithEmail(
|
||||
context,
|
||||
value,
|
||||
|
@ -76,13 +72,24 @@ class _ContinueWithEmailAndPasswordState
|
|||
controller.text,
|
||||
),
|
||||
),
|
||||
// VSpace(theme.spacing.l),
|
||||
// ContinueWithPassword(
|
||||
// onTap: () => _pushContinueWithPasswordPage(
|
||||
// context,
|
||||
// controller.text,
|
||||
// ),
|
||||
// ),
|
||||
VSpace(theme.spacing.l),
|
||||
ContinueWithPassword(
|
||||
onTap: () {
|
||||
final email = controller.text;
|
||||
|
||||
if (!isEmail(email)) {
|
||||
emailKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.signIn_invalidEmail.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_pushContinueWithPasswordPage(
|
||||
context,
|
||||
email,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -147,31 +154,34 @@ class _ContinueWithEmailAndPasswordState
|
|||
_hasPushedContinueWithMagicLinkOrPasscodePage = true;
|
||||
}
|
||||
|
||||
// void _pushContinueWithPasswordPage(
|
||||
// BuildContext context,
|
||||
// String email,
|
||||
// ) {
|
||||
// final signInBloc = context.read<SignInBloc>();
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => BlocProvider.value(
|
||||
// value: signInBloc,
|
||||
// child: ContinueWithPasswordPage(
|
||||
// email: email,
|
||||
// backToLogin: () => Navigator.pop(context),
|
||||
// onEnterPassword: (password) => signInBloc.add(
|
||||
// SignInEvent.signInWithEmailAndPassword(
|
||||
// email: email,
|
||||
// password: password,
|
||||
// ),
|
||||
// ),
|
||||
// onForgotPassword: () {
|
||||
// // todo: implement forgot password
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
void _pushContinueWithPasswordPage(
|
||||
BuildContext context,
|
||||
String email,
|
||||
) {
|
||||
final signInBloc = context.read<SignInBloc>();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: signInBloc,
|
||||
child: ContinueWithPasswordPage(
|
||||
email: email,
|
||||
backToLogin: () {
|
||||
emailKey.currentState?.clearError();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onEnterPassword: (password) => signInBloc.add(
|
||||
SignInEvent.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
onForgotPassword: () {
|
||||
// todo: implement forgot password
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
|
||||
final inputPasscodeKey = GlobalKey<AFTextFieldState>();
|
||||
|
||||
bool isSubmitting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
passcodeController.dispose();
|
||||
|
@ -54,6 +56,10 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (state.isSubmitting != isSubmitting) {
|
||||
setState(() => isSubmitting = state.isSubmitting);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
|
@ -81,6 +87,15 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
List<Widget> _buildEnterCodeManually() {
|
||||
// todo: ask designer to provide the spacing
|
||||
final spacing = VSpace(20);
|
||||
final textStyle = AFButtonSize.l.buildTextStyle(context);
|
||||
final textHeight = textStyle.height;
|
||||
final textFontSize = textStyle.fontSize;
|
||||
|
||||
// the indicator height is the height of the text style.
|
||||
double indicatorHeight = 20;
|
||||
if (textHeight != null && textFontSize != null) {
|
||||
indicatorHeight = textHeight * textFontSize;
|
||||
}
|
||||
|
||||
if (!isEnteringPasscode) {
|
||||
return [
|
||||
|
@ -101,7 +116,6 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
controller: passcodeController,
|
||||
hintText: LocaleKeys.signIn_enterCode.tr(),
|
||||
keyboardType: TextInputType.number,
|
||||
radius: 10,
|
||||
autoFocus: true,
|
||||
onSubmitted: (passcode) {
|
||||
if (passcode.isEmpty) {
|
||||
|
@ -117,26 +131,55 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
VSpace(12),
|
||||
|
||||
// continue to login
|
||||
AFFilledTextButton.primary(
|
||||
text: LocaleKeys.signIn_continueToSignIn.tr(),
|
||||
onTap: () {
|
||||
final passcode = passcodeController.text;
|
||||
if (passcode.isEmpty) {
|
||||
inputPasscodeKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),
|
||||
);
|
||||
} else {
|
||||
widget.onEnterPasscode(passcode);
|
||||
}
|
||||
},
|
||||
size: AFButtonSize.l,
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
!isSubmitting
|
||||
? _buildContinueButton(textStyle: textStyle)
|
||||
: _buildIndicator(indicatorHeight: indicatorHeight),
|
||||
|
||||
spacing,
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildContinueButton({
|
||||
required TextStyle textStyle,
|
||||
}) {
|
||||
return AFFilledTextButton.primary(
|
||||
text: LocaleKeys.signIn_continueToSignIn.tr(),
|
||||
onTap: () {
|
||||
final passcode = passcodeController.text;
|
||||
if (passcode.isEmpty) {
|
||||
inputPasscodeKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),
|
||||
);
|
||||
} else {
|
||||
widget.onEnterPasscode(passcode);
|
||||
}
|
||||
},
|
||||
textStyle: textStyle.copyWith(
|
||||
color: AppFlowyTheme.of(context).textColorScheme.onFill,
|
||||
),
|
||||
size: AFButtonSize.l,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIndicator({
|
||||
required double indicatorHeight,
|
||||
}) {
|
||||
return AFFilledButton.disabled(
|
||||
size: AFButtonSize.l,
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Align(
|
||||
child: SizedBox.square(
|
||||
dimension: indicatorHeight,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildBackToLogin() {
|
||||
return [
|
||||
AFGhostTextButton(
|
||||
|
@ -167,7 +210,7 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
// title
|
||||
Text(
|
||||
LocaleKeys.signIn_checkYourEmail.tr(),
|
||||
style: theme.textStyle.heading.h3(
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
@ -199,7 +242,7 @@ class _ContinueWithMagicLinkOrPasscodePageState
|
|||
// title
|
||||
Text(
|
||||
LocaleKeys.signIn_enterCode.tr(),
|
||||
style: theme.textStyle.heading.h3(
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/application/sign_in_bloc.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -43,9 +46,16 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
width: 320,
|
||||
child: BlocListener<SignInBloc, SignInState>(
|
||||
listener: (context, state) {
|
||||
if (state.passwordError != null) {
|
||||
final successOrFail = state.successOrFail;
|
||||
if (successOrFail != null && successOrFail.isFailure) {
|
||||
successOrFail.onFailure((error) {
|
||||
inputPasswordKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(),
|
||||
);
|
||||
});
|
||||
} else if (state.passwordError != null) {
|
||||
inputPasswordKey.currentState?.syncError(
|
||||
errorText: 'Incorrect password. Please try again.',
|
||||
errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(),
|
||||
);
|
||||
} else {
|
||||
inputPasswordKey.currentState?.clearError();
|
||||
|
@ -80,8 +90,8 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
|
||||
// title
|
||||
Text(
|
||||
'Enter password',
|
||||
style: theme.textStyle.heading.h3(
|
||||
LocaleKeys.signIn_enterPassword.tr(),
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
@ -92,13 +102,13 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Login as ',
|
||||
text: LocaleKeys.signIn_loginAs.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: widget.email,
|
||||
text: ' ${widget.email}',
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
|
@ -111,13 +121,26 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
}
|
||||
|
||||
List<Widget> _buildPasswordSection() {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final iconSize = 20.0;
|
||||
return [
|
||||
// Password input
|
||||
AFTextField(
|
||||
key: inputPasswordKey,
|
||||
controller: passwordController,
|
||||
hintText: 'Enter password',
|
||||
hintText: LocaleKeys.signIn_enterPassword.tr(),
|
||||
autoFocus: true,
|
||||
obscureText: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(
|
||||
isObscured: isObscured,
|
||||
onTap: () {
|
||||
inputPasswordKey.currentState?.syncObscured(!isObscured);
|
||||
},
|
||||
),
|
||||
onSubmitted: widget.onEnterPassword,
|
||||
),
|
||||
// todo: ask designer to provide the spacing
|
||||
|
@ -127,7 +150,7 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AFGhostTextButton(
|
||||
text: 'Forget password?',
|
||||
text: LocaleKeys.signIn_forgotPassword.tr(),
|
||||
size: AFButtonSize.s,
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: widget.onForgotPassword,
|
||||
|
@ -144,7 +167,7 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
|
||||
// Continue button
|
||||
AFFilledTextButton.primary(
|
||||
text: 'Continue',
|
||||
text: LocaleKeys.web_continue.tr(),
|
||||
onTap: () => widget.onEnterPassword(passwordController.text),
|
||||
size: AFButtonSize.l,
|
||||
alignment: Alignment.center,
|
||||
|
@ -156,7 +179,7 @@ class _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {
|
|||
List<Widget> _buildBackToLogin() {
|
||||
return [
|
||||
AFGhostTextButton(
|
||||
text: 'Back to Login',
|
||||
text: LocaleKeys.signIn_backToLogin.tr(),
|
||||
size: AFButtonSize.s,
|
||||
onTap: widget.backToLogin,
|
||||
padding: EdgeInsets.zero,
|
||||
|
|
|
@ -1,220 +0,0 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/sign_up_bloc.dart';
|
||||
import 'package:appflowy/user/presentation/router.dart';
|
||||
import 'package:appflowy/user/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||
show UserProfilePB;
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SignUpScreen extends StatelessWidget {
|
||||
const SignUpScreen({
|
||||
super.key,
|
||||
required this.router,
|
||||
});
|
||||
|
||||
static const routeName = '/SignUpScreen';
|
||||
final AuthRouter router;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<SignUpBloc>(),
|
||||
child: BlocListener<SignUpBloc, SignUpState>(
|
||||
listener: (context, state) {
|
||||
final successOrFail = state.successOrFail;
|
||||
if (successOrFail != null) {
|
||||
_handleSuccessOrFail(context, successOrFail);
|
||||
}
|
||||
},
|
||||
child: const Scaffold(body: SignUpForm()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSuccessOrFail(
|
||||
BuildContext context,
|
||||
FlowyResult<UserProfilePB, FlowyError> result,
|
||||
) {
|
||||
result.fold(
|
||||
(user) => router.pushWorkspaceStartScreen(context, user),
|
||||
(error) => showSnapBar(context, error.msg),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SignUpForm extends StatelessWidget {
|
||||
const SignUpForm({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
child: AuthFormContainer(
|
||||
children: [
|
||||
FlowyLogoTitle(
|
||||
title: LocaleKeys.signUp_title.tr(),
|
||||
logoSize: const Size(60, 60),
|
||||
),
|
||||
const VSpace(30),
|
||||
const EmailTextField(),
|
||||
const VSpace(5),
|
||||
const PasswordTextField(),
|
||||
const VSpace(5),
|
||||
const RepeatPasswordTextField(),
|
||||
const VSpace(30),
|
||||
const SignUpButton(),
|
||||
const VSpace(10),
|
||||
const SignUpPrompt(),
|
||||
if (context.read<SignUpBloc>().state.isSubmitting) ...[
|
||||
const SizedBox(height: 8),
|
||||
const LinearProgressIndicator(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SignUpPrompt extends StatelessWidget {
|
||||
const SignUpPrompt({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.signUp_alreadyHaveAnAccount.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.signIn_buttonText.tr(),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SignUpButton extends StatelessWidget {
|
||||
const SignUpButton({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.signUp_getStartedText.tr(),
|
||||
height: 48,
|
||||
onPressed: () {
|
||||
context
|
||||
.read<SignUpBloc>()
|
||||
.add(const SignUpEvent.signUpWithUserEmailAndPassword());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordTextField extends StatelessWidget {
|
||||
const PasswordTextField({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SignUpBloc, SignUpState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.passwordError != current.passwordError,
|
||||
builder: (context, state) {
|
||||
return RoundedInputField(
|
||||
obscureText: true,
|
||||
obscureIcon: const FlowySvg(FlowySvgs.hide_m),
|
||||
obscureHideIcon: const FlowySvg(FlowySvgs.show_m),
|
||||
hintText: LocaleKeys.signUp_passwordHint.tr(),
|
||||
normalBorderColor: Theme.of(context).colorScheme.outline,
|
||||
errorBorderColor: Theme.of(context).colorScheme.error,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
errorText: context.read<SignUpBloc>().state.passwordError ?? '',
|
||||
onChanged: (value) => context
|
||||
.read<SignUpBloc>()
|
||||
.add(SignUpEvent.passwordChanged(value)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RepeatPasswordTextField extends StatelessWidget {
|
||||
const RepeatPasswordTextField({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SignUpBloc, SignUpState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.repeatPasswordError != current.repeatPasswordError,
|
||||
builder: (context, state) {
|
||||
return RoundedInputField(
|
||||
obscureText: true,
|
||||
obscureIcon: const FlowySvg(FlowySvgs.hide_m),
|
||||
obscureHideIcon: const FlowySvg(FlowySvgs.show_m),
|
||||
hintText: LocaleKeys.signUp_repeatPasswordHint.tr(),
|
||||
normalBorderColor: Theme.of(context).colorScheme.outline,
|
||||
errorBorderColor: Theme.of(context).colorScheme.error,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
errorText: context.read<SignUpBloc>().state.repeatPasswordError ?? '',
|
||||
onChanged: (value) => context
|
||||
.read<SignUpBloc>()
|
||||
.add(SignUpEvent.repeatPasswordChanged(value)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmailTextField extends StatelessWidget {
|
||||
const EmailTextField({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SignUpBloc, SignUpState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.emailError != current.emailError,
|
||||
builder: (context, state) {
|
||||
return RoundedInputField(
|
||||
hintText: LocaleKeys.signUp_emailHint.tr(),
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
normalBorderColor: Theme.of(context).colorScheme.outline,
|
||||
errorBorderColor: Theme.of(context).colorScheme.error,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
errorText: context.read<SignUpBloc>().state.emailError ?? '',
|
||||
onChanged: (value) =>
|
||||
context.read<SignUpBloc>().add(SignUpEvent.emailChanged(value)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import 'package:appflowy/user/presentation/helpers/helpers.dart';
|
|||
import 'package:appflowy/user/presentation/router.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
@ -61,32 +60,15 @@ class SplashScreen extends StatelessWidget {
|
|||
BuildContext context,
|
||||
Authenticated authenticated,
|
||||
) async {
|
||||
final userProfile = authenticated.userProfile;
|
||||
|
||||
/// After a user is authenticated, this function checks if encryption is required.
|
||||
final result = await UserEventCheckEncryptionSign().send();
|
||||
await result.fold(
|
||||
(check) async {
|
||||
/// If encryption is needed, the user is navigated to the encryption screen.
|
||||
/// Otherwise, it fetches the current workspace for the user and navigates them
|
||||
if (check.requireSecret) {
|
||||
getIt<AuthRouter>().pushEncryptionScreen(context, userProfile);
|
||||
} else {
|
||||
final result = await FolderEventGetCurrentWorkspaceSetting().send();
|
||||
result.fold(
|
||||
(workspaceSetting) {
|
||||
// After login, replace Splash screen by corresponding home screen
|
||||
getIt<SplashRouter>().goHomeScreen(
|
||||
context,
|
||||
);
|
||||
},
|
||||
(error) => handleOpenWorkspaceError(context, error),
|
||||
);
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
final result = await FolderEventGetCurrentWorkspaceSetting().send();
|
||||
result.fold(
|
||||
(workspaceSetting) {
|
||||
// After login, replace Splash screen by corresponding home screen
|
||||
getIt<SplashRouter>().goHomeScreen(
|
||||
context,
|
||||
);
|
||||
},
|
||||
(error) => handleOpenWorkspaceError(context, error),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
@ -86,7 +85,6 @@ class WorkspaceErrorScreen extends StatelessWidget {
|
|||
const VSpace(50),
|
||||
const LogoutButton(),
|
||||
const VSpace(20),
|
||||
const ResetWorkspaceButton(),
|
||||
]);
|
||||
|
||||
return Center(
|
||||
|
@ -157,43 +155,3 @@ class LogoutButton extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ResetWorkspaceButton extends StatelessWidget {
|
||||
const ResetWorkspaceButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
height: 40,
|
||||
child: BlocBuilder<WorkspaceErrorBloc, WorkspaceErrorState>(
|
||||
builder: (context, state) {
|
||||
final isLoading = state.loadingState?.isLoading() ?? false;
|
||||
final icon = isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
: null;
|
||||
|
||||
return FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.workspace_reset.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
onTap: () {
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.workspace_resetWorkspacePrompt.tr(),
|
||||
confirm: () {
|
||||
context.read<WorkspaceErrorBloc>().add(
|
||||
const WorkspaceErrorEvent.resetWorkspace(),
|
||||
);
|
||||
},
|
||||
).show(context);
|
||||
},
|
||||
rightIcon: icon,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class FlowyLogoTitle extends StatelessWidget {
|
|||
const VSpace(20),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textStyle.heading.h3(
|
||||
style: theme.textStyle.heading3.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -3,14 +3,14 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
|||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
|
||||
show WorkspaceSettingPB;
|
||||
show WorkspaceLatestPB;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'home_bloc.freezed.dart';
|
||||
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
HomeBloc(WorkspaceSettingPB workspaceSetting)
|
||||
HomeBloc(WorkspaceLatestPB workspaceSetting)
|
||||
: _workspaceListener = FolderListener(),
|
||||
super(HomeState.initial(workspaceSetting)) {
|
||||
_dispatch(workspaceSetting);
|
||||
|
@ -24,7 +24,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
|||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch(WorkspaceSettingPB workspaceSetting) {
|
||||
void _dispatch(WorkspaceLatestPB workspaceSetting) {
|
||||
on<HomeEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
|
@ -36,10 +36,9 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
|||
});
|
||||
|
||||
_workspaceListener.start(
|
||||
onSettingUpdated: (result) {
|
||||
onLatestUpdated: (result) {
|
||||
result.fold(
|
||||
(setting) =>
|
||||
add(HomeEvent.didReceiveWorkspaceSetting(setting)),
|
||||
(latest) => add(HomeEvent.didReceiveWorkspaceSetting(latest)),
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
},
|
||||
|
@ -78,7 +77,7 @@ class HomeEvent with _$HomeEvent {
|
|||
const factory HomeEvent.initial() = _Initial;
|
||||
const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading;
|
||||
const factory HomeEvent.didReceiveWorkspaceSetting(
|
||||
WorkspaceSettingPB setting,
|
||||
WorkspaceLatestPB setting,
|
||||
) = _DidReceiveWorkspaceSetting;
|
||||
}
|
||||
|
||||
|
@ -86,11 +85,11 @@ class HomeEvent with _$HomeEvent {
|
|||
class HomeState with _$HomeState {
|
||||
const factory HomeState({
|
||||
required bool isLoading,
|
||||
required WorkspaceSettingPB workspaceSetting,
|
||||
required WorkspaceLatestPB workspaceSetting,
|
||||
ViewPB? latestView,
|
||||
}) = _HomeState;
|
||||
|
||||
factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState(
|
||||
factory HomeState.initial(WorkspaceLatestPB workspaceSetting) => HomeState(
|
||||
isLoading: false,
|
||||
workspaceSetting: workspaceSetting,
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:appflowy/user/application/user_listener.dart';
|
|||
import 'package:appflowy/workspace/application/edit_panel/edit_context.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
|
||||
show WorkspaceSettingPB;
|
||||
show WorkspaceLatestPB;
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
@ -12,7 +12,7 @@ part 'home_setting_bloc.freezed.dart';
|
|||
|
||||
class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
|
||||
HomeSettingBloc(
|
||||
WorkspaceSettingPB workspaceSetting,
|
||||
WorkspaceLatestPB workspaceSetting,
|
||||
AppearanceSettingsCubit appearanceSettingsCubit,
|
||||
double screenWidthPx,
|
||||
) : _listener = FolderListener(),
|
||||
|
@ -124,7 +124,7 @@ class HomeSettingEvent with _$HomeSettingEvent {
|
|||
_ShowEditPanel;
|
||||
const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel;
|
||||
const factory HomeSettingEvent.didReceiveWorkspaceSetting(
|
||||
WorkspaceSettingPB setting,
|
||||
WorkspaceLatestPB setting,
|
||||
) = _DidReceiveWorkspaceSetting;
|
||||
const factory HomeSettingEvent.collapseMenu() = _CollapseMenu;
|
||||
const factory HomeSettingEvent.checkScreenSize(double screenWidthPx) =
|
||||
|
@ -139,7 +139,7 @@ class HomeSettingEvent with _$HomeSettingEvent {
|
|||
class HomeSettingState with _$HomeSettingState {
|
||||
const factory HomeSettingState({
|
||||
required EditPanelContext? panelContext,
|
||||
required WorkspaceSettingPB workspaceSetting,
|
||||
required WorkspaceLatestPB workspaceSetting,
|
||||
required bool unauthorized,
|
||||
required bool isMenuCollapsed,
|
||||
required bool keepMenuCollapsed,
|
||||
|
@ -150,7 +150,7 @@ class HomeSettingState with _$HomeSettingState {
|
|||
}) = _HomeSettingState;
|
||||
|
||||
factory HomeSettingState.initial(
|
||||
WorkspaceSettingPB workspaceSetting,
|
||||
WorkspaceLatestPB workspaceSetting,
|
||||
AppearanceSettingsState appearanceSettingsState,
|
||||
double screenWidthPx,
|
||||
) {
|
||||
|
|
|
@ -30,6 +30,10 @@ class LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {
|
|||
LocalAiPluginEvent event,
|
||||
Emitter<LocalAiPluginState> emit,
|
||||
) async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await event.when(
|
||||
didReceiveAiState: (aiState) {
|
||||
emit(
|
||||
|
@ -54,7 +58,9 @@ class LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {
|
|||
emit(LocalAiPluginState.loading());
|
||||
await AIEventToggleLocalAI().send().fold(
|
||||
(aiState) {
|
||||
add(LocalAiPluginEvent.didReceiveAiState(aiState));
|
||||
if (!isClosed) {
|
||||
add(LocalAiPluginEvent.didReceiveAiState(aiState));
|
||||
}
|
||||
},
|
||||
Log.error,
|
||||
);
|
||||
|
@ -69,10 +75,14 @@ class LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {
|
|||
void _startListening() {
|
||||
listener.start(
|
||||
stateCallback: (pluginState) {
|
||||
add(LocalAiPluginEvent.didReceiveAiState(pluginState));
|
||||
if (!isClosed) {
|
||||
add(LocalAiPluginEvent.didReceiveAiState(pluginState));
|
||||
}
|
||||
},
|
||||
resourceCallback: (data) {
|
||||
add(LocalAiPluginEvent.didReceiveLackOfResources(data));
|
||||
if (!isClosed) {
|
||||
add(LocalAiPluginEvent.didReceiveLackOfResources(data));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -80,7 +90,9 @@ class LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {
|
|||
void _getLocalAiState() {
|
||||
AIEventGetLocalAIState().send().fold(
|
||||
(aiState) {
|
||||
add(LocalAiPluginEvent.didReceiveAiState(aiState));
|
||||
if (!isClosed) {
|
||||
add(LocalAiPluginEvent.didReceiveAiState(aiState));
|
||||
}
|
||||
},
|
||||
Log.error,
|
||||
);
|
||||
|
|
|
@ -55,7 +55,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
|
|||
onProfileUpdated: _onProfileUpdated,
|
||||
onUserWorkspaceSettingUpdated: (settings) {
|
||||
if (!isClosed) {
|
||||
add(SettingsAIEvent.didLoadAISetting(settings));
|
||||
add(SettingsAIEvent.didLoadWorkspaceSetting(settings));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -85,7 +85,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
|
|||
),
|
||||
).send();
|
||||
},
|
||||
didLoadAISetting: (UseAISettingPB settings) {
|
||||
didLoadWorkspaceSetting: (WorkspaceSettingsPB settings) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
aiSettings: settings,
|
||||
|
@ -150,7 +150,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
|
|||
UserEventGetWorkspaceSetting(payload).send().then((result) {
|
||||
result.fold((settings) {
|
||||
if (!isClosed) {
|
||||
add(SettingsAIEvent.didLoadAISetting(settings));
|
||||
add(SettingsAIEvent.didLoadWorkspaceSetting(settings));
|
||||
}
|
||||
}, (err) {
|
||||
Log.error(err);
|
||||
|
@ -162,8 +162,8 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
|
|||
@freezed
|
||||
class SettingsAIEvent with _$SettingsAIEvent {
|
||||
const factory SettingsAIEvent.started() = _Started;
|
||||
const factory SettingsAIEvent.didLoadAISetting(
|
||||
UseAISettingPB settings,
|
||||
const factory SettingsAIEvent.didLoadWorkspaceSetting(
|
||||
WorkspaceSettingsPB settings,
|
||||
) = _DidLoadWorkspaceSetting;
|
||||
|
||||
const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch;
|
||||
|
@ -183,7 +183,7 @@ class SettingsAIEvent with _$SettingsAIEvent {
|
|||
class SettingsAIState with _$SettingsAIState {
|
||||
const factory SettingsAIState({
|
||||
required UserProfilePB userProfile,
|
||||
UseAISettingPB? aiSettings,
|
||||
WorkspaceSettingsPB? aiSettings,
|
||||
AvailableModelsPB? availableModels,
|
||||
@Default(true) bool enableSearchIndexing,
|
||||
}) = _SettingsAIState;
|
||||
|
|
|
@ -28,13 +28,12 @@ class MobileAppearance extends BaseAppearance {
|
|||
fontWeight: FontWeight.w400,
|
||||
);
|
||||
|
||||
final isLight = brightness == Brightness.light;
|
||||
final codeFontStyle = getFontStyle(fontFamily: codeFontFamily);
|
||||
|
||||
final theme = brightness == Brightness.light
|
||||
? appTheme.lightTheme
|
||||
: appTheme.darkTheme;
|
||||
final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme;
|
||||
|
||||
final colorTheme = brightness == Brightness.light
|
||||
final colorTheme = isLight
|
||||
? ColorScheme(
|
||||
brightness: brightness,
|
||||
primary: _primaryColor,
|
||||
|
@ -71,13 +70,9 @@ class MobileAppearance extends BaseAppearance {
|
|||
onSurface: const Color(0xffC5C6C7), // text/body color
|
||||
surfaceContainerHighest: theme.sidebarBg,
|
||||
);
|
||||
final hintColor = brightness == Brightness.light
|
||||
? const Color(0x991F2329)
|
||||
: _hintColorInDarkMode;
|
||||
final onBackground =
|
||||
brightness == Brightness.light ? _onBackgroundColor : Colors.white;
|
||||
final background =
|
||||
brightness == Brightness.light ? Colors.white : const Color(0xff121212);
|
||||
final hintColor = isLight ? const Color(0x991F2329) : _hintColorInDarkMode;
|
||||
final onBackground = isLight ? _onBackgroundColor : Colors.white;
|
||||
final background = isLight ? Colors.white : const Color(0xff121212);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: false,
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'package:appflowy/user/application/user_listener.dart';
|
|||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
@ -91,8 +90,8 @@ class SettingsDialogBloc
|
|||
AFRolePB? currentWorkspaceMemberRole,
|
||||
]) async {
|
||||
if ([
|
||||
AuthenticatorPB.Local,
|
||||
].contains(userProfile.authenticator)) {
|
||||
AuthTypePB.Local,
|
||||
].contains(userProfile.workspaceAuthType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -76,12 +76,6 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
|||
|
||||
final (spaces, publicViews, privateViews) = await _getSpaces();
|
||||
|
||||
final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog(
|
||||
spaces: spaces,
|
||||
publicViews: publicViews,
|
||||
privateViews: privateViews,
|
||||
);
|
||||
|
||||
final currentSpace = await _getLastOpenedSpace(spaces);
|
||||
final isExpanded = await _getSpaceExpandStatus(currentSpace);
|
||||
emit(
|
||||
|
@ -89,17 +83,11 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
|||
spaces: spaces,
|
||||
currentSpace: currentSpace,
|
||||
isExpanded: isExpanded,
|
||||
shouldShowUpgradeDialog: shouldShowUpgradeDialog,
|
||||
shouldShowUpgradeDialog: false,
|
||||
isInitialized: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldShowUpgradeDialog && !integrationMode().isTest) {
|
||||
if (!isClosed) {
|
||||
add(const SpaceEvent.migrate());
|
||||
}
|
||||
}
|
||||
|
||||
if (openFirstPage) {
|
||||
if (currentSpace != null) {
|
||||
if (!isClosed) {
|
||||
|
|
|
@ -54,17 +54,27 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
|||
);
|
||||
});
|
||||
},
|
||||
removeUserIcon: () {
|
||||
// Empty Icon URL = No icon
|
||||
_userService.updateUserProfile(iconUrl: "").then((result) {
|
||||
updateUserEmail: (String email) {
|
||||
_userService.updateUserProfile(email: email).then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
},
|
||||
updateUserEmail: (String email) {
|
||||
_userService.updateUserProfile(email: email).then((result) {
|
||||
updateUserPassword: (String oldPassword, String newPassword) {
|
||||
_userService
|
||||
.updateUserProfile(password: newPassword)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
},
|
||||
removeUserIcon: () {
|
||||
// Empty Icon URL = No icon
|
||||
_userService.updateUserProfile(iconUrl: "").then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
|
@ -104,10 +114,19 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
|||
@freezed
|
||||
class SettingsUserEvent with _$SettingsUserEvent {
|
||||
const factory SettingsUserEvent.initial() = _Initial;
|
||||
const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName;
|
||||
const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail;
|
||||
const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) =
|
||||
_UpdateUserIcon;
|
||||
const factory SettingsUserEvent.updateUserName({
|
||||
required String name,
|
||||
}) = _UpdateUserName;
|
||||
const factory SettingsUserEvent.updateUserEmail({
|
||||
required String email,
|
||||
}) = _UpdateEmail;
|
||||
const factory SettingsUserEvent.updateUserIcon({
|
||||
required String iconUrl,
|
||||
}) = _UpdateUserIcon;
|
||||
const factory SettingsUserEvent.updateUserPassword({
|
||||
required String oldPassword,
|
||||
required String newPassword,
|
||||
}) = _UpdateUserPassword;
|
||||
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
|
||||
const factory SettingsUserEvent.didReceiveUserProfile(
|
||||
UserProfilePB newUserProfile,
|
||||
|
|
|
@ -44,7 +44,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
final currentWorkspace = result.$1;
|
||||
final workspaces = result.$2;
|
||||
final isCollabWorkspaceOn =
|
||||
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud &&
|
||||
userProfile.userAuthType == AuthTypePB.Server &&
|
||||
FeatureFlag.collaborativeWorkspace.isOn;
|
||||
Log.info(
|
||||
'init workspace, current workspace: ${currentWorkspace?.workspaceId}, '
|
||||
|
@ -52,7 +52,10 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
);
|
||||
if (currentWorkspace != null && result.$3 == true) {
|
||||
Log.info('init open workspace: ${currentWorkspace.workspaceId}');
|
||||
await _userService.openWorkspace(currentWorkspace.workspaceId);
|
||||
await _userService.openWorkspace(
|
||||
currentWorkspace.workspaceId,
|
||||
currentWorkspace.workspaceAuthType,
|
||||
);
|
||||
}
|
||||
|
||||
emit(
|
||||
|
@ -86,10 +89,15 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
Log.info(
|
||||
'fetch workspaces: try to open workspace: ${currentWorkspace.workspaceId}',
|
||||
);
|
||||
add(OpenWorkspace(currentWorkspace.workspaceId));
|
||||
add(
|
||||
OpenWorkspace(
|
||||
currentWorkspace.workspaceId,
|
||||
currentWorkspace.workspaceAuthType,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
createWorkspace: (name) async {
|
||||
createWorkspace: (name, authType) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
actionResult: const UserWorkspaceActionResult(
|
||||
|
@ -99,7 +107,10 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
),
|
||||
),
|
||||
);
|
||||
final result = await _userService.createUserWorkspace(name);
|
||||
final result = await _userService.createUserWorkspace(
|
||||
name,
|
||||
authType,
|
||||
);
|
||||
final workspaces = result.fold(
|
||||
(s) => [...state.workspaces, s],
|
||||
(e) => state.workspaces,
|
||||
|
@ -118,7 +129,12 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
result
|
||||
..onSuccess((s) {
|
||||
Log.info('create workspace success: $s');
|
||||
add(OpenWorkspace(s.workspaceId));
|
||||
add(
|
||||
OpenWorkspace(
|
||||
s.workspaceId,
|
||||
s.workspaceAuthType,
|
||||
),
|
||||
);
|
||||
})
|
||||
..onFailure((f) {
|
||||
Log.error('create workspace error: $f');
|
||||
|
@ -171,7 +187,12 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
Log.info('delete workspace success: $workspaceId');
|
||||
// if the current workspace is deleted, open the first workspace
|
||||
if (state.currentWorkspace?.workspaceId == workspaceId) {
|
||||
add(OpenWorkspace(workspaces.first.workspaceId));
|
||||
add(
|
||||
OpenWorkspace(
|
||||
workspaces.first.workspaceId,
|
||||
workspaces.first.workspaceAuthType,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
..onFailure((f) {
|
||||
|
@ -179,7 +200,12 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
// if the workspace is deleted but return an error, we need to
|
||||
// open the first workspace
|
||||
if (!containsDeletedWorkspace) {
|
||||
add(OpenWorkspace(workspaces.first.workspaceId));
|
||||
add(
|
||||
OpenWorkspace(
|
||||
workspaces.first.workspaceId,
|
||||
workspaces.first.workspaceAuthType,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
emit(
|
||||
|
@ -193,7 +219,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
),
|
||||
);
|
||||
},
|
||||
openWorkspace: (workspaceId) async {
|
||||
openWorkspace: (workspaceId, authType) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
actionResult: const UserWorkspaceActionResult(
|
||||
|
@ -203,7 +229,10 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
),
|
||||
),
|
||||
);
|
||||
final result = await _userService.openWorkspace(workspaceId);
|
||||
final result = await _userService.openWorkspace(
|
||||
workspaceId,
|
||||
authType,
|
||||
);
|
||||
final currentWorkspace = result.fold(
|
||||
(s) => state.workspaces.firstWhereOrNull(
|
||||
(e) => e.workspaceId == workspaceId,
|
||||
|
@ -337,7 +366,12 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
Log.info('leave workspace success: $workspaceId');
|
||||
// if leaving the current workspace, open the first workspace
|
||||
if (state.currentWorkspace?.workspaceId == workspaceId) {
|
||||
add(OpenWorkspace(workspaces.first.workspaceId));
|
||||
add(
|
||||
OpenWorkspace(
|
||||
workspaces.first.workspaceId,
|
||||
workspaces.first.workspaceAuthType,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
..onFailure((f) {
|
||||
|
@ -441,12 +475,16 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||
class UserWorkspaceEvent with _$UserWorkspaceEvent {
|
||||
const factory UserWorkspaceEvent.initial() = Initial;
|
||||
const factory UserWorkspaceEvent.fetchWorkspaces() = FetchWorkspaces;
|
||||
const factory UserWorkspaceEvent.createWorkspace(String name) =
|
||||
CreateWorkspace;
|
||||
const factory UserWorkspaceEvent.createWorkspace(
|
||||
String name,
|
||||
AuthTypePB authType,
|
||||
) = CreateWorkspace;
|
||||
const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) =
|
||||
DeleteWorkspace;
|
||||
const factory UserWorkspaceEvent.openWorkspace(String workspaceId) =
|
||||
OpenWorkspace;
|
||||
const factory UserWorkspaceEvent.openWorkspace(
|
||||
String workspaceId,
|
||||
AuthTypePB authType,
|
||||
) = OpenWorkspace;
|
||||
const factory UserWorkspaceEvent.renameWorkspace(
|
||||
String workspaceId,
|
||||
String name,
|
||||
|
|
|
@ -404,7 +404,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
|||
});
|
||||
}
|
||||
|
||||
if (update.updateChildViews.isNotEmpty) {
|
||||
if (update.updateChildViews.isNotEmpty && update.parentViewId.isNotEmpty) {
|
||||
final view = await ViewBackendService.getView(update.parentViewId);
|
||||
final childViews = view.fold((l) => l.childViews, (r) => []);
|
||||
bool isSameOrder = true;
|
||||
|
|
|
@ -111,6 +111,12 @@ class ViewBackendService {
|
|||
static Future<FlowyResult<List<ViewPB>, FlowyError>> getChildViews({
|
||||
required String viewId,
|
||||
}) {
|
||||
if (viewId.isEmpty) {
|
||||
return Future.value(
|
||||
FlowyResult<List<ViewPB>, FlowyError>.success(<ViewPB>[]),
|
||||
);
|
||||
}
|
||||
|
||||
final payload = ViewIdPB.create()..value = viewId;
|
||||
|
||||
return FolderEventGetView(payload).send().then((result) {
|
||||
|
@ -262,6 +268,9 @@ class ViewBackendService {
|
|||
static Future<FlowyResult<ViewPB, FlowyError>> getView(
|
||||
String viewId,
|
||||
) async {
|
||||
if (viewId.isEmpty) {
|
||||
Log.error('ViewId is empty');
|
||||
}
|
||||
final payload = ViewIdPB.create()..value = viewId;
|
||||
return FolderEventGetView(payload).send();
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:appflowy/user/application/user_service.dart';
|
|||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
@ -64,7 +65,8 @@ class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState> {
|
|||
String desc,
|
||||
Emitter<WorkspaceState> emit,
|
||||
) async {
|
||||
final result = await userService.createWorkspace(name, desc);
|
||||
final result =
|
||||
await userService.createUserWorkspace(name, AuthTypePB.Server);
|
||||
emit(
|
||||
result.fold(
|
||||
(workspace) {
|
||||
|
|
|
@ -52,8 +52,8 @@ class DesktopHomeScreen extends StatelessWidget {
|
|||
return _buildLoading();
|
||||
}
|
||||
|
||||
final workspaceSetting = snapshots.data?[0].fold(
|
||||
(workspaceSettingPB) => workspaceSettingPB as WorkspaceSettingPB,
|
||||
final workspaceLatest = snapshots.data?[0].fold(
|
||||
(workspaceLatestPB) => workspaceLatestPB as WorkspaceLatestPB,
|
||||
(error) => null,
|
||||
);
|
||||
|
||||
|
@ -64,7 +64,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
|||
|
||||
// In the unlikely case either of the above is null, eg.
|
||||
// when a workspace is already open this can happen.
|
||||
if (workspaceSetting == null || userProfile == null) {
|
||||
if (workspaceLatest == null || userProfile == null) {
|
||||
return const WorkspaceFailedScreen();
|
||||
}
|
||||
|
||||
|
@ -86,11 +86,11 @@ class DesktopHomeScreen extends StatelessWidget {
|
|||
BlocProvider<TabsBloc>.value(value: getIt<TabsBloc>()),
|
||||
BlocProvider<HomeBloc>(
|
||||
create: (_) =>
|
||||
HomeBloc(workspaceSetting)..add(const HomeEvent.initial()),
|
||||
HomeBloc(workspaceLatest)..add(const HomeEvent.initial()),
|
||||
),
|
||||
BlocProvider<HomeSettingBloc>(
|
||||
create: (_) => HomeSettingBloc(
|
||||
workspaceSetting,
|
||||
workspaceLatest,
|
||||
context.read<AppearanceSettingsCubit>(),
|
||||
context.widthPx,
|
||||
)..add(const HomeSettingEvent.initial()),
|
||||
|
@ -137,7 +137,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
|||
child: _buildBody(
|
||||
context,
|
||||
userProfile,
|
||||
workspaceSetting,
|
||||
workspaceLatest,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -157,7 +157,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
|||
Widget _buildBody(
|
||||
BuildContext context,
|
||||
UserProfilePB userProfile,
|
||||
WorkspaceSettingPB workspaceSetting,
|
||||
WorkspaceLatestPB workspaceSetting,
|
||||
) {
|
||||
final layout = HomeLayout(context);
|
||||
final homeStack = HomeStack(
|
||||
|
@ -190,7 +190,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
|||
BuildContext context, {
|
||||
required HomeLayout layout,
|
||||
required UserProfilePB userProfile,
|
||||
required WorkspaceSettingPB workspaceSetting,
|
||||
required WorkspaceLatestPB workspaceSetting,
|
||||
}) {
|
||||
final homeMenu = HomeSideBar(
|
||||
userProfile: userProfile,
|
||||
|
|
|
@ -631,7 +631,7 @@ class PageNotifier extends ChangeNotifier {
|
|||
}
|
||||
|
||||
// Set the plugin view as the latest view.
|
||||
if (setLatest) {
|
||||
if (setLatest && newPlugin.id.isNotEmpty) {
|
||||
FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send();
|
||||
}
|
||||
|
||||
|
|
|
@ -105,9 +105,9 @@ class SidebarToast extends StatelessWidget {
|
|||
if (role.isOwner) {
|
||||
showSettingsDialog(
|
||||
context,
|
||||
userProfile,
|
||||
userWorkspaceBloc,
|
||||
SettingsPage.plan,
|
||||
userProfile: userProfile,
|
||||
userWorkspaceBloc: userWorkspaceBloc,
|
||||
initPage: SettingsPage.plan,
|
||||
);
|
||||
} else {
|
||||
final String message;
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/password/password_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
|
||||
|
@ -33,7 +34,7 @@ HotKeyItem openSettingsHotKey(
|
|||
),
|
||||
keyDownHandler: (_) {
|
||||
if (_settingsDialogKey.currentContext == null) {
|
||||
showSettingsDialog(context, userProfile);
|
||||
showSettingsDialog(context, userProfile: userProfile);
|
||||
} else {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.popUntil((route) => route.isFirst);
|
||||
|
@ -57,37 +58,55 @@ class UserSettingButton extends StatefulWidget {
|
|||
|
||||
class _UserSettingButtonState extends State<UserSettingButton> {
|
||||
late UserWorkspaceBloc _userWorkspaceBloc;
|
||||
late PasswordBloc _passwordBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
_passwordBloc = PasswordBloc(widget.userProfile)
|
||||
..add(PasswordEvent.init())
|
||||
..add(PasswordEvent.checkHasPassword());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passwordBloc.close();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: 24.0,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.settings_menu_open.tr(),
|
||||
child: FlowyButton(
|
||||
onTap: () => showSettingsDialog(
|
||||
context,
|
||||
widget.userProfile,
|
||||
_userWorkspaceBloc,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
text: FlowySvg(
|
||||
FlowySvgs.settings_s,
|
||||
color:
|
||||
widget.isHover ? Theme.of(context).colorScheme.onSurface : null,
|
||||
opacity: 0.7,
|
||||
child: BlocProvider.value(
|
||||
value: _passwordBloc,
|
||||
child: FlowyButton(
|
||||
onTap: () => showSettingsDialog(
|
||||
context,
|
||||
userProfile: widget.userProfile,
|
||||
userWorkspaceBloc: _userWorkspaceBloc,
|
||||
passwordBloc: _passwordBloc,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
text: FlowySvg(
|
||||
FlowySvgs.settings_s,
|
||||
color: widget.isHover
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: null,
|
||||
opacity: 0.7,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -96,21 +115,33 @@ class _UserSettingButtonState extends State<UserSettingButton> {
|
|||
}
|
||||
|
||||
void showSettingsDialog(
|
||||
BuildContext context,
|
||||
UserProfilePB userProfile, [
|
||||
UserWorkspaceBloc? bloc,
|
||||
BuildContext context, {
|
||||
required UserProfilePB userProfile,
|
||||
UserWorkspaceBloc? userWorkspaceBloc,
|
||||
PasswordBloc? passwordBloc,
|
||||
SettingsPage? initPage,
|
||||
]) {
|
||||
}) {
|
||||
AFFocusManager.maybeOf(context)?.notifyLoseFocus();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => MultiBlocProvider(
|
||||
key: _settingsDialogKey,
|
||||
providers: [
|
||||
passwordBloc != null
|
||||
? BlocProvider<PasswordBloc>.value(
|
||||
value: passwordBloc,
|
||||
)
|
||||
: BlocProvider(
|
||||
create: (context) => PasswordBloc(userProfile)
|
||||
..add(PasswordEvent.init())
|
||||
..add(PasswordEvent.checkHasPassword()),
|
||||
),
|
||||
BlocProvider<DocumentAppearanceCubit>.value(
|
||||
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
||||
),
|
||||
BlocProvider.value(value: bloc ?? context.read<UserWorkspaceBloc>()),
|
||||
BlocProvider.value(
|
||||
value: userWorkspaceBloc ?? context.read<UserWorkspaceBloc>(),
|
||||
),
|
||||
],
|
||||
child: SettingsDialog(
|
||||
userProfile,
|
||||
|
|
|
@ -60,7 +60,7 @@ class HomeSideBar extends StatelessWidget {
|
|||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
final WorkspaceLatestPB workspaceSetting;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
|||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
|
@ -173,42 +174,53 @@ class SpaceCancelOrConfirmButton extends StatelessWidget {
|
|||
required this.onConfirm,
|
||||
required this.confirmButtonName,
|
||||
this.confirmButtonColor,
|
||||
this.confirmButtonBuilder,
|
||||
});
|
||||
|
||||
final VoidCallback onCancel;
|
||||
final VoidCallback onConfirm;
|
||||
final String confirmButtonName;
|
||||
final Color? confirmButtonColor;
|
||||
|
||||
final WidgetBuilder? confirmButtonBuilder;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
OutlinedRoundedButton(
|
||||
AFOutlinedTextButton.normal(
|
||||
text: LocaleKeys.button_cancel.tr(),
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
onTap: onCancel,
|
||||
),
|
||||
const HSpace(12.0),
|
||||
DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: confirmButtonColor ?? Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
if (confirmButtonBuilder != null) ...[
|
||||
confirmButtonBuilder!(context),
|
||||
] else ...[
|
||||
DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color:
|
||||
confirmButtonColor ?? Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),
|
||||
radius: BorderRadius.circular(8),
|
||||
text: FlowyText.regular(
|
||||
confirmButtonName,
|
||||
lineHeight: 1.0,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onTap: onConfirm,
|
||||
),
|
||||
),
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),
|
||||
radius: BorderRadius.circular(8),
|
||||
text: FlowyText.regular(
|
||||
confirmButtonName,
|
||||
lineHeight: 1.0,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onTap: onConfirm,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -249,17 +261,11 @@ enum ConfirmPopupStyle {
|
|||
|
||||
class ConfirmPopupColor {
|
||||
static Color titleColor(BuildContext context) {
|
||||
if (Theme.of(context).isLightMode) {
|
||||
return const Color(0xFF171717).withValues(alpha: 0.8);
|
||||
}
|
||||
return const Color(0xFFffffff).withValues(alpha: 0.8);
|
||||
return AppFlowyTheme.of(context).textColorScheme.primary;
|
||||
}
|
||||
|
||||
static Color descriptionColor(BuildContext context) {
|
||||
if (Theme.of(context).isLightMode) {
|
||||
return const Color(0xFF171717).withValues(alpha: 0.7);
|
||||
}
|
||||
return const Color(0xFFffffff).withValues(alpha: 0.7);
|
||||
return AppFlowyTheme.of(context).textColorScheme.primary;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,6 +279,7 @@ class ConfirmPopup extends StatefulWidget {
|
|||
this.onCancel,
|
||||
this.confirmLabel,
|
||||
this.confirmButtonColor,
|
||||
this.confirmButtonBuilder,
|
||||
this.child,
|
||||
this.closeOnAction = true,
|
||||
this.showCloseButton = true,
|
||||
|
@ -315,6 +322,10 @@ class ConfirmPopup extends StatefulWidget {
|
|||
///
|
||||
final bool enableKeyboardListener;
|
||||
|
||||
/// Allows to build a custom confirm button.
|
||||
///
|
||||
final WidgetBuilder? confirmButtonBuilder;
|
||||
|
||||
@override
|
||||
State<ConfirmPopup> createState() => _ConfirmPopupState();
|
||||
}
|
||||
|
@ -368,28 +379,28 @@ class _ConfirmPopupState extends State<ConfirmPopup> {
|
|||
}
|
||||
|
||||
Widget _buildTitle() {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
child: Text(
|
||||
widget.title,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 22.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: ConfirmPopupColor.titleColor(context),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: ConfirmPopupColor.titleColor(context),
|
||||
),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
if (widget.showCloseButton) ...[
|
||||
FlowyButton(
|
||||
margin: const EdgeInsets.all(3),
|
||||
useIntrinsicWidth: true,
|
||||
text: const FlowySvg(
|
||||
FlowySvgs.upgrade_close_s,
|
||||
size: Size.square(18.0),
|
||||
),
|
||||
AFGhostButton.normal(
|
||||
size: AFButtonSize.s,
|
||||
padding: EdgeInsets.all(theme.spacing.xs),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
builder: (context, isHovering, disabled) => FlowySvg(
|
||||
FlowySvgs.password_close_m,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
@ -401,18 +412,24 @@ class _ConfirmPopupState extends State<ConfirmPopup> {
|
|||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return FlowyText.regular(
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Text(
|
||||
widget.description,
|
||||
fontSize: 16.0,
|
||||
color: ConfirmPopupColor.descriptionColor(context),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: ConfirmPopupColor.descriptionColor(context),
|
||||
),
|
||||
maxLines: 5,
|
||||
figmaLineHeight: 22.0,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStyledButton(BuildContext context) {
|
||||
switch (widget.style) {
|
||||
case ConfirmPopupStyle.onlyOk:
|
||||
if (widget.confirmButtonBuilder != null) {
|
||||
return widget.confirmButtonBuilder!(context);
|
||||
}
|
||||
|
||||
return SpaceOkButton(
|
||||
onConfirm: () {
|
||||
widget.onConfirm();
|
||||
|
@ -440,6 +457,7 @@ class _ConfirmPopupState extends State<ConfirmPopup> {
|
|||
widget.confirmLabel ?? LocaleKeys.space_delete.tr(),
|
||||
confirmButtonColor:
|
||||
widget.confirmButtonColor ?? Theme.of(context).colorScheme.error,
|
||||
confirmButtonBuilder: widget.confirmButtonBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue