chore: merge with main

This commit is contained in:
Zack Fu Zi Xiang 2024-05-09 17:05:00 +08:00
commit 6fde759061
No known key found for this signature in database
71 changed files with 1891 additions and 745 deletions

View File

@ -88,7 +88,16 @@ jobs:
model: 'iPhone 15'
shutdown_after_job: false
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
# - name: Run AppFlowy on simulator
# working-directory: frontend/appflowy_flutter
# run: |
# flutter run -d ${{ steps.simulator-action.outputs.udid }} &
# pid=$!
# sleep 500
# kill $pid
# continue-on-error: true
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/settings_appearance.dart';
@ -82,8 +83,10 @@ void main() {
await tester.openSettingsPage(SettingsPage.appearance);
expect(
find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
findsOneWidget,
find.textContaining(
DefaultAppearanceSettings.kDefaultFontFamily.fontFamilyDisplayName,
),
findsNWidgets(2),
);
});
});

View File

@ -0,0 +1,139 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/dir.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document page style', () {
double getCurrentEditorFontSize() {
final editorPage = find
.byType(AppFlowyEditorPage)
.evaluate()
.single
.widget as AppFlowyEditorPage;
return editorPage.styleCustomizer
.style()
.textStyleConfiguration
.text
.fontSize!;
}
double getCurrentEditorLineHeight() {
final editorPage = find
.byType(AppFlowyEditorPage)
.evaluate()
.single
.widget as AppFlowyEditorPage;
return editorPage.styleCustomizer
.style()
.textStyleConfiguration
.text
.height!;
}
testWidgets('change font size in page style settings', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
expect(getCurrentEditorFontSize(), PageStyleFontLayout.normal.fontSize);
// change font size from normal to large
await tester.tapSvgButton(FlowySvgs.m_font_size_large_s);
expect(getCurrentEditorFontSize(), PageStyleFontLayout.large.fontSize);
// change font size from large to small
await tester.tapSvgButton(FlowySvgs.m_font_size_small_s);
expect(getCurrentEditorFontSize(), PageStyleFontLayout.small.fontSize);
});
testWidgets('change line height in page style settings', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.normal.lineHeight,
);
// change line height from normal to large
await tester.tapSvgButton(FlowySvgs.m_layout_large_s);
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.large.lineHeight,
);
// change line height from large to small
await tester.tapSvgButton(FlowySvgs.m_layout_small_s);
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.small.lineHeight,
);
});
testWidgets('use built-in image as cover', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
// toggle the preset button
await tester.tapSvgButton(FlowySvgs.m_page_style_presets_m);
// select the first preset
final firstBuiltInImage = find.byWidgetPredicate(
(widget) =>
widget is Image &&
widget.image is AssetImage &&
(widget.image as AssetImage).assetName ==
PageStyleCoverImageType.builtInImagePath('1'),
);
await tester.tap(firstBuiltInImage);
// click done button to exit the page style settings
await tester.tapButton(find.byType(BottomSheetDoneButton).first);
await tester.tapButton(find.byType(BottomSheetDoneButton).first);
// check the cover
final builtInCover = find.descendant(
of: find.byType(DocumentImmersiveCover),
matching: firstBuiltInImage,
);
expect(builtInCover, findsOneWidget);
});
});
}

View File

@ -1,8 +1,10 @@
import 'package:integration_test/integration_test.dart';
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
Future<void> runIntegrationOnMobile() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
anonymous_sign_in_test.main();
create_new_page_test.main();
}

View File

