mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-11 19:17:43 +03:00
refactor: mobile view page (#5288)
* refactor: mobile view page * fix: provider not found * chore: add page style integration tests * fix: android title bar padding * fix: unable to click mentioned page on Android * fix: font family not available log
This commit is contained in:
parent
dbbdc13d96
commit
6edb184bfb
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 }}
|
||||
|
@ -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();
|
||||
}
|
@ -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,55 +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,
|
||||
30.0,
|
||||
iconPadding: 6.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: actions,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
_rebuildScrollNotificationObserver(context);
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: FlowyAppBar(
|
||||
title: title,
|
||||
actions: actions,
|
||||
),
|
||||
body: child,
|
||||
);
|
||||
}
|
||||
|
||||
void _rebuildScrollNotificationObserver(BuildContext context) {
|
||||
@ -251,163 +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, 30.0),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBarMoreButton(ViewPB view) {
|
||||
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: (_) => _buildAppBarMoreBottomSheet(context),
|
||||
);
|
||||
},
|
||||
child: _buildImmersiveAppBarIcon(FlowySvgs.m_app_bar_more_s, 30.0),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImmersiveAppBarIcon(
|
||||
FlowySvgData icon,
|
||||
double dimension, {
|
||||
double iconPadding = 5.0,
|
||||
}) {
|
||||
assert(
|
||||
dimension > 0.0 && dimension <= kToolbarHeight,
|
||||
'dimension must be greater than 0, and less than or equal to kToolbarHeight',
|
||||
);
|
||||
return UnconstrainedBox(
|
||||
child: SizedBox.square(
|
||||
dimension: dimension,
|
||||
child: 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: 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;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -418,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 ||
|
||||
@ -428,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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -95,6 +95,8 @@ class EditorStyleCustomizer {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -213,7 +214,7 @@ class EditorStyleCustomizer {
|
||||
);
|
||||
|
||||
TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) {
|
||||
if (fontFamily == null) {
|
||||
if (fontFamily == null || fontFamily == defaultFontFamily) {
|
||||
return TextStyle(fontWeight: fontWeight);
|
||||
}
|
||||
try {
|
||||
|
@ -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 ||
|
||||
|
Loading…
Reference in New Issue
Block a user