mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: merge with main
This commit is contained in:
commit
6fde759061
17
.github/workflows/ios_ci.yaml
vendored
17
.github/workflows/ios_ci.yaml
vendored
@ -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 }}
|
||||
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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)}');
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 ||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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>()));
|
||||
|
22
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
22
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
22
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
22
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
22
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
22
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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": "目前我們只支援搜尋頁面"
|
||||
}
|
||||
}
|
24
frontend/rust-lib/Cargo.lock
generated
24
frontend/rust-lib/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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 }
|
||||
|
@ -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 }
|
||||
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user