@ -1,13 +1,10 @@
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
@ -31,6 +28,10 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'emoji.dart';
@ -537,6 +538,30 @@ extension CommonOperations on WidgetTester {
await tapButtonWithName(LocaleKeys.button_ok.tr());
}
// For mobile platform to launch the app in anonymous mode
Future<void> launchInAnonymousMode() async {
assert(
[TargetPlatform.android, TargetPlatform.iOS]
.contains(defaultTargetPlatform),
'This method is only supported on mobile platforms',
);
await initializeAppFlowy();
final anonymousSignInButton = find.byType(SignInAnonymousButtonV2);
expect(anonymousSignInButton, findsOneWidget);
await tapButton(anonymousSignInButton);
await pumpUntilFound(find.byType(MobileHomeScreen));
}
Future<void> tapSvgButton(FlowySvgData svg) async {
final button = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg.path == svg.path,
);
await tapButton(button);
}
}
extension SettingsFinder on CommonFinders {

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
@ -12,8 +11,10 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
@ -183,25 +184,35 @@ extension Expectation on WidgetTester {
String? parentName,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
if (PlatformExtension.isDesktop) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}
return find.descendant(
of: find.byWidgetPredicate(
(widget) =>
widget is InnerViewItem &&
widget.view.name == parentName &&
widget.view.layout == parentLayout,
skipOffstage: false,
),
matching: findPageName(name, layout: layout),
);
}
return find.descendant(
of: find.byWidgetPredicate(
(widget) =>
widget is InnerViewItem &&
widget.view.name == parentName &&
widget.view.layout == parentLayout,
skipOffstage: false,
),
matching: findPageName(name, layout: layout),
return find.byWidgetPredicate(
(widget) =>
widget is SingleMobileInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}

View File

@ -0,0 +1,99 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'mobile_view_page_bloc.freezed.dart';
class MobileViewPageBloc
extends Bloc<MobileViewPageEvent, MobileViewPageState> {
MobileViewPageBloc({
required this.viewId,
}) : _viewListener = ViewListener(viewId: viewId),
super(MobileViewPageState.initial()) {
on<MobileViewPageEvent>(
(event, emit) async {
await event.when(
initial: () async {
_registerListeners();
final result = await ViewBackendService.getView(viewId);
final isImmersiveMode =
_isImmersiveMode(result.fold((s) => s, (f) => null));
emit(
state.copyWith(
isLoading: false,
result: result,
isImmersiveMode: isImmersiveMode,
),
);
},
updateImmersionMode: (isImmersiveMode) {
emit(
state.copyWith(
isImmersiveMode: isImmersiveMode,
),
);
},
);
},
);
}
final String viewId;
final ViewListener _viewListener;
@override
Future<void> close() {
_viewListener.stop();
return super.close();
}
void _registerListeners() {
_viewListener.start(
onViewUpdated: (view) {
final isImmersiveMode = _isImmersiveMode(view);
add(MobileViewPageEvent.updateImmersionMode(isImmersiveMode));
},
);
}
// only the document page supports immersive mode (version 0.5.6)
bool _isImmersiveMode(ViewPB? view) {
if (view == null) {
return false;
}
final cover = view.cover;
if (cover == null || cover.type == PageStyleCoverImageType.none) {
return false;
} else if (view.layout == ViewLayoutPB.Document) {
// only support immersive mode for document layout
return true;
}
return false;
}
}
@freezed
class MobileViewPageEvent with _$MobileViewPageEvent {
const factory MobileViewPageEvent.initial() = Initial;
const factory MobileViewPageEvent.updateImmersionMode(bool isImmersiveMode) =
UpdateImmersionMode;
}
@freezed
class MobileViewPageState with _$MobileViewPageState {
const factory MobileViewPageState({
@Default(true) bool isLoading,
@Default(null) FlowyResult<ViewPB, FlowyError>? result,
@Default(false) bool isImmersiveMode,
}) = _MobileViewPageState;
factory MobileViewPageState.initial() => const MobileViewPageState();
}

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
@ -179,8 +178,8 @@ class DocumentPageStyleBloc
);
}
String _getSelectedFontFamily(Map layoutObject) {
return layoutObject[ViewExtKeys.fontKey] ?? builtInFontFamily();
String? _getSelectedFontFamily(Map layoutObject) {
return layoutObject[ViewExtKeys.fontKey];
}
(PageStyleCoverImageType, String colorValue) _getSelectedCover(

View File

@ -1,14 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart';
import 'package:appflowy/plugins/shared/sync_indicator.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
@ -16,16 +12,12 @@ import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPage extends StatefulWidget {
const MobileViewPage({
@ -47,94 +39,33 @@ class MobileViewPage extends StatefulWidget {
}
class _MobileViewPageState extends State<MobileViewPage> {
late final Future<FlowyResult<ViewPB, FlowyError>> future;
// used to determine if the user has scrolled down and show the app bar in immersive mode
ScrollNotificationObserverState? _scrollNotificationObserver;
// control the app bar opacity when in immersive mode
final ValueNotifier<double> _appBarOpacity = ValueNotifier(0.0);
// only enable immersive mode for document layout
final ValueNotifier<bool> _isImmersiveMode = ValueNotifier(false);
ViewListener? viewListener;
@override
void initState() {
super.initState();
future = ViewBackendService.getView(widget.id);
}
final ValueNotifier<double> _appBarOpacity = ValueNotifier(1.0);
@override
void dispose() {
_appBarOpacity.dispose();
_isImmersiveMode.dispose();
viewListener?.stop();
_scrollNotificationObserver = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final child = FutureBuilder(
future: future,
builder: (context, state) {
Widget body;
ViewPB? viewPB;
final actions = <Widget>[];
if (state.connectionState != ConnectionState.done) {
body = const Center(
child: CircularProgressIndicator(),
);
} else if (!state.hasData) {
body = FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: state.error.toString(),
);
} else {
body = state.data!.fold((view) {
viewPB = view;
_updateImmersiveMode(view);
viewListener?.stop();
viewListener = ViewListener(viewId: view.id)
..start(
onViewUpdated: _updateImmersiveMode,
);
return BlocProvider(
create: (_) => MobileViewPageBloc(viewId: widget.id)
..add(const MobileViewPageEvent.initial()),
child: BlocBuilder<MobileViewPageBloc, MobileViewPageState>(
builder: (context, state) {
final view = state.result?.fold((s) => s, (f) => null);
final body = _buildBody(context, state);
actions.addAll([
if (FeatureFlag.syncDocument.isOn) ...[
DocumentCollaborators(
width: 60,
height: 44,
fontSize: 14,
padding: const EdgeInsets.symmetric(vertical: 8),
view: view,
),
const HSpace(16.0),
view.layout == ViewLayoutPB.Document
? DocumentSyncIndicator(view: view)
: DatabaseSyncIndicator(view: view),
const HSpace(8.0),
],
_buildAppBarLayoutButton(view),
_buildAppBarMoreButton(view),
]);
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
}, (error) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: error.toString(),
);
});
}
if (view == null) {
return _buildApp(context, null, body);
}
if (viewPB != null) {
return MultiBlocProvider(
providers: [
BlocProvider(
@ -143,47 +74,165 @@ class _MobileViewPageState extends State<MobileViewPage> {
),
BlocProvider(
create: (_) =>
ViewBloc(view: viewPB!)..add(const ViewEvent.initial()),
ViewBloc(view: view)..add(const ViewEvent.initial()),
),
BlocProvider.value(
value: getIt<ReminderBloc>()
..add(const ReminderEvent.started()),
),
if (viewPB!.layout == ViewLayoutPB.Document)
if (view.layout.isDocumentView)
BlocProvider(
create: (_) => DocumentPageStyleBloc(view: viewPB!)
..add(
const DocumentPageStyleEvent.initial(),
),
create: (_) => DocumentPageStyleBloc(view: view)
..add(const DocumentPageStyleEvent.initial()),
),
],
child: Builder(
builder: (context) {
final view = context.watch<ViewBloc>().state.view;
return _buildApp(view, actions, body);
return _buildApp(context, view, body);
},
),
);
} else {
return _buildApp(null, [], body);
}
},
},
),
);
return child;
}
Widget _buildApp(ViewPB? view, List<Widget> actions, Widget child) {
// only enable immersive mode for document layout
final isImmersive = view?.layout == ViewLayoutPB.Document;
Widget _buildApp(
BuildContext context,
ViewPB? view,
Widget child,
) {
final isImmersiveMode = view?.layout.isDocumentView ?? false;
final title = _buildTitle(context, view);
final appBar = MobileViewPageImmersiveAppBar(
preferredSize: Size(
double.infinity,
AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight,
),
title: title,
isImmersiveMode: isImmersiveMode,
appBarOpacity: _appBarOpacity,
actions: _buildAppBarActions(context, view),
);
final body = isImmersiveMode
? Builder(
builder: (context) {
_rebuildScrollNotificationObserver(context);
return child;
},
)
: child;
return Scaffold(
extendBodyBehindAppBar: isImmersiveMode,
appBar: appBar,
body: body,
);
}
Widget _buildBody(BuildContext context, MobileViewPageState state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
final result = state.result;
if (result == null) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: '',
);
}
return result.fold(
(view) {
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
},
(error) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: error.toString(),
);
},
);
}
// Document:
// - [ collaborators, sync_indicator, layout_button, more_button]
// Database:
// - [ sync_indicator, more_button]
List<Widget> _buildAppBarActions(BuildContext context, ViewPB? view) {
if (view == null) {
return [];
}
final isImmersiveMode =
context.read<MobileViewPageBloc>().state.isImmersiveMode;
final actions = <Widget>[];
if (FeatureFlag.syncDocument.isOn) {
// only document supports displaying collaborators.
if (view.layout.isDocumentView) {
actions.addAll([
DocumentCollaborators(
width: 60,
height: 44,
fontSize: 14,
padding: const EdgeInsets.symmetric(vertical: 8),
view: view,
),
const HSpace(16.0),
DocumentSyncIndicator(view: view),
const HSpace(8.0),
]);
} else {
actions.addAll([
DatabaseSyncIndicator(view: view),
const HSpace(8.0),
]);
}
}
if (view.layout.isDocumentView) {
actions.addAll([
MobileViewPageLayoutButton(
view: view,
isImmersiveMode: isImmersiveMode,
appBarOpacity: _appBarOpacity,
),
]);
}
actions.addAll([
MobileViewPageMoreButton(
view: view,
isImmersiveMode: isImmersiveMode,
appBarOpacity: _appBarOpacity,
),
]);
return actions;
}
Widget _buildTitle(BuildContext context, ViewPB? view) {
final icon = view?.icon.value;
final title = Row(
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.isNotEmpty)
EmojiText(
emoji: '$icon ',
fontSize: 22.0,
ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 34.0),
child: EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
),
Expanded(
child: FlowyText.medium(
@ -194,51 +243,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
),
],
);
if (isImmersive) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: Size(
double.infinity,
AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight,
),
child: ValueListenableBuilder(
valueListenable: _appBarOpacity,
builder: (_, opacity, __) => FlowyAppBar(
backgroundColor:
AppBarTheme.of(context).backgroundColor?.withOpacity(opacity),
showDivider: false,
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
leading: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 2.0, vertical: 4.0),
child: AppBarButton(
padding: EdgeInsets.zero,
onTap: (context) => context.pop(),
child: _buildImmersiveAppBarIcon(FlowySvgs.m_app_bar_back_s),
),
),
actions: actions,
),
),
),
body: Builder(
builder: (context) {
_rebuildScrollNotificationObserver(context);
return child;
},
),
);
}
return Scaffold(
appBar: FlowyAppBar(
title: title,
actions: actions,
),
body: child,
);
}
void _rebuildScrollNotificationObserver(BuildContext context) {
@ -247,150 +251,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
_scrollNotificationObserver?.addListener(_onScrollNotification);
}
Widget _buildAppBarLayoutButton(ViewPB view) {
// only display the layout button if the view is a document
if (view.layout != ViewLayoutPB.Document) {
return const SizedBox.shrink();
}
return AppBarButton(
padding: const EdgeInsets.symmetric(vertical: 2.0),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
title: LocaleKeys.pageStyle_title.tr(),
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
child: PageStyleBottomSheet(
view: context.read<ViewBloc>().state.view,
),
),
);
},
child: _buildImmersiveAppBarIcon(FlowySvgs.m_layout_s),
);
}
Widget _buildAppBarMoreButton(ViewPB view) {
return AppBarButton(
padding: const EdgeInsets.only(left: 8, right: 16, top: 2, bottom: 2),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => _buildAppBarMoreBottomSheet(context),
);
},
child: _buildImmersiveAppBarIcon(FlowySvgs.m_app_bar_more_s),
);
}
Widget _buildImmersiveAppBarIcon(FlowySvgData icon) {
return ValueListenableBuilder(
valueListenable: _isImmersiveMode,
builder: (context, isImmersiveMode, child) {
return ValueListenableBuilder(
valueListenable: _appBarOpacity,
builder: (context, appBarOpacity, child) {
Color? color;
// if there's no cover or the cover is not immersive,
// make sure the app bar is always visible
if (!isImmersiveMode) {
color = null;
} else if (appBarOpacity < 0.99) {
color = Colors.white;
}
Widget child = Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: FlowySvg(
icon,
color: color,
),
);
if (isImmersiveMode && appBarOpacity <= 0.99) {
child = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
color: Colors.black.withOpacity(0.2),
),
child: child,
);
}
return child;
},
);
},
);
}
Widget _buildAppBarMoreBottomSheet(BuildContext context) {
final view = context.read<ViewBloc>().state.view;
return ViewPageBottomSheet(
view: view,
onAction: (action) {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
// show toast
break;
case MobileViewBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
// pop to home page
context
..pop()
..pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.pop();
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
if (name != view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
// immersive mode related
// auto show or hide the app bar based on the scroll position
void _onScrollNotification(ScrollNotification notification) {
@ -401,7 +261,10 @@ class _MobileViewPageState extends State<MobileViewPage> {
if (notification is ScrollUpdateNotification &&
defaultScrollNotificationPredicate(notification)) {
final ScrollMetrics metrics = notification.metrics;
final height = MediaQuery.of(context).padding.top;
double height = MediaQuery.of(context).padding.top;
if (defaultTargetPlatform == TargetPlatform.android) {
height += AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight;
}
final progress = (metrics.pixels / height).clamp(0.0, 1.0);
// reduce the sensitivity of the app bar opacity change
if ((progress - _appBarOpacity.value).abs() >= 0.1 ||
@ -411,16 +274,4 @@ class _MobileViewPageState extends State<MobileViewPage> {
}
}
}
void _updateImmersiveMode(ViewPB view) {
final cover = view.cover;
if (cover == null || cover.type == PageStyleCoverImageType.none) {
_isImmersiveMode.value = false;
} else if (view.layout != ViewLayoutPB.Document) {
// only support immersive mode for document layout
_isImmersiveMode.value = false;
} else {
_isImmersiveMode.value = true;
}
}
}

View File

@ -0,0 +1,234 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/more_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPageImmersiveAppBar extends StatelessWidget
implements PreferredSizeWidget {
const MobileViewPageImmersiveAppBar({
super.key,
required this.preferredSize,
required this.isImmersiveMode,
required this.appBarOpacity,
required this.title,
required this.actions,
});
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
final Widget title;
final List<Widget> actions;
@override
final Size preferredSize;
@override
Widget build(BuildContext context) {
if (!isImmersiveMode) {
FlowyAppBar(
title: title,
actions: actions,
);
}
return ValueListenableBuilder(
valueListenable: appBarOpacity,
builder: (_, opacity, __) => FlowyAppBar(
backgroundColor:
AppBarTheme.of(context).backgroundColor?.withOpacity(opacity),
showDivider: false,
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 4.0),
child: _buildAppBarBackButton(context),
),
actions: actions,
),
);
}
Widget _buildAppBarBackButton(BuildContext context) {
return AppBarButton(
padding: EdgeInsets.zero,
onTap: (context) => context.pop(),
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_app_bar_back_s,
dimension: 30.0,
iconPadding: 6.0,
isImmersiveMode: isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class MobileViewPageMoreButton extends StatelessWidget {
const MobileViewPageMoreButton({
super.key,
required this.view,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final ViewPB view;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
return AppBarButton(
padding: const EdgeInsets.only(left: 8, right: 16),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<ViewBloc>()),
BlocProvider.value(value: context.read<FavoriteBloc>()),
],
child: MobileViewPageMoreBottomSheet(view: view),
),
);
},
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_app_bar_more_s,
dimension: 30.0,
iconPadding: 5.0,
isImmersiveMode: isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class MobileViewPageLayoutButton extends StatelessWidget {
const MobileViewPageLayoutButton({
super.key,
required this.view,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final ViewPB view;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
// only display the layout button if the view is a document
if (view.layout != ViewLayoutPB.Document) {
return const SizedBox.shrink();
}
return AppBarButton(
padding: const EdgeInsets.symmetric(vertical: 2.0),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
title: LocaleKeys.pageStyle_title.tr(),
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
child: PageStyleBottomSheet(
view: context.read<ViewBloc>().state.view,
),
),
);
},
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_layout_s,
dimension: 30.0,
iconPadding: 5.0,
isImmersiveMode: isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class _ImmersiveAppBarButton extends StatelessWidget {
const _ImmersiveAppBarButton({
required this.icon,
required this.dimension,
required this.iconPadding,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final FlowySvgData icon;
final double dimension;
final double iconPadding;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
assert(
dimension > 0.0 && dimension <= kToolbarHeight,
'dimension must be greater than 0, and less than or equal to kToolbarHeight',
);
// if the immersive mode is on, the icon should be white and add a black background
// also, the icon opacity will change based on the app bar opacity
return UnconstrainedBox(
child: SizedBox.square(
dimension: dimension,
child: ValueListenableBuilder(
valueListenable: appBarOpacity,
builder: (context, appBarOpacity, child) {
Color? color;
// if there's no cover or the cover is not immersive,
// make sure the app bar is always visible
if (!isImmersiveMode) {
color = null;
} else if (appBarOpacity < 0.99) {
color = Colors.white;
}
Widget child = Container(
margin: EdgeInsets.all(iconPadding),
child: FlowySvg(icon, color: color),
);
if (isImmersiveMode && appBarOpacity <= 0.99) {
child = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimension / 2.0),
color: Colors.black.withOpacity(0.2),
),
child: child,
);
}
return child;
},
),
),
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPageMoreBottomSheet extends StatelessWidget {
const MobileViewPageMoreBottomSheet({super.key, required this.view});
final ViewPB view;
@override
Widget build(BuildContext context) {
return ViewPageBottomSheet(
view: view,
onAction: (action) {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
// show toast
break;
case MobileViewBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
// pop to home page
context
..pop()
..pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.pop();
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
if (name != view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
}

View File

@ -3,7 +3,7 @@ import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:easy_localization/easy_localization.dart';
@ -13,7 +13,7 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
final List<String> _availableFonts = [
builtInFontFamily(),
defaultFontFamily,
...GoogleFonts.asMap().keys,
];
@ -106,16 +106,12 @@ class _FontSelectorState extends State<FontSelector> {
}
final fontFamilyName = availableFonts[index - 1];
final usingDefaultFontFamily = fontFamilyName == builtInFontFamily();
final usingDefaultFontFamily = fontFamilyName == defaultFontFamily;
final fontFamily = !usingDefaultFontFamily
? getGoogleFontSafely(fontFamilyName).fontFamily
: TextStyle(fontFamily: builtInFontFamily()).fontFamily;
: defaultFontFamily;
return FlowyOptionTile.checkbox(
// display the default font name if the font family name is empty
// or using the default font family
text: fontFamilyName.isNotEmpty && !usingDefaultFontFamily
? fontFamilyName.parseFontFamilyName()
: LocaleKeys.settings_appearance_fontFamily_defaultFont.tr(),
text: fontFamilyName.fontFamilyDisplayName,
isSelected: widget.selectedFontFamilyName == fontFamilyName,
showTopBorder: false,
onTap: () => widget.onFontFamilySelected(fontFamilyName),

View File

@ -3,8 +3,8 @@ import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -22,9 +22,7 @@ class FontSetting extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final selectedFont = context.watch<AppearanceSettingsCubit>().state.font;
final name = selectedFont == builtInFontFamily()
? LocaleKeys.settings_appearance_fontFamily_defaultFont.tr()
: selectedFont;
final name = selectedFont.fontFamilyDisplayName;
return MobileSettingItem(
name: LocaleKeys.settings_appearance_fontFamily_label.tr(),
trailing: Row(

View File

@ -54,12 +54,7 @@ class ViewTitleBarWithRow extends StatelessWidget {
return Visibility(
visible: maxWidth < constraints.maxWidth,
// if the width is too small, only show one view title bar without the ancestors
replacement: _ViewTitle(
key: ValueKey(state.ancestors.last),
view: state.ancestors.last,
maxTitleWidth: constraints.maxWidth - 50.0,
onUpdated: () {},
),
replacement: _buildRowName(),
child: Row(
// refresh the view title bar when the ancestors changed
key: ValueKey(state.ancestors.hashCode),
@ -104,42 +99,39 @@ class ViewTitleBarWithRow extends StatelessWidget {
}
Widget _buildRowName() {
return BlocBuilder<DatabaseDocumentTitleBloc, DatabaseDocumentTitleState>(
builder: (context, state) {
if (state.databaseController == null) {
return const SizedBox.shrink();
}
return _RowName(
cellBuilder: EditableCellBuilder(
databaseController: state.databaseController!,
),
primaryFieldId: state.fieldId!,
rowId: rowId,
);
},
return _RowName(
rowId: rowId,
);
}
}
class _RowName extends StatelessWidget {
const _RowName({
required this.cellBuilder,
required this.primaryFieldId,
required this.rowId,
});
final EditableCellBuilder cellBuilder;
final String primaryFieldId;
final String rowId;
@override
Widget build(BuildContext context) {
return cellBuilder.buildCustom(
CellContext(
fieldId: primaryFieldId,
rowId: rowId,
),
skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),
return BlocBuilder<DatabaseDocumentTitleBloc, DatabaseDocumentTitleState>(
builder: (context, state) {
if (state.databaseController == null) {
return const SizedBox.shrink();
}
final cellBuilder = EditableCellBuilder(
databaseController: state.databaseController!,
);
return cellBuilder.buildCustom(
CellContext(
fieldId: state.fieldId!,
rowId: rowId,
),
skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),
);
},
);
}
}
@ -220,12 +212,10 @@ enum _ViewTitleBehavior {
class _ViewTitle extends StatefulWidget {
const _ViewTitle({
super.key,
required this.view,
this.behavior = _ViewTitleBehavior.editable,
this.maxTitleWidth = 180,
required this.onUpdated,
});
}) : maxTitleWidth = 180;
final ViewPB view;
final _ViewTitleBehavior behavior;

View File

@ -57,9 +57,9 @@ class DocumentAppearance {
class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
DocumentAppearanceCubit()
: super(
DocumentAppearance(
const DocumentAppearance(
fontSize: 16.0,
fontFamily: builtInFontFamily(),
fontFamily: defaultFontFamily,
codeFontFamily: builtInCodeFontFamily,
),
);
@ -69,7 +69,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
final fontSize =
prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0;
final fontFamily = prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ??
builtInFontFamily();
defaultFontFamily;
final defaultTextDirection =
prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection);

View File

@ -127,7 +127,7 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
BuildContext context,
DocumentImmersiveCoverState state,
) {
String? fontFamily = builtInFontFamily();
String? fontFamily = defaultFontFamily;
final documentFontFamily =
context.read<DocumentPageStyleBloc>().state.fontFamily;
if (documentFontFamily != null && fontFamily != documentFontFamily) {

View File

@ -1,6 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/permission/permission_checker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -20,22 +22,28 @@ class UploadImageFileWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlowyHover(
child: FlowyButton(
showDefaultBoxDecorationOnMobile: true,
text: Container(
margin: const EdgeInsets.all(4.0),
alignment: Alignment.center,
child: FlowyText(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
),
final child = FlowyButton(
showDefaultBoxDecorationOnMobile: true,
text: Container(
margin: const EdgeInsets.all(4.0),
alignment: Alignment.center,
child: FlowyText(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
),
onTap: _uploadImage,
),
onTap: () => _uploadImage(context),
);
if (PlatformExtension.isDesktopOrWeb) {
return FlowyHover(
child: child,
);
}
return child;
}
Future<void> _uploadImage() async {
Future<void> _uploadImage(BuildContext context) async {
if (PlatformExtension.isDesktopOrWeb) {
// on desktop, the users can pick a image file from folder
final result = await getIt<FilePickerService>().pickFiles(
@ -45,6 +53,12 @@ class UploadImageFileWidget extends StatelessWidget {
);
onPickFile(result?.files.firstOrNull?.path);
} else {
final photoPermission =
await PermissionChecker.checkPhotoPermission(context);
if (!photoPermission) {
Log.error('Has no permission to access the photo library');
return;
}
// on mobile, the users can pick a image file from camera or image library
final result = await ImagePicker().pickImage(source: ImageSource.gallery);
onPickFile(result?.path);

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
@ -25,6 +23,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
@ -110,40 +109,46 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
}
final iconSize = widget.textStyle?.fontSize ?? 16.0;
final child = GestureDetector(
onTap: handleTap,
onDoubleTap: handleDoubleTap,
behavior: HitTestBehavior.translucent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 12,
textAlign: TextAlign.center,
lineHeight: 1.3,
)
: FlowySvg(
view.layout.icon,
size: Size.square(iconSize + 2.0),
),
const HSpace(2),
FlowyText(
view.name,
decoration: TextDecoration.underline,
fontSize: widget.textStyle?.fontSize,
fontWeight: widget.textStyle?.fontWeight,
),
const HSpace(2),
],
),
);
if (PlatformExtension.isMobile) {
return child;
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: FlowyHover(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: handleTap,
onDoubleTap: handleDoubleTap,
behavior: HitTestBehavior.translucent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 12,
textAlign: TextAlign.center,
lineHeight: 1.3,
)
: FlowySvg(
view.layout.icon,
size: Size.square(iconSize + 2.0),
),
const HSpace(2),
FlowyText(
view.name,
decoration: TextDecoration.underline,
fontSize: widget.textStyle?.fontSize,
fontWeight: widget.textStyle?.fontWeight,
),
const HSpace(2),
],
),
),
child: child,
),
);
},

View File

@ -5,7 +5,7 @@ import 'package:appflowy/plugins/document/application/document_appearance_cubit.
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

View File

@ -4,13 +4,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';
import 'package:appflowy/shared/feedback_gesture_detector.dart';
import 'package:appflowy/startup/tasks/device_info_task.dart';
import 'package:appflowy/shared/permission/permission_checker.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
@ -18,11 +17,9 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class PageStyleCoverImage extends StatelessWidget {
PageStyleCoverImage({
@ -121,7 +118,8 @@ class PageStyleCoverImage extends StatelessWidget {
}
Future<void> _pickImage(BuildContext context) async {
final photoPermission = await _checkPhotoPermission(context);
final photoPermission =
await PermissionChecker.checkPhotoPermission(context);
if (!photoPermission) {
Log.error('Has no permission to access the photo library');
return;
@ -129,9 +127,7 @@ class PageStyleCoverImage extends StatelessWidget {
XFile? result;
try {
result = await _imagePicker.pickImage(
source: ImageSource.gallery,
);
result = await _imagePicker.pickImage(source: ImageSource.gallery);
} catch (e) {
Log.error('Error while picking image: $e');
return;
@ -224,54 +220,6 @@ class PageStyleCoverImage extends StatelessWidget {
},
);
}
Future<bool> _checkPhotoPermission(BuildContext context) async {
// check the permission first
final status = await Permission.photos.status;
// if the permission is permanently denied, we should open the app settings
if (status.isPermanentlyDenied && context.mounted) {
unawaited(
showFlowyMobileConfirmDialog(
context,
title: FlowyText.semibold(
LocaleKeys.pageStyle_photoPermissionTitle.tr(),
maxLines: 3,
textAlign: TextAlign.center,
),
content: FlowyText(
LocaleKeys.pageStyle_photoPermissionDescription.tr(),
maxLines: 5,
textAlign: TextAlign.center,
fontSize: 12.0,
),
actionAlignment: ConfirmDialogActionAlignment.vertical,
actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(),
actionButtonColor: Colors.blue,
cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(),
cancelButtonColor: Colors.blue,
onActionButtonPressed: () {
openAppSettings();
},
),
);
return false;
} else if (status.isDenied) {
// https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937
Permission permission = Permission.photos;
if (defaultTargetPlatform == TargetPlatform.android &&
ApplicationInfo.androidSDKVersion <= 32) {
permission = Permission.storage;
}
// if the permission is denied, we should request the permission
final newStatus = await permission.request();
if (newStatus.isDenied) {
return false;
}
}
return true;
}
}
class _UnsplashCover extends StatelessWidget {

View File

@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_she
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';
import 'package:appflowy/shared/feedback_gesture_detector.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -163,11 +164,8 @@ class _FontButton extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, state) {
String fontFamily = state.fontFamily ?? builtInFontFamily();
if (fontFamily == builtInFontFamily()) {
fontFamily =
LocaleKeys.settings_appearance_fontFamily_defaultFont.tr();
}
final fontFamilyDisplayName =
(state.fontFamily ?? defaultFontFamily).fontFamilyDisplayName;
return GestureDetector(
onTap: () => _showFontSelector(context),
behavior: HitTestBehavior.opaque,
@ -182,7 +180,7 @@ class _FontButton extends StatelessWidget {
const HSpace(16.0),
FlowyText(LocaleKeys.titleBar_font.tr()),
const Spacer(),
FlowyText(fontFamily),
FlowyText(fontFamilyDisplayName),
const HSpace(6.0),
const FlowySvg(FlowySvgs.m_page_style_arrow_right_s),
const HSpace(12.0),
@ -219,7 +217,7 @@ class _FontButton extends StatelessWidget {
child: FontSelector(
scrollController: controller,
selectedFontFamilyName:
state.fontFamily ?? builtInFontFamily(),
state.fontFamily ?? defaultFontFamily,
onFontFamilySelected: (fontFamilyName) {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateFontFamily(

View File

@ -8,7 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
@ -92,9 +92,11 @@ class EditorStyleCustomizer {
final theme = Theme.of(context);
final fontSize = pageStyle.fontLayout.fontSize;
final lineHeight = pageStyle.lineHeightLayout.lineHeight;
final fontFamily = pageStyle.fontFamily ?? builtInFontFamily();
final fontFamily = pageStyle.fontFamily ?? defaultFontFamily;
final defaultTextDirection =
context.read<DocumentAppearanceCubit>().state.defaultTextDirection;
final textScaleFactor =
context.read<AppearanceSettingsCubit>().state.textScaleFactor;
final baseTextStyle = this.baseTextStyle(fontFamily);
final codeFontSize = max(0.0, fontSize - 2);
return EditorStyle.mobile(
@ -131,8 +133,7 @@ class EditorStyleCustomizer {
textSpanDecorator: customizeAttributeDecorator,
mobileDragHandleBallSize: const Size.square(12.0),
magnifierSize: const Size(144, 96),
textScaleFactor:
context.watch<AppearanceSettingsCubit>().state.textScaleFactor,
textScaleFactor: textScaleFactor,
);
}
@ -178,7 +179,7 @@ class EditorStyleCustomizer {
TextStyle outlineBlockPlaceholderStyleBuilder() {
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return TextStyle(
fontFamily: builtInFontFamily(),
fontFamily: defaultFontFamily,
fontSize: fontSize,
height: 1.5,
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.6),
@ -213,13 +214,14 @@ class EditorStyleCustomizer {
);
TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) {
if (fontFamily == null) {
if (fontFamily == null || fontFamily == defaultFontFamily) {
return TextStyle(fontWeight: fontWeight);
}
try {
return getGoogleFontSafely(fontFamily, fontWeight: fontWeight);
} on Exception {
if ([builtInFontFamily(), builtInCodeFontFamily].contains(fontFamily)) {
if ([defaultFontFamily, fallbackFontFamily, builtInCodeFontFamily]
.contains(fontFamily)) {
return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight);
}

View File

@ -0,0 +1,64 @@
// Check if the user has the required permission to access the device's
// - camera
// - storage
// - ...
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/startup/tasks/device_info_task.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
class PermissionChecker {
static Future<bool> checkPhotoPermission(BuildContext context) async {
// check the permission first
final status = await Permission.photos.status;
// if the permission is permanently denied, we should open the app settings
if (status.isPermanentlyDenied && context.mounted) {
unawaited(
showFlowyMobileConfirmDialog(
context,
title: FlowyText.semibold(
LocaleKeys.pageStyle_photoPermissionTitle.tr(),
maxLines: 3,
textAlign: TextAlign.center,
),
content: FlowyText(
LocaleKeys.pageStyle_photoPermissionDescription.tr(),
maxLines: 5,
textAlign: TextAlign.center,
fontSize: 12.0,
),
actionAlignment: ConfirmDialogActionAlignment.vertical,
actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(),
actionButtonColor: Colors.blue,
cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(),
cancelButtonColor: Colors.blue,
onActionButtonPressed: () {
openAppSettings();
},
),
);
return false;
} else if (status.isDenied) {
// https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937
Permission permission = Permission.photos;
if (defaultTargetPlatform == TargetPlatform.android &&
ApplicationInfo.androidSDKVersion <= 32) {
permission = Permission.storage;
}
// if the permission is denied, we should request the permission
final newStatus = await permission.request();
if (newStatus.isDenied) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,15 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:easy_localization/easy_localization.dart';
extension FontFamilyExtension on String {
String parseFontFamilyName() => replaceAll('_regular', '')
.replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}');
// display the default font name if the font family name is empty
// or using the default font family
String get fontFamilyDisplayName => isEmpty || this == defaultFontFamily
? LocaleKeys.settings_appearance_fontFamily_defaultFont.tr()
: parseFontFamilyName();
}

View File

@ -1,6 +0,0 @@
import 'package:appflowy/shared/patterns/common_patterns.dart';
extension GoogleFontsParser on String {
String parseFontFamilyName() => replaceAll('_regular', '')
.replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}');
}

View File

@ -1,9 +1,10 @@
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
/// A class for the default appearance settings for the app
class DefaultAppearanceSettings {
static const kDefaultFontFamily = 'Poppins';
static const kDefaultFontFamily = defaultFontFamily;
static const kDefaultThemeMode = ThemeMode.system;
static const kDefaultThemeName = "Default";
static const kDefaultTheme = BuiltInTheme.defaultTheme;

View File

@ -1,28 +1,19 @@
import 'dart:io';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
String builtInFontFamily() {
if (PlatformExtension.isDesktopOrWeb) {
return 'Poppins';
}
// the default font family is empty, so we can use the default font family of the platform
// the system will choose the default font family of the platform
// iOS: San Francisco
// Android: Roboto
// Desktop: Based on the OS
const defaultFontFamily = '';
if (Platform.isIOS) {
return 'San Francisco';
}
if (Platform.isAndroid) {
return 'Roboto';
}
return 'Roboto';
}
// 'Poppins';
// the Poppins font is embedded in the app, so we can use it without GoogleFonts
// TODO(Lucas): after releasing version 0.5.6, remove it.
const fallbackFontFamily = 'Poppins';
const builtInCodeFontFamily = 'RobotoMono';
abstract class BaseAppearance {
@ -48,17 +39,15 @@ abstract class BaseAppearance {
letterSpacing = fontSize * (letterSpacing ?? 0.005);
final textStyle = TextStyle(
fontFamily: fontFamily,
fontFamily: fontFamily.isEmpty ? null : fontFamily,
fontSize: fontSize,
color: fontColor,
fontWeight: fontWeight,
fontFamilyFallback: [builtInFontFamily()],
letterSpacing: letterSpacing,
height: lineHeight,
);
// we embed Poppins font in the app, so we can use it without GoogleFonts
if (fontFamily == builtInFontFamily()) {
if (fontFamily == defaultFontFamily || fontFamily == fallbackFontFamily) {
return textStyle;
}

View File

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
class DesktopAppearance extends BaseAppearance {
@override
@ -13,7 +12,6 @@ class DesktopAppearance extends BaseAppearance {
String fontFamily,
String codeFontFamily,
) {
assert(fontFamily.isNotEmpty);
assert(codeFontFamily.isNotEmpty);
final theme = brightness == Brightness.light

View File

@ -151,6 +151,15 @@ extension ViewLayoutExtension on ViewLayoutPB {
_ => throw Exception('Unknown layout type'),
};
bool get isDocumentView => switch (this) {
ViewLayoutPB.Document => true,
ViewLayoutPB.Grid ||
ViewLayoutPB.Board ||
ViewLayoutPB.Calendar =>
false,
_ => throw Exception('Unknown layout type'),
};
bool get isDatabaseView => switch (this) {
ViewLayoutPB.Grid ||
ViewLayoutPB.Board ||

View File

@ -1,17 +1,17 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
@ -83,7 +83,10 @@ class FontFamilyDropDown extends StatefulWidget {
}
class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
final List<String> availableFonts = GoogleFonts.asMap().keys.toList();
final List<String> availableFonts = [
defaultFontFamily,
...GoogleFonts.asMap().keys,
];
final ValueNotifier<String> query = ValueNotifier('');
@override
@ -94,10 +97,11 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
@override
Widget build(BuildContext context) {
final currentValue = widget.currentFontFamily.fontFamilyDisplayName;
return FlowySettingValueDropDown(
popoverKey: ThemeFontFamilySetting.popoverKey,
popoverController: widget.popoverController,
currentValue: widget.currentFontFamily.parseFontFamilyName(),
currentValue: currentValue,
onClose: () {
query.value = '';
widget.onClose?.call();
@ -168,8 +172,8 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
BuildContext context,
TextStyle style,
) {
final buttonFontFamily = style.fontFamily!.parseFontFamilyName();
final buttonFontFamily =
style.fontFamily?.parseFontFamilyName() ?? defaultFontFamily;
return Tooltip(
message: buttonFontFamily,
waitDuration: const Duration(milliseconds: 150),
@ -179,8 +183,8 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
child: FlowyButton(
onHover: (_) => FocusScope.of(context).unfocus(),
text: FlowyText.medium(
buttonFontFamily,
fontFamily: style.fontFamily!,
buttonFontFamily.fontFamilyDisplayName,
fontFamily: buttonFontFamily,
),
rightIcon:
buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()
@ -190,15 +194,14 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
if (widget.onFontFamilyChanged != null) {
widget.onFontFamilyChanged!(buttonFontFamily);
} else {
final fontFamily = style.fontFamily!.parseFontFamilyName();
if (widget.currentFontFamily.parseFontFamilyName() !=
buttonFontFamily) {
context
.read<AppearanceSettingsCubit>()
.setFontFamily(fontFamily);
.setFontFamily(buttonFontFamily);
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(fontFamily);
.syncFontFamily(buttonFontFamily);
}
}
PopoverContainer.of(context).close();

View File

@ -36,7 +36,7 @@ void main() {
AppTheme.fallback,
),
verify: (bloc) {
expect(bloc.state.font, builtInFontFamily());
expect(bloc.state.font, defaultFontFamily);
expect(bloc.state.monospaceFont, 'SF Mono');
expect(bloc.state.themeMode, ThemeMode.system);
},

View File

@ -27,7 +27,7 @@ void main() {
test('Initial state', () {
expect(cubit.state.fontSize, 16.0);
expect(cubit.state.fontFamily, builtInFontFamily());
expect(cubit.state.fontFamily, defaultFontFamily);
});
test('Fetch document appearance from SharedPreferences', () async {

View File

@ -1,7 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.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/presentation/settings/widgets/settings_appearance/font_family_setting.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';
@ -56,9 +58,9 @@ void main() {
value: documentAppearanceCubit,
),
],
child: Scaffold(
child: const Scaffold(
body: ThemeFontFamilySetting(
currentFontFamily: builtInFontFamily(),
currentFontFamily: defaultFontFamily,
),
),
),
@ -71,7 +73,10 @@ void main() {
await tester.pumpAndSettle();
// Verify the initial font family
expect(find.text(builtInFontFamily()), findsAtLeastNWidgets(1));
expect(
find.text(LocaleKeys.settings_appearance_fontFamily_defaultFont.tr()),
findsAtLeastNWidgets(1),
);
when(() => appearanceSettingsCubit.setFontFamily(any<String>()))
.thenAnswer((_) async {});
verifyNever(() => appearanceSettingsCubit.setFontFamily(any<String>()));

View File

@ -162,7 +162,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"bincode",
@ -739,8 +739,8 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"again",
"anyhow",
@ -786,7 +786,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"futures-channel",
"futures-util",
@ -1025,7 +1025,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"bincode",
@ -1050,7 +1050,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"async-trait",
@ -1407,7 +1407,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",
@ -2777,7 +2777,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"futures-util",
@ -2794,7 +2794,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",
@ -3226,7 +3226,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"reqwest",
@ -5714,7 +5714,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",

View File

@ -47,6 +47,13 @@ collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFl
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" }
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ef8e6f3" }
[dependencies]
serde_json.workspace = true
serde.workspace = true
@ -95,11 +102,3 @@ default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io]
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "47e6f1e8" }

View File

@ -216,7 +216,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"bincode",
@ -547,8 +547,8 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"again",
"anyhow",
@ -594,7 +594,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"futures-channel",
"futures-util",
@ -772,7 +772,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"bincode",
@ -797,7 +797,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"async-trait",
@ -1011,7 +1011,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",
@ -1788,7 +1788,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"futures-util",
@ -1805,7 +1805,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",
@ -2106,7 +2106,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"reqwest",
@ -3732,7 +3732,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",

View File

@ -50,6 +50,13 @@ collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlo
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" }
yrs = "0.18.7"
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ef8e6f3" }
[profile.dev]
@ -61,12 +68,3 @@ codegen-units = 16
lto = true
opt-level = 3
codegen-units = 1
[patch.crates-io]
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "47e6f1e8" }

View File

@ -153,7 +153,7 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"bincode",
@ -713,8 +713,8 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"again",
"anyhow",
@ -760,7 +760,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"futures-channel",
"futures-util",
@ -1008,7 +1008,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"bincode",
@ -1033,7 +1033,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"async-trait",
@ -1394,7 +1394,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",
@ -2851,7 +2851,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"futures-util",
@ -2868,7 +2868,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",
@ -3305,7 +3305,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"reqwest",
@ -5809,7 +5809,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",

View File

@ -47,6 +47,13 @@ collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFl
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" }
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ef8e6f3" }
[dependencies]
serde_json.workspace = true
serde.workspace = true
@ -95,10 +102,3 @@ default = ["custom-protocol"]
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io]
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "47e6f1e8" }

View File

@ -622,14 +622,14 @@
"typeAValue": "Einen Wert eingeben...",
"layout": "Layout",
"databaseLayout": "Layout",
"viewList": "Datenbank-Ansichten",
"editView": "Ansicht editieren",
"boardSettings": "Board-Einstellungen",
"calendarSettings": "Kalender-Einstellungen",
"createView": "New Ansicht",
"duplicateView": "Ansicht duplizieren",
"deleteView": "Anslicht löschen",
"numberOfVisibleFields": "{} angezeigt"
"numberOfVisibleFields": "{} angezeigt",
"viewList": "Datenbank-Ansichten"
},
"textFilter": {
"contains": "Enthält",

View File

@ -42,15 +42,29 @@
"emailHint": "Correo",
"passwordHint": "Contraseña",
"dontHaveAnAccount": "¿No posee credenciales?",
"createAccount": "Crear cuenta",
"repeatPasswordEmptyError": "La contraseña no puede estar en blanco",
"unmatchedPasswordError": "Las contraseñas no coinciden",
"syncPromptMessage": "La sincronización de los datos puede tardar un poco. Por favor no cierres esta página",
"or": "O",
"signInWithGoogle": "Iniciar sesión con Google",
"signInWithGithub": "Iniciar sesión con Github",
"signInWithDiscord": "Iniciar sesión con Discord",
"signUpWithGoogle": "Registrarse con Google",
"signUpWithGithub": "Registrarse con Github",
"signUpWithDiscord": "Registrarse con Discord",
"signInWith": "Inicia sesión con:",
"signInWithEmail": "Iniciar sesión con correo electrónico",
"signInWithMagicLink": "Iniciar sesión con enlace mágico",
"signUpWithMagicLink": "Registrarse con enlace mágico",
"pleaseInputYourEmail": "Por favor, introduzca su dirección de correo electrónico",
"settings": "Configuración",
"magicLinkSent": "Enlace mágico enviado a tu correo electrónico, por favor revisa tu bandeja de entrada",
"invalidEmail": "Por favor, introduce una dirección de correo electrónico válida",
"alreadyHaveAnAccount": "¿Ya tienes cuenta?",
"logIn": "Iniciar sesión",
"generalError": "Algo ha salido mal. Por favor, inténtalo más tarde",
"limitRateError": "Por razones de seguridad, solo puedes solicitar un enlace mágico cada 60 segundos",
"LogInWithGoogle": "Iniciar sesión con Google",
"LogInWithGithub": "Iniciar sesión con Github",
"LogInWithDiscord": "Iniciar sesión con Discord",
@ -78,11 +92,14 @@
"createLimitExceeded": "Has alcanzado el límite máximo de espacio de trabajo permitido para su cuenta. Si necesita espacios de trabajo adicionales para continuar su trabajo, solicítelos en Github",
"deleteSuccess": "Espacio de trabajo eliminado correctamente",
"deleteFailed": "No se pudo eliminar el espacio de trabajo",
"openSuccess": "Espacio de trabajo abierto correctamente",
"openFailed": "No se pudo abrir el espacio de trabajo",
"renameSuccess": "Espacio de trabajo renombrado exitosamente",
"renameFailed": "No se pudo cambiar el nombre del espacio de trabajo",
"updateIconSuccess": "Icono de espacio de trabajo actualizado correctamente",
"updateIconFailed": "Fallo actualizando el icono del espacio de trabajo",
"cannotDeleteTheOnlyWorkspace": "No se puede eliminar el único espacio de trabajo",
"fetchWorkspacesFailed": "No se pudieron recuperar los espacios de trabajo",
"leaveCurrentWorkspace": "Salir del espacio de trabajo",
"leaveCurrentWorkspacePrompt": "¿Está seguro de que desea abandonar el espacio de trabajo actual?"
},
@ -219,6 +236,8 @@
"private": "Privado",
"workspace": "Espacio de trabajo",
"favorites": "Favoritos",
"clickToHidePrivate": "Haz clic para ocultar el espacio privado\nLas páginas que creaste aquí solo son visibles para ti",
"clickToHideWorkspace": "Haga clic para ocultar el espacio de trabajo\nLas páginas que creaste aquí son visibles para todos los miembros",
"clickToHidePersonal": "Haga clic para ocultar la sección personal",
"clickToHideFavorites": "Haga clic para ocultar la sección de favoritos",
"addAPage": "Añadir una página",
@ -240,6 +259,7 @@
},
"button": {
"ok": "OK",
"confirm": "Confirmar",
"done": "Hecho",
"cancel": "Cancelar",
"signIn": "Ingresar",
@ -267,6 +287,7 @@
"helpCenter": "Centro de ayuda",
"add": "Añadir",
"yes": "Si",
"clear": "Limpiar",
"remove": "Eliminar",
"dontRemove": "no quitar",
"copyLink": "Copiar enlace",
@ -301,6 +322,35 @@
},
"settings": {
"title": "Ajustes",
"accountPage": {
"menuLabel": "Mi cuenta",
"title": "Mi cuenta",
"description": "Personaliza tu perfil, administra la seguridad de la cuenta y las claves API de IA, o inicia sesión en tu cuenta.",
"general": {
"title": "Nombre de cuenta e imagen de perfil",
"changeProfilePicture": "Cambiar"
},
"email": {
"title": "Email",
"actions": {
"change": "Cambiar email"
}
},
"keys": {
"title": "Claves API de IA",
"openAILabel": "Clave API de OpenAI",
"openAITooltip": "La clave API de OpenAI para usar en los modelos de IA",
"openAIHint": "Ingresa tu clave API de OpenAI",
"stabilityAILabel": "Clave API de Stability",
"stabilityAITooltip": "La clave API de Stability que se utilizará en los modelos de IA",
"stabilityAIHint": "Ingresa tu clave API de Stability"
},
"login": {
"title": "Inicio de sesión en la cuenta",
"loginLabel": "Inicio de sesión",
"logoutLabel": "Cerrar sesión"
}
},
"menu": {
"appearance": "Apariencia",
"language": "Lenguaje",
@ -322,10 +372,12 @@
"cloudLocal": "Local",
"cloudSupabase": "Supabase",
"cloudSupabaseUrl": "URL de la base de datos",
"cloudSupabaseUrlCanNotBeEmpty": "La URL de supabase no puede estar vacía.",
"cloudSupabaseAnonKey": "Supabase clave anon",
"cloudSupabaseAnonKeyCanNotBeEmpty": "La clave anon no puede estar vacía si la URL de supabase no está vacía",
"cloudAppFlowy": "Nube AppFlowy",
"cloudAppFlowySelfHost": "AppFlowy Cloud autohospedado",
"appFlowyCloudUrlCanNotBeEmpty": "La URL de la nube no puede estar vacía",
"clickToCopy": "Haga clic para copiar",
"selfHostStart": "Si no tiene un servidor, consulte la",
"selfHostContent": "documento",
@ -335,14 +387,20 @@
"cloudWSURLHint": "Ingrese la dirección websocket de su servidor",
"restartApp": "Reiniciar",
"restartAppTip": "Reinicie la aplicación para que se apliquen los cambios. Tenga en cuenta que esto podría cerrar la sesión de su cuenta actual.",
"changeServerTip": "Después de cambiar el servidor, debes hacer clic en el botón reiniciar para que los cambios surtan efecto",
"enableEncryptPrompt": "Activa el cifrado para proteger tus datos con esta clave. Guárdalo de forma segura; una vez habilitado, no se puede desactivar. Si se pierden, tus datos se vuelven irrecuperables. Haz clic para copiar",
"inputEncryptPrompt": "Introduzca su secreto de cifrado para",
"clickToCopySecret": "Haga clic para copiar el código secreto",
"configServerSetting": "Configure los ajustes de su servidor",
"configServerGuide": "Después de seleccionar \"Inicio rápido\", navega hasta \"Configuración\" y luego \"Configuración de la nube\" para configurar tu servidor autoalojado.",
"inputTextFieldHint": "Su código secreto",
"historicalUserList": "Historial de inicio de sesión del usuario",
"historicalUserListTooltip": "Esta lista muestra tus cuentas anónimas. Puedes hacer clic en una cuenta para ver sus detalles. Las cuentas anónimas se crean haciendo clic en el botón \"Comenzar\".",
"openHistoricalUser": "Haga clic para abrir la cuenta anónima",
"customPathPrompt": "Almacenar la carpeta de datos de AppFlowy en una carpeta sincronizada en la nube, como Google Drive, puede presentar riesgos. Si se accede a la base de datos dentro de esta carpeta o se modifica desde varias ubicaciones al mismo tiempo, se pueden producir conflictos de sincronización y posibles daños en los datos",
"importAppFlowyData": "Importar datos desde una carpeta externa de AppFlowy",
"importingAppFlowyDataTip": "La importación de datos está en curso. Por favor no cierres la aplicación.",
"importAppFlowyDataDescription": "Copia los datos de una carpeta de datos externa de AppFlowy e impórtalos a la carpeta de datos actual de AppFlowy",
"importSuccess": "Importó exitosamente la carpeta de datos de AppFlowy",
"importFailed": "Error al importar la carpeta de datos de AppFlowy",
"importGuide": "Para obtener más detalles, consulte el documento de referencia.",
@ -358,7 +416,8 @@
"resetSetting": "restaurar",
"fontFamily": {
"label": "Familia tipográfica",
"search": "Buscar"
"search": "Buscar",
"defaultFont": "Fuente predeterminada"
},
"themeMode": {
"label": "Theme Mode",
@ -366,6 +425,7 @@
"dark": "Modo Oscuro",
"system": "Adapt to System"
},
"fontScaleFactor": "Escala de la fuente",
"documentSettings": {
"cursorColor": "Color del cursor del documento",
"selectionColor": "Color de selección de documento",
@ -386,6 +446,7 @@
},
"textDirection": {
"label": "Dirección de texto predeterminada",
"hint": "Especifica si el texto debe comenzar desde la izquierda o desde la derecha de forma predeterminada.",
"ltr": "LTR (de izquierda hacia derecha)",
"rtl": "RTL (de derecha hacia izquierda)",
"auto": "AUTO",
@ -418,10 +479,37 @@
"twelveHour": "doce horas",
"twentyFourHour": "veinticuatro horas"
},
"showNamingDialogWhenCreatingPage": "Mostrar diálogo de nombres al crear una página",
"enableRTLToolbarItems": "Habilitar elementos de la barra de herramientas RTL",
"members": {
"title": "Configuración de miembros",
"inviteMembers": "Invitar miembros",
"sendInvite": "Enviar invitación",
"user": "Usuario"
"copyInviteLink": "Copiar enlace de invitación",
"label": "Miembros",
"user": "Usuario",
"role": "Rol",
"removeFromWorkspace": "Quitar del espacio de trabajo",
"owner": "Dueño",
"guest": "Invitado",
"member": "Miembro",
"memberHintText": "Un miembro puede leer, comentar y editar páginas. Invitar a miembros e invitados.",
"guestHintText": "Un Invitado puede leer, reaccionar, comentar y editar ciertas páginas con permiso.",
"emailInvalidError": "Email no válido, compruébalo y vuelve a intentarlo.",
"emailSent": "Email enviado, por favor revisa la bandeja de entrada",
"members": "miembros",
"membersCount": {
"zero": "{} miembros",
"one": "{} miembro",
"other": "{} miembros"
},
"memberLimitExceeded": "Has alcanzado el límite máximo de miembros permitidos para tu cuenta. Si deseas agregar más miembros adicionales para continuar con tu trabajo, solicítalo en Github.",
"failedToAddMember": "No se pudo agregar el miembro",
"addMemberSuccess": "Miembro agregado con éxito",
"removeMember": "Eliminar miembro",
"areYouSureToRemoveMember": "¿Estás seguro de que deseas eliminar a este miembro?",
"inviteMemberSuccess": "La invitación ha sido enviada con éxito",
"failedToInviteMember": "No se pudo invitar al miembro"
}
},
"files": {
@ -458,7 +546,11 @@
"recoverLocationTooltips": "Restablecer al directorio de datos predeterminado de AppFlowy",
"exportFileSuccess": "¡Exportar archivo con éxito!",
"exportFileFail": "¡Error en la exportación del archivo!",
"export": "Exportar"
"export": "Exportar",
"clearCache": "Limpiar caché",
"clearCacheDesc": "Si tienes problemas con las imágenes que no cargan o las fuentes no se muestran correctamente, intenta limpiar la caché. Esta acción no eliminará tus datos de usuario.",
"areYouSureToClearCache": "¿Estás seguro de limpiar el caché?",
"clearCacheSuccess": "¡Caché limpiada exitosamente!"
},
"user": {
"name": "Nombre",
@ -472,10 +564,24 @@
"shortcuts": {
"shortcutsLabel": "Atajos",
"command": "Commando",
"keyBinding": "Atajos",
"addNewCommand": "Añadir nuevo comando",
"updateShortcutStep": "Presione la combinación de teclas deseada y presione ENTER",
"shortcutIsAlreadyUsed": "Este atajo ya se utiliza para: {conflict}",
"resetToDefault": "Restablecer los atajos predeterminados",
"couldNotLoadErrorMsg": "No se pudieron cargar los atajos. Inténtalo de nuevo.",
"couldNotSaveErrorMsg": "No se pudieron guardar los atajos. Inténtalo de nuevo."
"couldNotSaveErrorMsg": "No se pudieron guardar los atajos. Inténtalo de nuevo.",
"commands": {
"codeBlockNewParagraph": "Insertar un nuevo párrafo al lado del bloque de código",
"codeBlockIndentLines": "Insertar dos espacios al inicio de la línea en el bloque de código",
"codeBlockOutdentLines": "Eliminar dos espacios al inicio de la línea en el bloque de código",
"codeBlockAddTwoSpaces": "Insertar dos espacios en la posición del cursor en el bloque de código",
"codeBlockSelectAll": "Seleccionar todo el contenido dentro de un bloque de código",
"codeBlockPasteText": "Pegar texto en bloque de código",
"textAlignLeft": "Alinear texto a la izquierda",
"textAlignCenter": "Alinear el texto al centro",
"textAlignRight": "Alinear el texto a la derecha"
}
},
"mobile": {
"personalInfo": "Informacion personal",
@ -489,6 +595,7 @@
"userAgreement": "Acuerdo del Usuario",
"termsAndConditions": "Términos y condiciones",
"userprofileError": "No se pudo cargar el perfil de usuario",
"userprofileErrorDescription": "Intenta cerrar sesión y volver a entrar para comprobar si el problema persiste.",
"selectLayout": "Seleccionar diseño",
"selectStartingDay": "Seleccione el día de inicio",
"version": "Versión"
@ -513,14 +620,15 @@
"typeAValue": "Escriba un valor...",
"layout": "Disposición",
"databaseLayout": "Disposición",
"viewList": "Vistas de base de datos",
"editView": "Editar vista",
"boardSettings": "Configuración del tablero",
"calendarSettings": "Configuración del calendario",
"createView": "Nueva vista",
"duplicateView": "Duplicar vista",
"deleteView": "Eliminar vista",
"Properties": "Propiedades",
"viewList": "Vistas de base de datos"
"numberOfVisibleFields": "{} mostrado",
"Properties": "Propiedades"
},
"textFilter": {
"contains": "Contiene",
@ -566,7 +674,25 @@
"onOrAfter": "Es en o después",
"between": "Está entre",
"empty": "Esta vacio",
"notEmpty": "No está vacío"
"notEmpty": "No está vacío",
"choicechipPrefix": {
"before": "Antes",
"after": "Después",
"onOrBefore": "En o antes de",
"onOrAfter": "En o después de",
"isEmpty": "Está vacio",
"isNotEmpty": "No está vacío"
}
},
"numberFilter": {
"equal": "Es igual",
"notEqual": "No es igual",
"lessThan": "Es menor que",
"greaterThan": "Es mayor que",
"lessThanOrEqualTo": "Es menor o igual que",
"greaterThanOrEqualTo": "Es mayor o igual que",
"isEmpty": "Está vacío",
"isNotEmpty": "No está vacío"
},
"field": {
"hide": "Ocultar",
@ -575,6 +701,8 @@
"insertRight": "Insertar a la Derecha",
"duplicate": "Duplicar",
"delete": "Eliminar",
"wrapCellContent": "Ajustar texto",
"clear": "Borrar celdas",
"textFieldName": "Texto",
"checkboxFieldName": "Casilla de verificación",
"dateFieldName": "Fecha",
@ -585,6 +713,7 @@
"multiSelectFieldName": "Selección múltiple",
"urlFieldName": "URL",
"checklistFieldName": "Lista de Verificación",
"relationFieldName": "Relación",
"numberFormat": "Formato numérico",
"dateFormat": "Formato de fecha",
"includeTime": "Incluir tiempo",
@ -614,18 +743,36 @@
"editProperty": "Editar propiedad",
"newProperty": "Nueva propiedad",
"deleteFieldPromptMessage": "¿Está seguro? Esta propiedad será eliminada",
"clearFieldPromptMessage": "¿Estás seguro? Se vaciarán todas las celdas de esta columna.",
"newColumn": "Nueva columna",
"format": "Formato"
"format": "Formato",
"reminderOnDateTooltip": "Esta celda tiene un recordatorio programado",
"optionAlreadyExist": "La opción ya existe"
},
"rowPage": {
"newField": "Agregar un nuevo campo",
"fieldDragElementTooltip": "Haga clic para abrir el menú"
"fieldDragElementTooltip": "Haga clic para abrir el menú",
"showHiddenFields": {
"one": "Mostrar {count} campo oculto",
"many": "Mostrar {count} campos ocultos",
"other": "Mostrar {count} campos ocultos"
},
"hideHiddenFields": {
"one": "Ocultar {count} campo oculto",
"many": "Ocultar {count} campos ocultos",
"other": "Ocultar {count} campos ocultos"
}
},
"sort": {
"ascending": "ascendente",
"descending": "Descendente",
"by": "Por",
"empty": "Sin ordenamiento activo",
"cannotFindCreatableField": "No se encuentra un campo adecuado para ordenar",
"deleteAllSorts": "Eliminar todos filtros",
"addSort": "Agregar clasificación",
"removeSorting": "¿Le gustaría eliminar la ordenación?",
"fieldInUse": "Ya estás ordenando por este campo",
"deleteSort": "Borrar ordenar"
},
"row": {
@ -673,10 +820,36 @@
},
"url": {
"launch": "Abrir en el navegador",
"copy": "Copiar URL"
"copy": "Copiar URL",
"textFieldHint": "Introduce una URL",
"copiedNotification": "¡Copiado al portapapeles!"
},
"relation": {
"relatedDatabasePlaceLabel": "Base de datos relacionada",
"relatedDatabasePlaceholder": "Ninguno",
"inRelatedDatabase": "En",
"rowSearchTextFieldPlaceholder": "Buscar",
"noDatabaseSelected": "No se seleccionó ninguna base de datos, seleccione una primero de la lista a continuación:",
"emptySearchResult": "No se encontraron registros",
"linkedRowListLabel": "{count} filas vinculadas",
"unlinkedRowListLabel": "Vincular otra fila"
},
"menuName": "Cuadrícula",
"referencedGridPrefix": "Vista de"
"referencedGridPrefix": "Vista de",
"calculate": "Calcular",
"calculationTypeLabel": {
"none": "Ninguno",
"average": "Promedio",
"max": "Max",
"median": "Media",
"min": "Min",
"sum": "Suma",
"count": "Contar",
"countEmpty": "Contar vacío",
"countEmptyShort": "VACÍO",
"countNonEmpty": "Contar no vacíos",
"countNonEmptyShort": "RELLENO"
}
},
"document": {
"menuName": "Documento",
@ -730,10 +903,14 @@
"discardResponse": "¿Quieres descartar las respuestas de IA?",
"createInlineMathEquation": "Crear ecuación",
"fonts": "Tipo de letra",
"insertDate": "Insertar fecha",
"emoji": "Emoji",
"toggleList": "Alternar lista",
"quoteList": "Lista de citas",
"numberedList": "lista numerada",
"bulletedList": "Lista con viñetas",
"todoList": "Lista de tareas",
"callout": "Callout",
"cover": {
"changeCover": "Cubierta de cambio",
"colors": "Colores",
@ -777,17 +954,22 @@
"left": "Izquierda",
"center": "Centro",
"right": "Bien",
"defaultColor": "Por defecto"
"defaultColor": "Por defecto",
"depth": "Profundidad"
},
"image": {
"copiedToPasteBoard": "El enlace de la imagen se ha copiado en el portapapeles.",
"addAnImage": "Añadir una imagen"
"addAnImage": "Añadir una imagen",
"imageUploadFailed": "Error al subir la imagen",
"errorCode": "Código de error"
},
"urlPreview": {
"copiedToPasteBoard": "El enlace ha sido copiado al portapapeles."
"copiedToPasteBoard": "El enlace ha sido copiado al portapapeles.",
"convertToLink": "Convertir en enlace incrustado"
},
"outline": {
"addHeadingToCreateOutline": "Agregue encabezados para crear una tabla de contenido."
"addHeadingToCreateOutline": "Agregue encabezados para crear una tabla de contenido.",
"noMatchHeadings": "No se han encontrado títulos coincidentes."
},
"table": {
"addAfter": "Agregar después",
@ -810,7 +992,11 @@
"toContinue": "continuar",
"newDatabase": "Nueva base de datos",
"linkToDatabase": "Enlace a la base de datos"
}
},
"date": "Fecha"
},
"outlineBlock": {
"placeholder": "Tabla de contenidos"
},
"textBlock": {
"placeholder": "Escriba '/' para comandos"
@ -829,27 +1015,48 @@
"placeholder": "Introduce la URL de la imagen"
},
"ai": {
"label": "Generar imagen desde OpenAI"
"label": "Generar imagen desde OpenAI",
"placeholder": "Ingrese el prompt para que OpenAI genere una imagen"
},
"stability_ai": {
"label": "Generar imagen desde Stability AI",
"placeholder": "Ingrese el prompt para que Stability AI genere una imagen"
},
"support": "El límite de tamaño de la imagen es de 5 MB. Formatos admitidos: JPEG, PNG, GIF, SVG",
"error": {
"invalidImage": "Imagen inválida",
"invalidImageSize": "El tamaño de la imagen debe ser inferior a 5 MB",
"invalidImageFormat": "El formato de imagen no es compatible. Formatos admitidos: JPEG, PNG, GIF, SVG",
"invalidImageUrl": "URL de imagen no válida"
"invalidImageUrl": "URL de imagen no válida",
"noImage": "El fichero o directorio no existe"
},
"embedLink": {
"label": "Insertar enlace",
"placeholder": "Pega o escribe el enlace de una imagen"
},
"unsplash": {
"label": "Desempaquetar"
},
"searchForAnImage": "Buscar una imagen",
"saveImageToGallery": "Guardar imagen"
"pleaseInputYourOpenAIKey": "ingresa tu clave OpenAI en la página de Configuración",
"pleaseInputYourStabilityAIKey": "ingresa tu clave de Stability AI en la página de configuración",
"saveImageToGallery": "Guardar imagen",
"failedToAddImageToGallery": "No se pudo agregar la imagen a la galería",
"successToAddImageToGallery": "Imagen agregada a la galería con éxito",
"unableToLoadImage": "No se puede cargar la imagen",
"maximumImageSize": "El tamaño máximo de imagen es de 10 MB",
"uploadImageErrorImageSizeTooBig": "El tamaño de la imagen debe ser inferior a 10 MB.",
"imageIsUploading": "La imagen se está subiendo"
},
"codeBlock": {
"language": {
"label": "Idioma",
"placeholder": "Seleccione el idioma"
}
"placeholder": "Seleccione el idioma",
"auto": "Auto"
},
"copyTooltip": "Copiar el contenido del bloque de código.",
"searchLanguageHint": "Buscar un idioma",
"codeCopiedSnackbar": "¡Código copiado al portapapeles!"
},
"inlineLink": {
"placeholder": "Pegar o escribir un enlace",
@ -866,13 +1073,25 @@
}
},
"mention": {
"placeholder": "Menciona una persona, una página o fecha...",
"page": {
"label": "Enlace a la página",
"tooltip": "Haga clic para abrir la página"
}
},
"deleted": "Eliminado",
"deletedContent": "Este contenido no existe o ha sido eliminado."
},
"toolbar": {
"resetToDefaultFont": "Restablecer a los predeterminados"
},
"errorBlock": {
"theBlockIsNotSupported": "La versión actual no admite este bloque.",
"blockContentHasBeenCopied": "El contenido del bloque ha sido copiado."
},
"mobilePageSelector": {
"title": "Seleccionar página",
"failedToLoad": "No se pudo cargar la lista de páginas",
"noPagesFound": "No se encontraron páginas"
}
},
"board": {
@ -880,14 +1099,19 @@
"createNewCard": "Nuevo",
"renameGroupTooltip": "Presione para cambiar el nombre del grupo",
"createNewColumn": "Agregar un nuevo grupo",
"addToColumnTopTooltip": "Añade una nueva tarjeta en la parte superior",
"addToColumnBottomTooltip": "Añade una nueva tarjeta en la parte inferior.",
"renameColumn": "Renombrar",
"hideColumn": "Ocultar",
"newGroup": "Nuevo grupo",
"deleteColumn": "Borrar",
"deleteColumnConfirmation": "Esto eliminará este grupo y todas las tarjetas que contiene.\n¿Estás seguro de que quieres continuar?",
"groupActions": "Acciones grupales"
},
"hiddenGroupSection": {
"sectionTitle": "Grupos ocultos"
"sectionTitle": "Grupos ocultos",
"collapseTooltip": "Ocultar los grupos ocultos",
"expandTooltip": "Ver los grupos ocultos"
},
"cardDetail": "Detalle de la tarjeta",
"cardActions": "Acciones de tarjeta",
@ -921,6 +1145,10 @@
"previousMonth": "Mes anterior",
"nextMonth": "Próximo mes"
},
"mobileEventScreen": {
"emptyTitle": "No hay eventos",
"emptyBody": "Presiona el botón más para crear un evento en este día."
},
"settings": {
"showWeekNumbers": "Mostrar números de semana",
"showWeekends": "Mostrar fines de semana",
@ -928,12 +1156,14 @@
"layoutDateField": "Diseño de calendario por",
"changeLayoutDateField": "Cambiar campo de diseño",
"noDateTitle": "Sin cita",
"noDateHint": "Los eventos no programados se mostrarán aquí",
"unscheduledEventsTitle": "Eventos no programados",
"clickToAdd": "Haga clic para agregar al calendario",
"name": "Diseño de calendario",
"noDateHint": "Los eventos no programados se mostrarán aquí"
"name": "Diseño de calendario"
},
"referencedCalendarPrefix": "Vista de",
"quickJumpYear": "Ir a"
"quickJumpYear": "Ir a",
"duplicateEvent": "duplicar evento"
},
"errorDialog": {
"title": "Error de flujo de aplicación",
@ -1003,6 +1233,7 @@
},
"inlineActions": {
"noResults": "No hay resultados",
"recentPages": "Paginas recientes",
"pageReference": "Referencia de página",
"docReference": "Referencia de documento",
"boardReference": "Referencia del tablero",
@ -1020,7 +1251,24 @@
"includeTime": "incluir tiempo",
"isRange": "Fecha final",
"timeFormat": "Formato de tiempo",
"clearDate": "Borrar fecha"
"clearDate": "Borrar fecha",
"reminderLabel": "Recordatorio",
"selectReminder": "Seleccionar recordatorio",
"reminderOptions": {
"none": "Ninguno",
"atTimeOfEvent": "Hora del evento",
"fiveMinsBefore": "5 minutos antes",
"tenMinsBefore": "10 minutos antes",
"fifteenMinsBefore": "15 minutos antes",
"thirtyMinsBefore": "30 minutos antes",
"oneHourBefore": "1 hora antes",
"twoHoursBefore": "2 horas antes",
"onDayOfEvent": "El día del evento",
"oneDayBefore": "1 dia antes",
"twoDaysBefore": "2 dias antes",
"oneWeekBefore": "1 semana antes",
"custom": "Personalizado"
}
},
"relativeDates": {
"yesterday": "Ayer",
@ -1033,6 +1281,7 @@
"mobile": {
"title": "Actualizaciones"
},
"emptyTitle": "¡Todo al día!",
"emptyBody": "No hay notificaciones ni acciones pendientes. Disfruta de la calma.",
"tabs": {
"inbox": "Bandeja de entrada",
@ -1066,14 +1315,18 @@
"replace": "Reemplazar",
"replaceAll": "Reemplaza todo",
"noResult": "No hay resultados",
"caseSensitive": "Distingue mayúsculas y minúsculas"
"caseSensitive": "Distingue mayúsculas y minúsculas",
"searchMore": "Busca para encontrar más resultados"
},
"error": {
"weAreSorry": "Lo lamentamos"
"weAreSorry": "Lo lamentamos",
"loadingViewError": "Estamos teniendo problemas para cargar esta vista. Verifica tu conexión a Internet, actualiza la aplicación y no dudes en comunicarte con el equipo si el problema continúa."
},
"editor": {
"bold": "Negrita",
"bulletedList": "Lista con viñetas",
"bulletedListShortForm": "Con viñetas",
"checkbox": "Checkbox",
"embedCode": "Código de inserción",
"heading1": "H1",
"heading2": "H2",
@ -1081,9 +1334,12 @@
"highlight": "Destacar",
"color": "Color",
"image": "Imagen",
"date": "Fecha",
"page": "Página",
"italic": "Itálico",
"link": "Enlace",
"numberedList": "Lista numerada",
"numberedListShortForm": "Numerado",
"quote": "Cita",
"strikethrough": "Tachado",
"text": "Texto",
@ -1107,6 +1363,184 @@
"backgroundColorBlue": "Fondo azul",
"backgroundColorPurple": "fondo morado",
"backgroundColorPink": "fondo rosa",
"backgroundColorRed": "fondo rojo"
"backgroundColorRed": "fondo rojo",
"backgroundColorLime": "Fondo lima",
"backgroundColorAqua": "Fondo aguamarina",
"done": "Hecho",
"cancel": "Cancelar",
"tint1": "Tono 1",
"tint2": "Tono 2",
"tint3": "Tono 3",
"tint4": "Tono 4",
"tint5": "Tono 5",
"tint6": "Tono 6",
"tint7": "Tono 7",
"tint8": "Tono 8",
"tint9": "Tono 9",
"lightLightTint1": "Morado",
"lightLightTint2": "Rosa",
"lightLightTint3": "Rosa claro",
"lightLightTint4": "Naranja",
"lightLightTint5": "Amarillo",
"lightLightTint6": "Lima",
"lightLightTint7": "Verde",
"lightLightTint8": "Aqua",
"lightLightTint9": "Azul",
"urlHint": "URL",
"mobileHeading1": "Encabezado 1",
"mobileHeading2": "Encabezado 2",
"mobileHeading3": "Encabezado 3",
"textColor": "Color de texto",
"backgroundColor": "Color de fondo",
"addYourLink": "Añadir enlace",
"openLink": "Abrir enlace",
"copyLink": "Copiar enlace",
"removeLink": "Quitar enlace",
"editLink": "Editar enlace",
"linkText": "Texto",
"linkTextHint": "Introduce un texto",
"linkAddressHint": "Introduce una URL",
"highlightColor": "Color de resaltado",
"clearHighlightColor": "Quitar color de resaltado",
"customColor": "Color personalizado",
"hexValue": "Valor Hex",
"opacity": "Transparencia",
"resetToDefaultColor": "Reestablecer color predeterminado",
"ltr": "LTR",
"rtl": "RTL",
"auto": "Auto",
"cut": "Cortar",
"copy": "Copiar",
"paste": "Pegar",
"find": "Buscar",
"select": "Seleccionar",
"selectAll": "Seleccionar todo",
"previousMatch": "Resultado anterior",
"nextMatch": "Siguiente resultado",
"closeFind": "Cerrar",
"replace": "Reemplazar",
"replaceAll": "Reemplazar todo",
"regex": "Expresión regular",
"caseSensitive": "Distingue mayúsculas y minúsculas",
"uploadImage": "Subir imagen",
"urlImage": "URL de la Imagen",
"incorrectLink": "Enlace incorrecto",
"upload": "Subir",
"chooseImage": "Elige una imagen",
"loading": "Cargando",
"imageLoadFailed": "Error al subir la imagen",
"divider": "Divisor",
"table": "Tabla",
"colAddBefore": "Añadir antes",
"rowAddBefore": "Añadir antes",
"colAddAfter": "Añadir después",
"rowAddAfter": "Añadir después",
"colRemove": "Quitar",
"rowRemove": "Quitar",
"colDuplicate": "Duplicar",
"rowDuplicate": "Duplicar",
"colClear": "Borrar contenido",
"rowClear": "Borrar contenido",
"slashPlaceHolder": "Escribe '/' para insertar un bloque o comienza a escribir",
"typeSomething": "Escribe algo...",
"toggleListShortForm": "Alternar",
"quoteListShortForm": "Cita",
"mathEquationShortForm": "Fórmula",
"codeBlockShortForm": "Código"
},
"favorite": {
"noFavorite": "Ninguna página favorita",
"noFavoriteHintText": "Desliza la página hacia la izquierda para agregarla a tus favoritos"
},
"cardDetails": {
"notesPlaceholder": "Escribe una / para insertar un bloque o comienza a escribir"
},
"blockPlaceholders": {
"todoList": "Por hacer",
"bulletList": "Lista",
"numberList": "Lista",
"quote": "Cita",
"heading": "Título {}"
},
"titleBar": {
"pageIcon": "Icono de página",
"language": "Idioma",
"font": "Fuente",
"actions": "Acciones",
"date": "Fecha",
"addField": "Añadir campo",
"userIcon": "Icono de usuario"
},
"noLogFiles": "No hay archivos de registro",
"newSettings": {
"myAccount": {
"title": "Mi cuenta",
"subtitle": "Personaliza tu perfil, administra la seguridad de la cuenta, abre claves IA o inicia sesión en tu cuenta.",
"profileLabel": "Nombre de cuenta e imagen de perfil",
"profileNamePlaceholder": "Introduce tu nombre",
"accountSecurity": "Seguridad de la cuenta",
"2FA": "Autenticación de 2 pasos",
"aiKeys": "Claves IA",
"accountLogin": "Inicio de sesión de la cuenta",
"updateNameError": "No se pudo actualizar el nombre",
"updateIconError": "No se pudo actualizar el ícono",
"deleteAccount": {
"title": "Borrar cuenta",
"subtitle": "Elimina permanentemente tu cuenta y todos tus datos.",
"deleteMyAccount": "Borrar mi cuenta",
"dialogTitle": "Borrar cuenta",
"dialogContent1": "¿Estás seguro de que deseas eliminar permanentemente tu cuenta?",
"dialogContent2": "Esta acción no se puede deshacer y eliminará el acceso a todos los espacios de equipo, borrará toda tu cuenta, incluidos los espacios de trabajo privados, y lo eliminará de todos los espacios de trabajo compartidos."
}
},
"workplace": {
"name": "Espacio de trabajo",
"title": "Configuración del espacio de trabajo",
"subtitle": "Personaliza la apariencia, el tema, la fuente, el diseño del texto, la fecha, la hora y el idioma de tu espacio de trabajo.",
"workplaceName": "Nombre del espacio de trabajo",
"workplaceNamePlaceholder": "Introduce el nombre del espacio de trabajo",
"workplaceIcon": "Icono del espacio de trabajo",
"workplaceIconSubtitle": "Sube una imagen o usa un emoji para tu espacio de trabajo. El icono se mostrará en la barra lateral y en las notificaciones.",
"renameError": "Error al renombrar el espacio de trabajo",
"updateIconError": "Error al actualizar el ícono",
"appearance": {
"name": "Apariencia",
"themeMode": {
"auto": "Auto",
"light": "Claro",
"dark": "Oscuro"
},
"language": "Idioma"
}
},
"syncState": {
"syncing": "Sincronización",
"synced": "Sincronizado",
"noNetworkConnected": "Ninguna red conectada"
}
},
"pageStyle": {
"title": "Estilo de página",
"layout": "Disposición",
"coverImage": "Imagen de portada",
"pageIcon": "Icono de página",
"colors": "Colores",
"gradient": "Degradado",
"backgroundImage": "Imagen de fondo",
"presets": "Preajustes",
"photo": "Foto",
"unsplash": "Desempaquetar",
"pageCover": "Portada de página",
"none": "Ninguno"
},
"commandPalette": {
"placeholder": "Escribe para buscar vistas...",
"bestMatches": "Mejores resultados",
"recentHistory": "Historial reciente",
"navigateHint": "para navegar",
"loadingTooltip": "Buscando resultados...",
"betaLabel": "BETA",
"betaTooltip": "Actualmente solo admitimos la búsqueda de páginas.",
"fromTrashHint": "De la papelera"
}
}

View File

@ -45,10 +45,28 @@
"unmatchedPasswordError": "密碼重複輸入不一致",
"syncPromptMessage": "同步資料可能需要一些時間。請不要關閉此頁面",
"or": "或",
"signInWithGoogle": "使用Google 登入",
"signInWithGithub": "使用Github 登入",
"signInWithDiscord": "使用Discord 登入",
"signUpWithGoogle": "使用Google 註冊",
"signUpWithGithub": "使用Github 註冊",
"signUpWithDiscord": "使用Discord 註冊",
"signInWith": "透過以下方式登入:",
"signInWithEmail": "使用電子郵件登入",
"signInWithMagicLink": "使用Magic Link 登入",
"signUpWithMagicLink": "使用Magic Link 註冊",
"pleaseInputYourEmail": "請輸入您的電郵地址",
"settings": "設定",
"magicLinkSent": "我們己發送Magic Link 到您的電子郵件,點擊連結登入",
"invalidEmail": "請輸入有效的電郵地址",
"alreadyHaveAnAccount": "已經有帳戶?",
"logIn": "登入",
"generalError": "出了些問題。請稍後再試",
"limitRateError": "出於安全原因您只能每60 秒申請一次Magic Link",
"LogInWithGoogle": "使用 Google 登入",
"LogInWithGithub": "使用 Github 登入",
"LogInWithDiscord": "使用 Discord 登入"
"LogInWithDiscord": "使用 Discord 登入",
"loginAsGuestButtonText": "以訪客身分登入"
},
"workspace": {
"chooseWorkspace": "選擇你的工作區",
@ -63,12 +81,30 @@
"reportIssueOnGithub": "在 Github 提交 issue",
"exportLogFiles": "匯出日誌記錄檔案",
"reachOut": "在 Discord 上聯絡我們"
}
},
"menuTitle": "工作區",
"deleteWorkspaceHintText": "您確定要刪除工作區嗎?此操作無法撤銷",
"createSuccess": "成功創建工作區",
"createFailed": "無法創建工作區",
"deleteSuccess": "工作區刪除成功",
"deleteFailed": "工作區刪除失敗",
"openSuccess": "成功開啟工作區",
"openFailed": "無法開啟工作區",
"renameSuccess": "工作區重命名成功",
"renameFailed": "無法重命名工作區",
"updateIconSuccess": "更新工作區圖標成功",
"updateIconFailed": "無法更新工作區圖標",
"cannotDeleteTheOnlyWorkspace": "無法刪除唯一的工作區",
"fetchWorkspacesFailed": "無法取得工作區",
"leaveCurrentWorkspace": "離開工作區",
"leaveCurrentWorkspacePrompt": "您確定要離開當前工作區嗎?"
},
"shareAction": {
"buttonText": "分享",
"workInProgress": "即將推出",
"markdown": "Markdown",
"html": "HTML",
"clipboard": "複製到剪貼簿",
"csv": "CSV",
"copyLink": "複製連結"
},
@ -127,7 +163,8 @@
"emptyDescription": "您沒有任何已刪除的檔案",
"isDeleted": "已刪除",
"isRestored": "已還原"
}
},
"confirmDeleteTitle": "確定永久刪除此頁面"
},
"deletePagePrompt": {
"text": "此頁面在垃圾桶中",
@ -181,16 +218,23 @@
"dragRow": "長按以重新排序列",
"viewDataBase": "檢視資料庫",
"referencePage": "這個 {name} 已被引用",
"addBlockBelow": "在下方新增一個區塊"
"addBlockBelow": "在下方新增一個區塊",
"genSummary": "產成摘要"
},
"sideBar": {
"closeSidebar": "關閉側欄",
"openSidebar": "開啟側欄",
"personal": "個人",
"private": "私人",
"workspace": "工作區",
"favorites": "最愛",
"clickToHidePrivate": "點擊以隱藏私人空間\n您在此處建立的頁面只有您自己可見",
"clickToHideWorkspace": "點擊以隱藏工作區\n您在此處建立的頁面對每個成員都可見",
"clickToHidePersonal": "點選以隱藏個人區塊",
"clickToHideFavorites": "點選以隱藏最愛區塊",
"addAPage": "新增頁面",
"addAPageToPrivate": "新增頁面到私人空間",
"addAPageToWorkspace": "將頁面新增至工作區",
"recent": "最近"
},
"notifications": {
@ -207,6 +251,7 @@
},
"button": {
"ok": "確定",
"confirm": "確認",
"done": "完成",
"cancel": "取消",
"signIn": "登入",
@ -233,7 +278,20 @@
"rename": "重新命名",
"helpCenter": "支援中心",
"add": "新增",
"yes": "是"
"yes": "是",
"clear": "清除",
"remove": "刪除",
"dontRemove": "不要刪除",
"copyLink": "複製連結",
"align": "對齊",
"login": "登入",
"logout": "登出",
"deleteAccount": "刪除帳號",
"back": "返回",
"signInGoogle": "使用Google 登入",
"signInGithub": "使用Github 登入",
"signInDiscord": "使用Discord 登入",
"tryAGain": "再試一次"
},
"label": {
"welcome": "歡迎!",
@ -257,6 +315,35 @@
},
"settings": {
"title": "設定",
"accountPage": {
"menuLabel": "我的帳號",
"title": "我的帳號",
"description": "自訂您的個人資料、管理帳戶安全性和 AI API 金鑰,或登入您的帳號",
"general": {
"title": "帳號名稱和個人資料圖片",
"changeProfilePicture": "更改個人資料圖片"
},
"email": {
"title": "電子郵件",
"actions": {
"change": "更改電子郵件"
}
},
"keys": {
"title": "AI API 金鑰",
"openAILabel": "Open AI API 金鑰",
"openAITooltip": "以OpenAI API 金鑰使用AI 模型",
"openAIHint": "輸入您的 OpenAI API 金鑰",
"stabilityAILabel": "Stability API 金鑰",
"stabilityAITooltip": "以Stability API 金鑰使用AI 模型",
"stabilityAIHint": "輸入您的Stability API 金鑰"
},
"login": {
"title": "帳號登入",
"loginLabel": "登入",
"logoutLabel": "登出"
}
},
"menu": {
"appearance": "外觀",
"language": "語言",
@ -309,7 +396,8 @@
"importAppFlowyDataDescription": "從外部 AppFlowy 資料夾複製資料並匯入到目前的 AppFlowy 資料夾",
"importSuccess": "成功匯入 AppFlowy 資料夾",
"importFailed": "匯入 AppFlowy 資料夾失敗",
"importGuide": "欲瞭解更多詳細資訊,請查閱參考文件"
"importGuide": "欲瞭解更多詳細資訊,請查閱參考文件",
"supabaseSetting": "supabase 設定"
},
"notifications": {
"enableNotifications": {
@ -321,7 +409,8 @@
"resetSetting": "重設",
"fontFamily": {
"label": "字型",
"search": "搜尋"
"search": "搜尋",
"defaultFont": "系統預設"
},
"themeMode": {
"label": "主題模式",
@ -329,6 +418,7 @@
"dark": "深色模式",
"system": "依照系統設定"
},
"fontScaleFactor": "字體比例",
"documentSettings": {
"cursorColor": "文件游標顏色",
"selectionColor": "文件選取顏色",
@ -381,7 +471,31 @@
"twelveHour": "12 小時制",
"twentyFourHour": "24 小時制"
},
"showNamingDialogWhenCreatingPage": "建立頁面時顯示命名對話框"
"showNamingDialogWhenCreatingPage": "建立頁面時顯示命名對話框",
"members": {
"title": "成員設定",
"inviteMembers": "邀請成員",
"sendInvite": "發送邀請",
"copyInviteLink": "複製邀請連結",
"label": "成員",
"user": "使用者",
"role": "身分組",
"removeFromWorkspace": "從工作區中刪除",
"owner": "擁有者",
"guest": "訪客",
"member": "成員",
"memberHintText": "成員可以閱讀、評論和編輯頁面。邀請其他成員和訪客",
"guestHintText": "訪客可以閱讀、做出回應、發表評論,並且可以在獲得許可的情況下編輯頁面",
"emailInvalidError": "電郵無效,請檢查並重試",
"emailSent": "郵件已發送,請查看您的收件匣",
"members": "成員",
"failedToAddMember": "新增成員失敗",
"addMemberSuccess": "成員新增成功",
"removeMember": "刪除成員",
"areYouSureToRemoveMember": "確定要刪除該成員?",
"inviteMemberSuccess": "邀請已成功發送",
"failedToInviteMember": "邀請成員失敗"
}
},
"files": {
"copy": "複製",
@ -417,7 +531,11 @@
"recoverLocationTooltips": "重設為 AppFlowy 的預設資料目錄",
"exportFileSuccess": "匯出檔案成功!",
"exportFileFail": "匯出檔案失敗!",
"export": "匯出"
"export": "匯出",
"clearCache": "清除快取",
"clearCacheDesc": "如果您遇到圖像無法載入或字體無法正確顯示等問題,請嘗試清除快取。此操作不會刪除您的使用者資料",
"areYouSureToClearCache": "確定清除快取?",
"clearCacheSuccess": "快取清除成功"
},
"user": {
"name": "名稱",
@ -437,7 +555,10 @@
"shortcutIsAlreadyUsed": "此快捷鍵已被使用於:{conflict}",
"resetToDefault": "重設為預設鍵盤綁定",
"couldNotLoadErrorMsg": "無法載入快捷鍵,請再試一次",
"couldNotSaveErrorMsg": "無法儲存快捷鍵,請再試一次"
"couldNotSaveErrorMsg": "無法儲存快捷鍵,請再試一次",
"commands": {
"textAlignRight": "向右對齊文字"
}
},
"mobile": {
"personalInfo": "個人資料",
@ -548,6 +669,7 @@
"multiSelectFieldName": "多選",
"urlFieldName": "網址",
"checklistFieldName": "核取清單",
"summaryFieldName": "AI 總結",
"numberFormat": "數字格式",
"dateFormat": "日期格式",
"includeTime": "包含時間",
@ -577,9 +699,11 @@
"editProperty": "編輯屬性",
"newProperty": "新增屬性",
"deleteFieldPromptMessage": "您確定嗎?這個屬性將被刪除",
"clearFieldPromptMessage": "確定操作,該列中的所有單元格都將被清空",
"newColumn": "新增欄位",
"format": "格式",
"reminderOnDateTooltip": "此欄位設有預定提醒"
"reminderOnDateTooltip": "此欄位設有預定提醒",
"optionAlreadyExist": "選項已存在"
},
"rowPage": {
"newField": "新增欄位",
@ -593,7 +717,8 @@
"one": "隱藏 {count} 個隱藏欄位",
"many": "隱藏 {count} 個隱藏欄位",
"other": "隱藏 {count} 個隱藏欄位"
}
},
"openAsFullPage": "以整頁形式打開"
},
"sort": {
"ascending": "升冪",
@ -614,7 +739,8 @@
"drag": "拖曳以移動",
"dragAndClick": "拖曳以移動,點選以開啟選單",
"insertRecordAbove": "在上方插入記錄",
"insertRecordBelow": "在下方插入記錄"
"insertRecordBelow": "在下方插入記錄",
"noContent": "無內容"
},
"selectOption": {
"create": "建立",
@ -646,7 +772,9 @@
},
"url": {
"launch": "在瀏覽器中開啟",
"copy": "複製網址"
"copy": "複製網址",
"textFieldHint": "輸入網址",
"copiedNotification": "已複製到剪貼簿"
},
"menuName": "網格",
"referencedGridPrefix": "檢視",
@ -712,6 +840,7 @@
"discardResponse": "確定捨棄 AI 的回覆?",
"createInlineMathEquation": "建立公式",
"fonts": "字型",
"insertDate": "插入日期",
"emoji": "表情符號",
"toggleList": "切換列表",
"quoteList": "引述列表",
@ -766,13 +895,17 @@
},
"image": {
"copiedToPasteBoard": "圖片連結已複製到剪貼簿",
"addAnImage": "新增圖片"
"addAnImage": "新增圖片",
"imageUploadFailed": "圖片上傳失敗",
"errorCode": "錯誤代碼"
},
"urlPreview": {
"copiedToPasteBoard": "連結已複製到剪貼簿"
"copiedToPasteBoard": "連結已複製到剪貼簿",
"convertToLink": "轉換為嵌入鏈接"
},
"outline": {
"addHeadingToCreateOutline": "新增標題以建立目錄。"
"addHeadingToCreateOutline": "新增標題以建立目錄。",
"noMatchHeadings": "未找到匹配的標題"
},
"table": {
"addAfter": "在後方新增",
@ -798,6 +931,9 @@
},
"date": "日期"
},
"outlineBlock": {
"placeholder": "目錄"
},
"textBlock": {
"placeholder": "輸入“/”作為命令"
},
@ -827,7 +963,8 @@
"invalidImage": "無效的圖片",
"invalidImageSize": "圖片大小必須小於 5MB",
"invalidImageFormat": "不支援的圖片格式。支援的格式JPEG、PNG、GIF、SVG",
"invalidImageUrl": "無效的圖片網址"
"invalidImageUrl": "無效的圖片網址",
"noImage": "沒有該檔案或目錄"
},
"embedLink": {
"label": "嵌入連結",
@ -844,13 +981,18 @@
"successToAddImageToGallery": "圖片已成功新增到相簿",
"unableToLoadImage": "無法載入圖片",
"maximumImageSize": "支援的最大上傳圖片大小為 10MB",
"uploadImageErrorImageSizeTooBig": "圖片大小必須小於 10MB"
"uploadImageErrorImageSizeTooBig": "圖片大小必須小於 10MB",
"imageIsUploading": "圖片上傳中"
},
"codeBlock": {
"language": {
"label": "語言",
"placeholder": "選擇語言"
}
"placeholder": "選擇語言",
"auto": "自動"
},
"copyTooltip": "複製區塊的內容",
"searchLanguageHint": "搜尋語言",
"codeCopiedSnackbar": "程式碼已複製到剪貼簿"
},
"inlineLink": {
"placeholder": "貼上或輸入連結",
@ -881,6 +1023,11 @@
"errorBlock": {
"theBlockIsNotSupported": "目前版本不支援此區塊。",
"blockContentHasBeenCopied": "區塊內容已被複製。"
},
"mobilePageSelector": {
"title": "選擇頁面",
"failedToLoad": "載入頁面清單失敗",
"noPagesFound": "沒有找到該頁面"
}
},
"board": {
@ -1025,6 +1172,7 @@
},
"inlineActions": {
"noResults": "無結果",
"recentPages": "最近的頁面",
"pageReference": "頁面參照",
"docReference": "文件參照",
"boardReference": "看板參照",
@ -1106,7 +1254,8 @@
"replace": "取代",
"replaceAll": "全部取代",
"noResult": "無結果",
"caseSensitive": "區分大小寫"
"caseSensitive": "區分大小寫",
"searchMore": "搜尋以查找更多結果"
},
"error": {
"weAreSorry": "我們很抱歉",
@ -1125,6 +1274,7 @@
"color": "顏色",
"image": "圖片",
"date": "日期",
"page": "頁面",
"italic": "斜體",
"link": "連結",
"numberedList": "編號清單",
@ -1200,6 +1350,8 @@
"copy": "複製",
"paste": "貼上",
"find": "尋找",
"select": "選取",
"selectAll": "選取所有",
"previousMatch": "上一個符合",
"nextMatch": "下一個符合",
"closeFind": "關閉",
@ -1256,5 +1408,75 @@
"addField": "新增欄位",
"userIcon": "使用者圖示"
},
"noLogFiles": "這裡沒有日誌記錄檔案"
"noLogFiles": "這裡沒有日誌記錄檔案",
"newSettings": {
"myAccount": {
"title": "我的帳戶",
"subtitle": "自訂您的個人資料、管理帳戶安全性、Open AI 金鑰或登入您的帳戶",
"profileLabel": "帳號名稱和個人資料圖片",
"profileNamePlaceholder": "輸入你的名字",
"accountSecurity": "帳戶安全性",
"2FA": "兩步驟驗證",
"accountLogin": "帳號登入",
"updateNameError": "名稱更新失敗",
"updateIconError": "個人頭像更新失敗",
"deleteAccount": {
"title": "刪除帳號",
"subtitle": "永久刪除您的帳號和所有資料",
"deleteMyAccount": "刪除我的帳號",
"dialogTitle": "刪除帳號",
"dialogContent1": "確定要永久刪除您的帳號",
"dialogContent2": "此操作無法撤銷,此操作將刪除所有團隊空間的存取權限,刪除您的整個帳戶(包括私人工作區),並將您從所有共用工作區中刪除"
}
},
"workplace": {
"name": "工作區",
"title": "工作區設定",
"subtitle": "自訂您的工作區外觀、主題、字體、文字佈局、日期、時間和語言",
"workplaceName": "工作區名稱",
"workplaceNamePlaceholder": "輸入工作區名稱",
"workplaceIcon": "工作區圖標",
"workplaceIconSubtitle": "為您的工作區上傳圖像或表情符號。圖示將顯示在您的側邊欄和通知中",
"renameError": "工作區重新命名失敗",
"updateIconError": "更新圖像失敗",
"appearance": {
"name": "外觀",
"themeMode": {
"auto": "自動",
"light": "亮白",
"dark": "黑暗"
},
"language": "語言"
}
},
"syncState": {
"syncing": "同步中",
"synced": "已同步",
"noNetworkConnected": "沒有連線網絡"
}
},
"pageStyle": {
"title": "頁面樣式",
"layout": "佈局",
"coverImage": "封面圖片",
"pageIcon": "頁面圖片",
"colors": "顏色",
"gradient": "漸變",
"backgroundImage": "背景圖片",
"presets": "預設",
"photo": "圖片",
"pageCover": "封面",
"none": "無",
"photoPermissionDescription": "允許存取圖片庫以上傳圖片",
"openSettings": "打開設定",
"photoPermissionTitle": "AppFlowy 希望存取您的圖片庫",
"doNotAllow": "不允許"
},
"commandPalette": {
"bestMatches": "最佳匹配",
"recentHistory": "最近歷史",
"loadingTooltip": "我們正在尋找結果...",
"betaLabel": "BETA",
"betaTooltip": "目前我們只支援搜尋頁面"
}
}

View File

@ -163,7 +163,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-cloud-billing-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client?rev=b141b6eefa0e46d80d5d0f59a8c9c51c0dfdbb78#b141b6eefa0e46d80d5d0f59a8c9c51c0dfdbb78"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client?rev=196d8ccbc6a63604f1ac56c66e772efd365818aa#196d8ccbc6a63604f1ac56c66e772efd365818aa"
dependencies = [
"client-api",
"reqwest",
@ -662,8 +662,8 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"again",
"anyhow",
@ -709,7 +709,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"futures-channel",
"futures-util",
@ -917,7 +917,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"bincode",
@ -942,7 +942,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"async-trait",
@ -1261,7 +1261,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",
@ -2475,7 +2475,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"futures-util",
@ -2492,7 +2492,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",
@ -2857,7 +2857,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"reqwest",
@ -4954,7 +4954,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc"
dependencies = [
"anyhow",
"app-error",

View File

@ -94,6 +94,14 @@ collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlo
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" }
yrs = "0.18.7"
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ef8e6f3" }
appflowy-cloud-billing-client = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client", rev = "196d8ccbc6a63604f1ac56c66e772efd365818aa" }
[profile.dev]
opt-level = 1
lto = false
@ -122,11 +130,3 @@ incremental = false
# TODO(Lucas.Xu) Upgrade to the latest version of RocksDB once PR(https://github.com/rust-rocksdb/rust-rocksdb/pull/869) is merged.
# Currently, using the following revision id. This commit is patched to fix the 32-bit build issue and it's checked out from 0.21.0, not 0.22.0.
rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec131b9d82dc94e178fe8efc0c147b09" }
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "47e6f1e8" }
appflowy-cloud-billing-client = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client", rev = "b141b6eefa0e46d80d5d0f59a8c9c51c0dfdbb78" }

View File

@ -30,7 +30,7 @@ collab = { workspace = true }
diesel.workspace = true
uuid.workspace = true
flowy-storage = { workspace = true }
client-api = { version = "0.1.0" }
client-api.workspace = true
tracing.workspace = true
futures-core = { version = "0.3", default-features = false }

View File

@ -31,7 +31,7 @@ collab-database = { workspace = true, optional = true }
collab-document = { workspace = true, optional = true }
collab-plugins = { workspace = true, optional = true }
collab-folder = { workspace = true, optional = true }
client-api = { version = "0.1.0", optional = true }
client-api = { workspace = true, optional = true }
tantivy = { version = "0.21.1", optional = true }

View File

@ -37,7 +37,7 @@ flowy-user-pub = { workspace = true }
flowy-folder-pub = { workspace = true }
flowy-database-pub = { workspace = true }
flowy-document-pub = { workspace = true }
appflowy-cloud-billing-client = "0.1.0"
appflowy-cloud-billing-client = { workspace = true }
flowy-error = { workspace = true, features = ["impl_from_serde", "impl_from_reqwest", "impl_from_url", "impl_from_appflowy_cloud"] }
flowy-server-pub = { workspace = true }
flowy-encrypt = { workspace = true }
@ -51,7 +51,7 @@ yrs.workspace = true
rand = "0.8.5"
[dependencies.client-api]
version = "0.1.0"
workspace = true
features = [
"collab-sync",
"test_util",

View File

@ -12,8 +12,10 @@ use client_api::ws::{
use client_api::{Client, ClientConfiguration};
use flowy_storage::ObjectStorageService;
use rand::Rng;
use tokio::sync::watch;
use tokio::select;
use tokio::sync::{watch, Mutex};
use tokio_stream::wrappers::WatchStream;
use tokio_util::sync::CancellationToken;
use tracing::{error, event, info, warn};
use uuid::Uuid;
@ -74,11 +76,22 @@ impl AppFlowyCloudServer {
let enable_sync = Arc::new(AtomicBool::new(enable_sync));
let network_reachable = Arc::new(AtomicBool::new(true));
let ws_client = WSClient::new(WSClientConfig::default(), api_client.clone());
let ws_client = WSClient::new(
WSClientConfig::default(),
api_client.clone(),
api_client.clone(),
);
let ws_client = Arc::new(ws_client);
let api_client = Arc::new(api_client);
let ws_connect_cancellation_token = Arc::new(Mutex::new(CancellationToken::new()));
spawn_ws_conn(token_state_rx, &ws_client, &api_client, &enable_sync);
spawn_ws_conn(
token_state_rx,
&ws_client,
ws_connect_cancellation_token,
&api_client,
&enable_sync,
);
Self {
config,
client: api_client,
@ -241,12 +254,14 @@ impl AppFlowyServer for AppFlowyCloudServer {
fn spawn_ws_conn(
mut token_state_rx: TokenStateReceiver,
ws_client: &Arc<WSClient>,
conn_cancellation_token: Arc<Mutex<CancellationToken>>,
api_client: &Arc<Client>,
enable_sync: &Arc<AtomicBool>,
) {
let weak_ws_client = Arc::downgrade(ws_client);
let weak_api_client = Arc::downgrade(api_client);
let enable_sync = enable_sync.clone();
let cloned_conn_cancellation_token = conn_cancellation_token.clone();
af_spawn(async move {
if let Some(ws_client) = weak_ws_client.upgrade() {
@ -256,15 +271,16 @@ fn spawn_ws_conn(
match state {
ConnectState::PingTimeout | ConnectState::Lost => {
// Try to reconnect if the connection is timed out.
if let Some(api_client) = weak_api_client.upgrade() {
if enable_sync.load(Ordering::SeqCst) {
attempt_reconnect(&ws_client, &api_client, 2).await;
}
if weak_api_client.upgrade().is_some() && enable_sync.load(Ordering::SeqCst) {
attempt_reconnect(&ws_client, 2, &cloned_conn_cancellation_token).await;
}
},
ConnectState::Unauthorized => {
if let Some(api_client) = weak_api_client.upgrade() {
if let Err(err) = api_client.refresh_token().await {
if let Err(err) = api_client
.refresh_token("websocket connect unauthorized")
.await
{
error!("Failed to refresh token: {}", err);
}
}
@ -276,21 +292,13 @@ fn spawn_ws_conn(
});
let weak_ws_client = Arc::downgrade(ws_client);
let weak_api_client = Arc::downgrade(api_client);
af_spawn(async move {
while let Ok(token_state) = token_state_rx.recv().await {
info!("🟢token state: {:?}", token_state);
match token_state {
TokenState::Refresh => {
if let (Some(api_client), Some(ws_client)) =
(weak_api_client.upgrade(), weak_ws_client.upgrade())
{
match api_client.ws_connect_info().await {
Ok(conn_info) => {
let _ = ws_client.connect(api_client.ws_addr(), conn_info).await;
},
Err(err) => error!("Failed to get ws url: {}", err),
}
if let Some(ws_client) = weak_ws_client.upgrade() {
attempt_reconnect(&ws_client, 5, &conn_cancellation_token).await;
}
},
TokenState::Invalid => {
@ -304,26 +312,43 @@ fn spawn_ws_conn(
});
}
/// Attempts to reconnect a WebSocket client with a randomized delay to mitigate the thundering herd problem.
///
/// This function cancels any existing reconnection attempt, sets up a new cancellation token, and then
/// attempts to reconnect after a randomized delay. The delay is set between a specified minimum and
/// that minimum plus 10 seconds.
///
async fn attempt_reconnect(
ws_client: &Arc<WSClient>,
api_client: &Arc<Client>,
minimum_delay: u64,
minimum_delay_in_secs: u64,
conn_cancellation_token: &Arc<Mutex<CancellationToken>>,
) {
// Introduce randomness in the reconnection attempts to avoid thundering herd problem
let delay_seconds = rand::thread_rng().gen_range(minimum_delay..8);
tokio::time::sleep(Duration::from_secs(delay_seconds)).await;
event!(
tracing::Level::INFO,
"🟢 Attempting to reconnect websocket."
);
match api_client.ws_connect_info().await {
Ok(conn_info) => {
if let Err(e) = ws_client.connect(api_client.ws_addr(), conn_info).await {
error!("Failed to reconnect websocket: {}", e);
// Cancel the previous reconnection attempt
let mut cancel_token_lock = conn_cancellation_token.lock().await;
cancel_token_lock.cancel();
let new_cancel_token = CancellationToken::new();
*cancel_token_lock = new_cancel_token.clone();
drop(cancel_token_lock);
// randomness in the reconnection attempts to avoid thundering herd problem
let delay_seconds = rand::thread_rng().gen_range(minimum_delay_in_secs..10);
let ws_client = ws_client.clone();
tokio::spawn(async move {
select! {
_ = new_cancel_token.cancelled() => {
event!(
tracing::Level::TRACE,
"🟢websocket reconnection attempt cancelled."
);
},
_ = tokio::time::sleep(Duration::from_secs(delay_seconds)) => {
if let Err(e) = ws_client.connect().await {
error!("Failed to reconnect websocket: {}", e);
}
}
},
Err(err) => error!("Failed to get websocket URL: {}", err),
}
}
});
}
pub trait AFServer: Send + Sync + 'static {

View File

@ -128,7 +128,7 @@ pub struct DocumentSettingsPB {
}
pub const APPEARANCE_DEFAULT_THEME: &str = "Default";
pub const APPEARANCE_DEFAULT_FONT: &str = "Poppins";
pub const APPEARANCE_DEFAULT_FONT: &str = ""; // Use system default font
pub const APPEARANCE_DEFAULT_MONOSPACE_FONT: &str = "SF Mono";
const APPEARANCE_RESET_AS_DEFAULT: bool = true;
const APPEARANCE_DEFAULT_IS_MENU_COLLAPSED: bool = false;