feat: immersive page style on mobile (#5135)

This commit is contained in:
Lucas.Xu 2024-04-30 16:55:15 +08:00 committed by GitHub
parent 6d0598b101
commit 33802fa62d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 3643 additions and 348 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

View File

@ -1,9 +1,6 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
@ -17,6 +14,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embe
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
@ -66,7 +65,7 @@ class EditorOperations {
Future<void> tapGettingStartedIcon() async {
await tester.tapButton(
find.descendant(
of: find.byType(DocumentHeaderNodeWidget),
of: find.byType(DocumentCoverWidget),
matching: find.findTextInFlowyText('⭐️'),
),
);

View File

@ -64,8 +64,6 @@ PODS:
- Flutter
- FlutterMacOS
- ReachabilitySwift (5.0.0)
- rich_clipboard_ios (0.0.1):
- Flutter
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2)
@ -100,7 +98,6 @@ DEPENDENCIES:
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@ -147,8 +144,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
rich_clipboard_ios:
:path: ".symlinks/plugins/rich_clipboard_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@ -164,10 +159,10 @@ SPEC CHECKSUMS:
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
@ -176,10 +171,9 @@ SPEC CHECKSUMS:
integration_test: 13825b8a9334a850581300559b8839134b124670
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
rich_clipboard_ios: 7588abe18f881a6d0e9ec0b12e51cae2761e8942
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695

View File

@ -0,0 +1,432 @@
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';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_page_style_bloc.freezed.dart';
class DocumentPageStyleBloc
extends Bloc<DocumentPageStyleEvent, DocumentPageStyleState> {
DocumentPageStyleBloc({
required this.view,
}) : super(DocumentPageStyleState.initial()) {
on<DocumentPageStyleEvent>(
(event, emit) async {
await event.when(
initial: () async {
try {
final layoutObject =
await ViewBackendService.getView(view.id).fold(
(s) => jsonDecode(s.extra),
(f) => {},
);
final fontLayout = _getSelectedFontLayout(layoutObject);
final lineHeightLayout = _getSelectedLineHeightLayout(
layoutObject,
);
final fontFamily = _getSelectedFontFamily(layoutObject);
final cover = _getSelectedCover(layoutObject);
final coverType = cover.$1;
final coverValue = cover.$2;
emit(
state.copyWith(
fontLayout: fontLayout,
fontFamily: fontFamily,
lineHeightLayout: lineHeightLayout,
coverImage: PageStyleCover(
type: coverType,
value: coverValue,
),
iconPadding: calculateIconPadding(
fontLayout,
lineHeightLayout,
),
),
);
} catch (e) {
Log.error('Failed to decode layout object: $e');
}
},
updateFont: (fontLayout) async {
emit(
state.copyWith(
fontLayout: fontLayout,
iconPadding: calculateIconPadding(
fontLayout,
state.lineHeightLayout,
),
),
);
unawaited(updateLayoutObject());
},
updateLineHeight: (lineHeightLayout) async {
emit(
state.copyWith(
lineHeightLayout: lineHeightLayout,
iconPadding: calculateIconPadding(
state.fontLayout,
lineHeightLayout,
),
),
);
unawaited(updateLayoutObject());
},
updateFontFamily: (fontFamily) async {
emit(
state.copyWith(
fontFamily: fontFamily,
),
);
unawaited(updateLayoutObject());
},
updateCoverImage: (coverImage) async {
emit(
state.copyWith(
coverImage: coverImage,
),
);
unawaited(updateLayoutObject());
},
);
},
);
}
final ViewPB view;
final ViewBackendService viewBackendService = ViewBackendService();
Future<void> updateLayoutObject() async {
final layoutObject = decodeLayoutObject();
if (layoutObject != null) {
await ViewBackendService.updateView(
viewId: view.id,
extra: layoutObject,
);
}
}
String? decodeLayoutObject() {
Map oldValue = {};
try {
final extra = view.extra;
oldValue = jsonDecode(extra);
} catch (e) {
Log.error('Failed to decode layout object: $e');
}
final newValue = {
ViewExtKeys.fontLayoutKey: state.fontLayout.toString(),
ViewExtKeys.lineHeightLayoutKey: state.lineHeightLayout.toString(),
ViewExtKeys.coverKey: {
ViewExtKeys.coverTypeKey: state.coverImage.type.toString(),
ViewExtKeys.coverValueKey: state.coverImage.value,
},
ViewExtKeys.fontKey: state.fontFamily,
};
final merged = mergeMaps(oldValue, newValue);
return jsonEncode(merged);
}
// because the line height can not be calculated accurately,
// we need to adjust the icon padding manually.
double calculateIconPadding(
PageStyleFontLayout fontLayout,
PageStyleLineHeightLayout lineHeightLayout,
) {
double padding = switch (fontLayout) {
PageStyleFontLayout.small => 1.0,
PageStyleFontLayout.normal => 2.0,
PageStyleFontLayout.large => 4.0,
};
switch (lineHeightLayout) {
case PageStyleLineHeightLayout.small:
padding -= 1.0;
break;
case PageStyleLineHeightLayout.normal:
break;
case PageStyleLineHeightLayout.large:
padding += 3.0;
break;
}
return max(0, padding);
}
PageStyleFontLayout _getSelectedFontLayout(Map layoutObject) {
final fontLayout = layoutObject[ViewExtKeys.fontLayoutKey] ??
PageStyleFontLayout.normal.toString();
return PageStyleFontLayout.values.firstWhere(
(e) => e.toString() == fontLayout,
);
}
PageStyleLineHeightLayout _getSelectedLineHeightLayout(Map layoutObject) {
final lineHeightLayout = layoutObject[ViewExtKeys.lineHeightLayoutKey] ??
PageStyleLineHeightLayout.normal.toString();
return PageStyleLineHeightLayout.values.firstWhere(
(e) => e.toString() == lineHeightLayout,
);
}
String _getSelectedFontFamily(Map layoutObject) {
return layoutObject[ViewExtKeys.fontKey] ?? builtInFontFamily();
}
(PageStyleCoverImageType, String colorValue) _getSelectedCover(
Map layoutObject,
) {
final cover = layoutObject[ViewExtKeys.coverKey] ?? {};
final coverType = cover[ViewExtKeys.coverTypeKey] ??
PageStyleCoverImageType.none.toString();
final coverValue = cover[ViewExtKeys.coverValueKey] ?? '';
return (
PageStyleCoverImageType.values.firstWhere(
(e) => e.toString() == coverType,
),
coverValue,
);
}
}
@freezed
class DocumentPageStyleEvent with _$DocumentPageStyleEvent {
const factory DocumentPageStyleEvent.initial() = Initial;
const factory DocumentPageStyleEvent.updateFont(
PageStyleFontLayout fontLayout,
) = UpdateFontSize;
const factory DocumentPageStyleEvent.updateLineHeight(
PageStyleLineHeightLayout lineHeightLayout,
) = UpdateLineHeight;
const factory DocumentPageStyleEvent.updateFontFamily(
String? fontFamily,
) = UpdateFontFamily;
const factory DocumentPageStyleEvent.updateCoverImage(
PageStyleCover coverImage,
) = UpdateCoverImage;
}
@freezed
class DocumentPageStyleState with _$DocumentPageStyleState {
const factory DocumentPageStyleState({
@Default(PageStyleFontLayout.normal) PageStyleFontLayout fontLayout,
@Default(PageStyleLineHeightLayout.normal)
PageStyleLineHeightLayout lineHeightLayout,
// the default font family is null, which means the system font
@Default(null) String? fontFamily,
@Default(2.0) double iconPadding,
required PageStyleCover coverImage,
}) = _DocumentPageStyleState;
factory DocumentPageStyleState.initial() => DocumentPageStyleState(
coverImage: PageStyleCover.none(),
);
}
enum PageStyleFontLayout {
small,
normal,
large;
@override
String toString() {
switch (this) {
case PageStyleFontLayout.small:
return 'small';
case PageStyleFontLayout.normal:
return 'normal';
case PageStyleFontLayout.large:
return 'large';
}
}
static PageStyleFontLayout fromString(String value) {
return PageStyleFontLayout.values.firstWhereOrNull(
(e) => e.toString() == value,
) ??
PageStyleFontLayout.normal;
}
double get fontSize {
switch (this) {
case PageStyleFontLayout.small:
return 14.0;
case PageStyleFontLayout.normal:
return 16.0;
case PageStyleFontLayout.large:
return 18.0;
}
}
List<double> get headingFontSizes {
switch (this) {
case PageStyleFontLayout.small:
return [22.0, 18.0, 16.0, 16.0, 16.0, 16.0];
case PageStyleFontLayout.normal:
return [24.0, 20.0, 18.0, 18.0, 18.0, 18.0];
case PageStyleFontLayout.large:
return [26.0, 22.0, 20.0, 20.0, 20.0, 20.0];
}
}
double get factor {
switch (this) {
case PageStyleFontLayout.small:
return PageStyleFontLayout.small.fontSize /
PageStyleFontLayout.normal.fontSize;
case PageStyleFontLayout.normal:
return 1.0;
case PageStyleFontLayout.large:
return PageStyleFontLayout.large.fontSize /
PageStyleFontLayout.normal.fontSize;
}
}
}
enum PageStyleLineHeightLayout {
small,
normal,
large;
@override
String toString() {
switch (this) {
case PageStyleLineHeightLayout.small:
return 'small';
case PageStyleLineHeightLayout.normal:
return 'normal';
case PageStyleLineHeightLayout.large:
return 'large';
}
}
static PageStyleLineHeightLayout fromString(String value) {
return PageStyleLineHeightLayout.values.firstWhereOrNull(
(e) => e.toString() == value,
) ??
PageStyleLineHeightLayout.normal;
}
double get lineHeight {
switch (this) {
case PageStyleLineHeightLayout.small:
return 1.4;
case PageStyleLineHeightLayout.normal:
return 1.5;
case PageStyleLineHeightLayout.large:
return 1.75;
}
}
double get padding {
switch (this) {
case PageStyleLineHeightLayout.small:
return 6.0;
case PageStyleLineHeightLayout.normal:
return 8.0;
case PageStyleLineHeightLayout.large:
return 8.0;
}
}
List<double> get headingPaddings {
switch (this) {
case PageStyleLineHeightLayout.small:
return [26.0, 22.0, 20.0, 20.0, 20.0, 20.0];
case PageStyleLineHeightLayout.normal:
return [30.0, 24.0, 22.0, 22.0, 22.0, 22.0];
case PageStyleLineHeightLayout.large:
return [34.0, 28.0, 26.0, 26.0, 26.0, 26.0];
}
}
}
// for the version above 0.5.5
enum PageStyleCoverImageType {
none,
// normal color
pureColor,
// gradient color
gradientColor,
// built in images
builtInImage,
// custom images, uploaded by the user
customImage,
// local image
localImage,
// unsplash images
unsplashImage;
@override
String toString() {
switch (this) {
case PageStyleCoverImageType.none:
return 'none';
case PageStyleCoverImageType.pureColor:
return 'color';
case PageStyleCoverImageType.gradientColor:
return 'gradient';
case PageStyleCoverImageType.builtInImage:
return 'built_in';
case PageStyleCoverImageType.customImage:
return 'custom';
case PageStyleCoverImageType.localImage:
return 'local';
case PageStyleCoverImageType.unsplashImage:
return 'unsplash';
}
}
static PageStyleCoverImageType fromString(String value) {
return PageStyleCoverImageType.values.firstWhereOrNull(
(e) => e.toString() == value,
) ??
PageStyleCoverImageType.none;
}
static String builtInImagePath(String value) {
return 'assets/images/built_in_cover_images/m_cover_image_$value.png';
}
}
class PageStyleCover {
const PageStyleCover({
required this.type,
required this.value,
});
factory PageStyleCover.none() => const PageStyleCover(
type: PageStyleCoverImageType.none,
value: '',
);
final PageStyleCoverImageType type;
// there're 4 types of values:
// 1. pure color: enum value
// 2. gradient color: enum value
// 3. built-in image: the image name, read from the assets
// 4. custom image or unsplash image: the image url
final String value;
bool get isPresets => isPureColor || isGradient || isBuiltInImage;
bool get isPhoto => isCustomImage || isLocalImage;
bool get isNone => type == PageStyleCoverImageType.none;
bool get isPureColor => type == PageStyleCoverImageType.pureColor;
bool get isGradient => type == PageStyleCoverImageType.gradientColor;
bool get isBuiltInImage => type == PageStyleCoverImageType.builtInImage;
bool get isCustomImage => type == PageStyleCoverImageType.customImage;
bool get isUnsplashImage => type == PageStyleCoverImageType.unsplashImage;
bool get isLocalImage => type == PageStyleCoverImageType.localImage;
}

View File

@ -1,8 +1,10 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/document_listener.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -21,10 +23,14 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
initial: () async {
_documentListener.start(
onDocEventUpdate: (docEvent) async {
final (coverType, coverValue) = await getCover();
if (state.coverTypeV2 != null) {
return;
}
final (coverType, coverValue) = await getCoverV1();
add(
RecentViewEvent.updateCover(
coverType,
null,
coverValue,
),
);
@ -38,17 +44,40 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
view.icon.value,
),
);
if (view.extra.isNotEmpty) {
final cover = view.cover;
add(
RecentViewEvent.updateCover(
CoverType.none,
cover?.type,
cover?.value,
),
);
}
},
);
final (coverType, coverValue) = await getCover();
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
coverType: coverType,
coverValue: coverValue,
),
);
final cover = getCoverV2();
if (cover != null) {
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
coverTypeV2: cover.type,
coverValue: cover.value,
),
);
} else {
final (coverTypeV1, coverValue) = await getCoverV1();
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
coverTypeV1: coverTypeV1,
coverValue: coverValue,
),
);
}
},
updateNameOrIcon: (name, icon) {
emit(
@ -58,10 +87,11 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
),
);
},
updateCover: (coverType, coverValue) {
updateCover: (coverTypeV1, coverTypeV2, coverValue) {
emit(
state.copyWith(
coverType: coverType,
coverTypeV1: coverTypeV1,
coverTypeV2: coverTypeV2,
coverValue: coverValue,
),
);
@ -76,7 +106,12 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
final DocumentListener _documentListener;
final ViewListener _viewListener;
Future<(CoverType, String?)> getCover() async {
PageStyleCover? getCoverV2() {
return view.cover;
}
// for the version under 0.5.5
Future<(CoverType, String?)> getCoverV1() async {
final result = await _service.getDocument(documentId: view.id);
final document = result.fold((s) => s.toDocument(), (f) => null);
if (document != null) {
@ -102,7 +137,8 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
class RecentViewEvent with _$RecentViewEvent {
const factory RecentViewEvent.initial() = Initial;
const factory RecentViewEvent.updateCover(
CoverType coverType,
CoverType coverTypeV1, // for the version under 0.5.5, including 0.5.5
PageStyleCoverImageType? coverTypeV2, // for the version above 0.5.5
String? coverValue,
) = UpdateCover;
const factory RecentViewEvent.updateNameOrIcon(
@ -116,7 +152,8 @@ class RecentViewState with _$RecentViewState {
const factory RecentViewState({
required String name,
required String icon,
@Default(CoverType.none) CoverType coverType,
@Default(CoverType.none) CoverType coverTypeV1,
PageStyleCoverImageType? coverTypeV2,
@Default(null) String? coverValue,
}) = _RecentViewState;

View File

@ -1,4 +1,4 @@
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -40,6 +40,7 @@ class FlowyAppBar extends AppBar {
super.centerTitle,
VoidCallback? onTapLeading,
bool showDivider = true,
super.backgroundColor,
}) : super(
title: title ??
FlowyText(

View File

@ -17,7 +17,7 @@ class AppBarBackButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap ?? () => Navigator.pop(context),
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
padding: padding,
child: const FlowySvg(
FlowySvgs.m_app_bar_back_s,
@ -37,7 +37,7 @@ class AppBarCloseButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap ?? () => Navigator.pop(context),
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
child: const FlowySvg(
FlowySvgs.m_app_bar_close_s,
),
@ -56,7 +56,7 @@ class AppBarCancelButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap ?? () => Navigator.pop(context),
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
child: FlowyText(
LocaleKeys.button_cancel.tr(),
overflow: TextOverflow.ellipsis,
@ -76,7 +76,7 @@ class AppBarDoneButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap,
onTap: (_) => onTap(),
padding: const EdgeInsets.all(12),
child: FlowyText(
LocaleKeys.button_done.tr(),
@ -103,7 +103,7 @@ class AppBarSaveButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: () {
onTap: (_) {
if (enable) {
onTap();
}
@ -166,7 +166,7 @@ class AppBarMoreButton extends StatelessWidget {
Widget build(BuildContext context) {
return AppBarButton(
padding: const EdgeInsets.all(12),
onTap: () => onTap(context),
onTap: onTap,
child: const FlowySvg(FlowySvgs.three_dots_s),
);
}
@ -180,7 +180,7 @@ class AppBarButton extends StatelessWidget {
this.padding,
});
final VoidCallback onTap;
final void Function(BuildContext context) onTap;
final Widget child;
final EdgeInsetsGeometry? padding;
@ -188,7 +188,7 @@ class AppBarButton extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
onTap: () => onTap(context),
child: Padding(
padding: padding ?? const EdgeInsets.all(12),
child: child,

View File

@ -1,11 +1,14 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.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/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';
@ -45,15 +48,26 @@ 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;
final ValueNotifier<double> _appBarOpacity = ValueNotifier(0.0);
@override
void initState() {
super.initState();
future = ViewBackendService.getView(widget.id);
}
@override
void dispose() {
_appBarOpacity.dispose();
_scrollNotificationObserver = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
final child = FutureBuilder(
future: future,
builder: (context, state) {
Widget body;
@ -73,6 +87,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
} else {
body = state.data!.fold((view) {
viewPB = view;
actions.addAll([
if (FeatureFlag.syncDocument.isOn) ...[
DocumentCollaborators(
@ -88,6 +103,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
: DatabaseSyncIndicator(view: view),
const HSpace(8.0),
],
_buildAppBarLayoutButton(view),
_buildAppBarMoreButton(view),
]);
final plugin = view.plugin(arguments: widget.arguments ?? const {})
@ -118,6 +134,13 @@ class _MobileViewPageState extends State<MobileViewPage> {
value: getIt<ReminderBloc>()
..add(const ReminderEvent.started()),
),
if (viewPB!.layout == ViewLayoutPB.Document)
BlocProvider(
create: (_) => DocumentPageStyleBloc(view: viewPB!)
..add(
const DocumentPageStyleEvent.initial(),
),
),
],
child: Builder(
builder: (context) {
@ -131,37 +154,109 @@ class _MobileViewPageState extends State<MobileViewPage> {
}
},
);
return child;
}
Widget _buildApp(ViewPB? view, List<Widget> actions, Widget child) {
// only enable immersive mode for document layout
final isImmersive = view?.layout == ViewLayoutPB.Document;
final icon = view?.icon.value;
final title = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.isNotEmpty)
EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
Expanded(
child: FlowyText.medium(
view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
),
),
],
);
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),
actions: actions,
),
),
),
body: Builder(
builder: (context) {
_rebuildScrollNotificationObserver(context);
return child;
},
),
);
}
return Scaffold(
appBar: FlowyAppBar(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.isNotEmpty)
EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
Expanded(
child: FlowyText.medium(
view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
),
),
],
),
title: title,
actions: actions,
),
body: SafeArea(child: child),
body: child,
);
}
void _rebuildScrollNotificationObserver(BuildContext context) {
_scrollNotificationObserver?.removeListener(_onScrollNotification);
_scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context);
_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(horizontal: 8),
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: const FlowySvg(FlowySvgs.m_layout_s),
);
}
Widget _buildAppBarMoreButton(ViewPB view) {
return AppBarMoreButton(
return AppBarButton(
padding: const EdgeInsets.only(left: 8, right: 16),
onTap: (context) {
EditorNotification.exitEditing().post();
@ -170,13 +265,14 @@ class _MobileViewPageState extends State<MobileViewPage> {
showDragHandle: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => _buildViewPageBottomSheet(context),
builder: (_) => _buildAppBarMoreBottomSheet(context),
);
},
child: const FlowySvg(FlowySvgs.m_app_bar_more_s),
);
}
Widget _buildViewPageBottomSheet(BuildContext context) {
Widget _buildAppBarMoreBottomSheet(BuildContext context) {
final view = context.read<ViewBloc>().state.view;
return ViewPageBottomSheet(
view: view,
@ -228,4 +324,24 @@ class _MobileViewPageState extends State<MobileViewPage> {
},
);
}
// immersive mode related
// auto show or hide the app bar based on the scroll position
void _onScrollNotification(ScrollNotification notification) {
if (_scrollNotificationObserver == null) {
return;
}
if (notification is ScrollUpdateNotification &&
defaultScrollNotificationPredicate(notification)) {
final ScrollMetrics metrics = notification.metrics;
final height = MediaQuery.of(context).padding.top;
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 ||
progress == 0 ||
progress == 1.0) {
_appBarOpacity.value = progress;
}
}
}
}

View File

@ -55,6 +55,31 @@ class BottomSheetDoneButton extends StatelessWidget {
}
}
class BottomSheetRemoveButton extends StatelessWidget {
const BottomSheetRemoveButton({
super.key,
required this.onRemove,
});
final VoidCallback onRemove;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onRemove,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0),
child: FlowyText(
LocaleKeys.button_remove.tr(),
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
textAlign: TextAlign.right,
),
),
);
}
}
class BottomSheetBackButton extends StatelessWidget {
const BottomSheetBackButton({
super.key,

View File

@ -32,6 +32,8 @@ Future<T?> showMobileBottomSheet<T>(
// this field is only used if showHeader is true
bool showBackButton = false,
bool showCloseButton = false,
bool showRemoveButton = false,
VoidCallback? onRemove,
// this field is only used if showHeader is true
String title = '',
bool isScrollControlled = true,
@ -46,6 +48,8 @@ Future<T?> showMobileBottomSheet<T>(
double? elevation,
bool showDoneButton = false,
bool enableDraggableScrollable = false,
// this field is only used if showDragHandle is true
Widget Function(BuildContext, ScrollController)? scrollableWidgetBuilder,
// only used when enableDraggableScrollable is true
double minChildSize = 0.5,
double maxChildSize = 0.8,
@ -102,7 +106,9 @@ Future<T?> showMobileBottomSheet<T>(
showCloseButton: showCloseButton,
showBackButton: showBackButton,
showDoneButton: showDoneButton,
showRemoveButton: showRemoveButton,
title: title,
onRemove: onRemove,
),
);
@ -116,24 +122,30 @@ Future<T?> showMobileBottomSheet<T>(
// ----- header area -----
if (enableDraggableScrollable) {
final keyboardSize =
context.bottomSheetPadding() / MediaQuery.of(context).size.height;
return DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: initialChildSize,
minChildSize: minChildSize,
maxChildSize: maxChildSize,
initialChildSize: (initialChildSize + keyboardSize).clamp(0, 1),
minChildSize: (minChildSize + keyboardSize).clamp(0, 1.0),
maxChildSize: (maxChildSize + keyboardSize).clamp(0, 1.0),
builder: (context, scrollController) {
return Column(
children: [
...children,
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
controller: scrollController,
child: child,
scrollableWidgetBuilder?.call(
context,
scrollController,
) ??
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
controller: scrollController,
child: child,
),
),
),
),
),
],
);
},
@ -175,14 +187,18 @@ class BottomSheetHeader extends StatelessWidget {
super.key,
required this.showBackButton,
required this.showCloseButton,
required this.showRemoveButton,
required this.title,
required this.showDoneButton,
this.onRemove,
});
final bool showBackButton;
final bool showCloseButton;
final bool showRemoveButton;
final String title;
final bool showDoneButton;
final VoidCallback? onRemove;
@override
Widget build(BuildContext context) {
@ -202,6 +218,13 @@ class BottomSheetHeader extends StatelessWidget {
alignment: Alignment.centerLeft,
child: BottomSheetCloseButton(),
),
if (showRemoveButton)
Align(
alignment: Alignment.centerLeft,
child: BottomSheetRemoveButton(
onRemove: () => onRemove?.call(),
),
),
Align(
child: FlowyText(
title,

View File

@ -66,6 +66,7 @@ Future<T?> showTransitionMobileBottomSheet<T>(
showCloseButton: showCloseButton,
showBackButton: showBackButton,
showDoneButton: showDoneButton,
showRemoveButton: false,
title: title,
),
if (showDivider)

View File

@ -1,7 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.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/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
import 'package:appflowy/plugins/database/domain/field_backend_service.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_empty.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.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/widgets/widgets.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';

View File

@ -1,7 +1,7 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';

View File

@ -1,14 +1,17 @@
import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
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';
@ -54,7 +57,8 @@ class MobileRecentView extends StatelessWidget {
topRight: Radius.circular(8),
),
child: _RecentCover(
coverType: state.coverType,
coverTypeV1: state.coverTypeV1,
coverTypeV2: state.coverTypeV2,
value: state.coverValue,
),
),
@ -108,11 +112,13 @@ class MobileRecentView extends StatelessWidget {
class _RecentCover extends StatelessWidget {
const _RecentCover({
required this.coverType,
required this.coverTypeV1,
this.coverTypeV2,
this.value,
});
final CoverType coverType;
final CoverType coverTypeV1;
final PageStyleCoverImageType? coverTypeV2;
final String? value;
@override
@ -125,7 +131,59 @@ class _RecentCover extends StatelessWidget {
if (value == null) {
return placeholder;
}
switch (coverType) {
if (coverTypeV2 != null) {
return _buildCoverV2(context, value, placeholder);
}
return _buildCoverV1(context, value, placeholder);
}
Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) {
final type = coverTypeV2;
if (type == null) {
return placeholder;
}
if (type == PageStyleCoverImageType.customImage ||
type == PageStyleCoverImageType.unsplashImage) {
final userProfilePB = Provider.of<UserProfilePB?>(context);
return FlowyNetworkImage(
url: value,
userProfilePB: userProfilePB,
);
}
if (type == PageStyleCoverImageType.builtInImage) {
return Image.asset(
PageStyleCoverImageType.builtInImagePath(value),
fit: BoxFit.cover,
);
}
if (type == PageStyleCoverImageType.pureColor) {
return ColoredBox(
color: FlowyTint.fromId(value).color(context),
);
}
if (type == PageStyleCoverImageType.gradientColor) {
return Container(
decoration: BoxDecoration(
gradient: FlowyGradientColor.fromId(value).linear,
),
);
}
if (type == PageStyleCoverImageType.localImage) {
return Image.file(
File(value),
fit: BoxFit.cover,
);
}
return placeholder;
}
Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) {
switch (coverTypeV1) {
case CoverType.file:
if (isURL(value)) {
final userProfilePB = Provider.of<UserProfilePB?>(context);

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart';
import 'package:easy_localization/easy_localization.dart';

View File

@ -1,17 +1,20 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
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/util/google_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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
final List<String> _availableFonts = GoogleFonts.asMap().keys.toList();
final List<String> _availableFonts = [
builtInFontFamily(),
...GoogleFonts.asMap().keys,
];
class FontPickerScreen extends StatelessWidget {
const FontPickerScreen({super.key});
@ -25,7 +28,9 @@ class FontPickerScreen extends StatelessWidget {
}
class LanguagePickerPage extends StatefulWidget {
const LanguagePickerPage({super.key});
const LanguagePickerPage({
super.key,
});
@override
State<LanguagePickerPage> createState() => _LanguagePickerPageState();
@ -51,47 +56,95 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
),
body: SafeArea(
child: Scrollbar(
child: ListView.builder(
itemCount: availableFonts.length + 1, // with search bar
itemBuilder: (context, index) {
if (index == 0) {
// search bar
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: FlowyMobileSearchTextField(
onChanged: (keyword) {
setState(() {
availableFonts = _availableFonts
.where(
(font) => font
.parseFontFamilyName()
.toLowerCase()
.contains(keyword.toLowerCase()),
)
.toList();
});
},
),
);
}
final fontFamilyName = availableFonts[index - 1];
final displayName = fontFamilyName.parseFontFamilyName();
return FlowyOptionTile.checkbox(
text: displayName,
isSelected: selectedFontFamilyName == fontFamilyName,
showTopBorder: false,
onTap: () => context.pop(fontFamilyName),
fontFamily: GoogleFonts.getFont(fontFamilyName).fontFamily,
backgroundColor: Colors.transparent,
);
},
child: FontSelector(
selectedFontFamilyName: selectedFontFamilyName,
onFontFamilySelected: (fontFamilyName) =>
context.pop(fontFamilyName),
),
),
),
);
}
}
class FontSelector extends StatefulWidget {
const FontSelector({
super.key,
this.scrollController,
required this.selectedFontFamilyName,
required this.onFontFamilySelected,
});
final ScrollController? scrollController;
final String selectedFontFamilyName;
final void Function(String fontFamilyName) onFontFamilySelected;
@override
State<FontSelector> createState() => _FontSelectorState();
}
class _FontSelectorState extends State<FontSelector> {
late List<String> availableFonts;
@override
void initState() {
super.initState();
availableFonts = _availableFonts;
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: widget.scrollController,
itemCount: availableFonts.length + 1, // with search bar
itemBuilder: (context, index) {
if (index == 0) {
// search bar
return _buildSearchBar(context);
}
final fontFamilyName = availableFonts[index - 1];
final fontFamily = fontFamilyName != builtInFontFamily()
? GoogleFonts.getFont(fontFamilyName).fontFamily
: TextStyle(fontFamily: builtInFontFamily()).fontFamily;
return FlowyOptionTile.checkbox(
// display the default font name if the font family name is empty
text: fontFamilyName.isNotEmpty
? fontFamilyName.parseFontFamilyName()
: LocaleKeys.settings_appearance_fontFamily_defaultFont.tr(),
isSelected: widget.selectedFontFamilyName == fontFamilyName,
showTopBorder: false,
onTap: () => widget.onFontFamilySelected(fontFamilyName),
fontFamily: fontFamily,
backgroundColor: Colors.transparent,
);
},
);
}
Widget _buildSearchBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: FlowyMobileSearchTextField(
onChanged: (keyword) {
setState(() {
availableFonts = _availableFonts
.where(
(font) =>
font.isEmpty || // keep the default one always
font
.parseFontFamilyName()
.toLowerCase()
.contains(keyword.toLowerCase()),
)
.toList();
});
},
),
);
}
}

View File

@ -1,16 +1,14 @@
import 'dart:async';
import 'package:flutter/material.dart';
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/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import '../setting.dart';
@ -30,7 +28,7 @@ class FontSetting extends StatelessWidget {
children: [
FlowyText(
selectedFont,
fontFamily: GoogleFonts.getFont(selectedFont).fontFamily,
// fontFamily: GoogleFonts.getFont(selectedFont).fontFamily,
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/language.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/setting/self_host_setting_group.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';

View File

@ -26,7 +26,7 @@ class FlowyMobileSearchTextField extends StatelessWidget {
onSubmitted: onSubmitted,
placeholder: hintText,
prefixIcon: const FlowySvg(FlowySvgs.m_search_m),
prefixInsets: const EdgeInsets.only(left: 16.0),
prefixInsets: const EdgeInsets.only(left: 16.0, right: 2.0),
suffixIcon: const Icon(Icons.close),
suffixInsets: const EdgeInsets.only(right: 16.0),
placeholderStyle: Theme.of(context).textTheme.titleSmall?.copyWith(
@ -34,6 +34,11 @@ class FlowyMobileSearchTextField extends StatelessWidget {
fontWeight: FontWeight.w400,
fontSize: 14.0,
),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w400,
fontSize: 14.0,
),
),
);
}

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/plugins/base/color/color_picker.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';

View File

@ -9,7 +9,7 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:google_fonts/google_fonts.dart';
// use a global value to store the selected emoji to prevent reloading every time.
EmojiData? _cachedEmojiData;
EmojiData? kCachedEmojiData;
class FlowyEmojiPicker extends StatefulWidget {
const FlowyEmojiPicker({
@ -34,12 +34,12 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
super.initState();
// load the emoji data from cache if it's available
if (_cachedEmojiData != null) {
emojiData = _cachedEmojiData;
if (kCachedEmojiData != null) {
emojiData = kCachedEmojiData;
} else {
EmojiData.builtIn().then(
(value) {
_cachedEmojiData = value;
kCachedEmojiData = value;
setState(() {
emojiData = value;
});

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:easy_localization/easy_localization.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/option_color_list.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';

View File

@ -1,11 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class DocumentAppearance {
@ -58,9 +57,9 @@ class DocumentAppearance {
class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
DocumentAppearanceCubit()
: super(
const DocumentAppearance(
DocumentAppearance(
fontSize: 16.0,
fontFamily: builtInFontFamily,
fontFamily: builtInFontFamily(),
codeFontFamily: builtInCodeFontFamily,
),
);
@ -70,7 +69,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
final fontSize =
prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0;
final fontFamily = prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ??
builtInFontFamily;
builtInFontFamily();
final defaultTextDirection =
prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection);

View File

@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/application/document_data_pb_extension
import 'package:appflowy/plugins/document/application/document_listener.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
@ -18,6 +19,7 @@ import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/util/debounce.dart';
import 'package:appflowy/util/throttle.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
@ -112,6 +114,11 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
final result = await _fetchDocumentState();
_onViewChanged();
_onDocumentChanged();
result.onSuccess((s) {
if (s != null) {
_migrateCover(s);
}
});
final newState = await result.fold(
(s) async {
final userProfilePB =
@ -396,6 +403,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
metadata: jsonEncode(metadata.toJson()),
);
}
// from version 0.5.5, the cover is stored in the view.ext
Future<void> _migrateCover(EditorState editorState) async {
final view = await ViewBackendService.getView(documentId);
view.onSuccess((s) {
return EditorMigration.migrateCoverIfNeeded(s, editorState);
});
}
}
@freezed

View File

@ -1,8 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.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/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/startup/startup.dart';
@ -104,16 +106,35 @@ class _DocumentPageState extends State<DocumentPage>
}
Widget _buildEditorPage(BuildContext context, DocumentState state) {
final appflowyEditorPage = AppFlowyEditorPage(
editorState: state.editorState!,
styleCustomizer: EditorStyleCustomizer(
context: context,
// the 44 is the width of the left action list
padding: EditorStyleCustomizer.documentPadding,
),
header: _buildCoverAndIcon(context, state.editorState!),
initialSelection: widget.initialSelection,
);
final Widget child;
if (PlatformExtension.isMobile) {
child = BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, styleState) {
return AppFlowyEditorPage(
editorState: state.editorState!,
styleCustomizer: EditorStyleCustomizer(
context: context,
// the 44 is the width of the left action list
padding: EditorStyleCustomizer.documentPadding,
),
header: _buildCoverAndIcon(context, state),
initialSelection: widget.initialSelection,
);
},
);
} else {
child = AppFlowyEditorPage(
editorState: state.editorState!,
styleCustomizer: EditorStyleCustomizer(
context: context,
// the 44 is the width of the left action list
padding: EditorStyleCustomizer.documentPadding,
),
header: _buildCoverAndIcon(context, state),
initialSelection: widget.initialSelection,
);
}
return Column(
children: [
@ -122,7 +143,7 @@ class _DocumentPageState extends State<DocumentPage>
// const DocumentSyncIndicator(),
if (state.isDeleted) _buildBanner(context),
Expanded(child: appflowyEditorPage),
Expanded(child: child),
],
);
}
@ -138,9 +159,22 @@ class _DocumentPageState extends State<DocumentPage>
);
}
Widget _buildCoverAndIcon(BuildContext context, EditorState editorState) {
Widget _buildCoverAndIcon(BuildContext context, DocumentState state) {
final editorState = state.editorState;
final userProfilePB = state.userProfilePB;
if (editorState == null || userProfilePB == null) {
return const SizedBox.shrink();
}
if (PlatformExtension.isMobile) {
return DocumentImmersiveCover(
view: widget.view,
userProfilePB: userProfilePB,
);
}
final page = editorState.document.root;
return DocumentHeaderNodeWidget(
return DocumentCoverWidget(
node: page,
editorState: editorState,
view: widget.view,
@ -163,7 +197,9 @@ class _DocumentPageState extends State<DocumentPage>
} else if (type == EditorNotificationType.redo) {
redoCommand.execute(editorState);
} else if (type == EditorNotificationType.exitEditing) {
editorState.selection = null;
if (editorState.selection != null) {
editorState.selection = null;
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart';
@ -11,6 +12,7 @@ import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Map<String, BlockComponentBuilder> getEditorBuilderMap({
required BuildContext context,
@ -30,10 +32,20 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
];
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
final configuration = BlockComponentConfiguration(
// use EdgeInsets.zero to remove the default padding.
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
padding: (node) {
if (PlatformExtension.isMobile) {
final pageStyle = context.read<DocumentPageStyleBloc>().state;
final factor = pageStyle.fontLayout.factor;
final padding = pageStyle.lineHeightLayout.padding * factor;
return EdgeInsets.only(
top: padding,
);
}
return const EdgeInsets.symmetric(vertical: 5.0);
},
indentPadding: (node, textDirection) => textDirection == TextDirection.ltr
? const EdgeInsets.only(left: 26.0)
: const EdgeInsets.only(right: 26.0),
@ -49,6 +61,12 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
configuration: configuration.copyWith(
placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(),
),
iconBuilder: PlatformExtension.isMobile
? (context, node, onCheck) => TodoListIcon(
node: node,
onCheck: onCheck,
)
: null,
toggleChildrenTriggers: [
LogicalKeyboardKey.shift,
LogicalKeyboardKey.shiftLeft,
@ -59,11 +77,22 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
configuration: configuration.copyWith(
placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(),
),
iconBuilder: PlatformExtension.isMobile
? (context, node) => BulletedListIcon(
node: node,
)
: null,
),
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
configuration: configuration.copyWith(
placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(),
),
iconBuilder: PlatformExtension.isMobile
? (context, node, textDirection) => NumberedListIcon(
node: node,
textDirection: textDirection,
)
: null,
),
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
configuration: configuration.copyWith(
@ -72,7 +101,20 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
),
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
configuration: configuration.copyWith(
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
padding: (node) {
if (PlatformExtension.isMobile) {
final pageStyle = context.read<DocumentPageStyleBloc>().state;
final factor = pageStyle.fontLayout.factor;
final headingPaddings = pageStyle.lineHeightLayout.headingPaddings
.map((e) => e * factor);
final level = node.attributes[HeadingBlockKeys.level] ?? 6;
return EdgeInsets.only(
top: headingPaddings.elementAt(level),
);
}
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
},
placeholderText: (node) => LocaleKeys.blockPlaceholders_heading.tr(
args: [node.attributes[HeadingBlockKeys.level].toString()],
),

View File

@ -143,16 +143,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
late final List<SelectionMenuItem> slashMenuItems;
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
getEditorBuilderMap(
slashMenuItems: slashMenuItems,
context: context,
editorState: widget.editorState,
styleCustomizer: widget.styleCustomizer,
showParagraphPlaceholder: widget.showParagraphPlaceholder,
placeholderText: widget.placeholderText,
);
List<CharacterShortcutEvent> get characterShortcutEvents => [
// code block
...codeBlockCharacterEvents,
@ -307,7 +297,14 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
// setup the theme
editorStyle: styleCustomizer.style(),
// customize the block builders
blockComponentBuilders: blockComponentBuilders,
blockComponentBuilders: getEditorBuilderMap(
slashMenuItems: slashMenuItems,
context: context,
editorState: widget.editorState,
styleCustomizer: widget.styleCustomizer,
showParagraphPlaceholder: widget.showParagraphPlaceholder,
placeholderText: widget.placeholderText,
),
// customize the shortcuts
characterShortcutEvents: characterShortcutEvents,
commandShortcutEvents: commandShortcutEvents,

View File

@ -12,4 +12,9 @@ extension BuildContextExtension on BuildContext {
box.hitTest(result, position: box.globalToLocal(offset));
return result.path.any((entry) => entry.target == box);
}
double get appBarHeight =>
AppBarTheme.of(this).toolbarHeight ?? kToolbarHeight;
double get statusBarHeight => statusBarAndAppBarHeight - appBarHeight;
double get statusBarAndAppBarHeight => MediaQuery.of(this).padding.top;
}

View File

@ -0,0 +1,50 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BulletedListIcon extends StatelessWidget {
const BulletedListIcon({
super.key,
required this.node,
});
final Node node;
static final bulletedListIcons = [
FlowySvgs.bulleted_list_icon_1_s,
FlowySvgs.bulleted_list_icon_2_s,
FlowySvgs.bulleted_list_icon_3_s,
];
int get level {
var level = 0;
var parent = node.parent;
while (parent != null) {
if (parent.type == BulletedListBlockKeys.type) {
level++;
}
parent = parent.parent;
}
return level;
}
FlowySvg get icon {
final index = level % bulletedListIcons.length;
return FlowySvg(bulletedListIcons[index]);
}
@override
Widget build(BuildContext context) {
final iconPadding = context.read<DocumentPageStyleBloc>().state.iconPadding;
return Container(
constraints: const BoxConstraints(
minWidth: 22,
minHeight: 22,
),
margin: EdgeInsets.only(top: iconPadding, right: 8.0),
child: icon,
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';

View File

@ -0,0 +1,222 @@
import 'dart:io';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
double kDocumentCoverHeight = 98.0;
double kDocumentTitlePadding = 20.0;
class DocumentImmersiveCover extends StatefulWidget {
const DocumentImmersiveCover({
super.key,
required this.view,
required this.userProfilePB,
});
final ViewPB view;
final UserProfilePB userProfilePB;
@override
State<DocumentImmersiveCover> createState() => _DocumentImmersiveCoverState();
}
class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
final textEditingController = TextEditingController();
final scrollController = ScrollController();
@override
void dispose() {
textEditingController.dispose();
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return IgnoreParentGestureWidget(
child: BlocProvider(
create: (context) => DocumentImmersiveCoverBloc(view: widget.view)
..add(const DocumentImmersiveCoverEvent.initial()),
child: BlocConsumer<DocumentImmersiveCoverBloc,
DocumentImmersiveCoverState>(
listener: (context, state) {
textEditingController.text = state.name;
},
builder: (_, state) {
final iconAndTitle = _buildIconAndTitle(context, state);
if (state.cover.type == PageStyleCoverImageType.none) {
return Padding(
padding: EdgeInsets.only(
top: context.statusBarAndAppBarHeight + kDocumentTitlePadding,
),
child: iconAndTitle,
);
}
return Stack(
children: [
_buildCover(context, state),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: iconAndTitle,
),
),
],
);
},
),
),
);
}
Widget _buildIconAndTitle(
BuildContext context,
DocumentImmersiveCoverState state,
) {
final icon = state.icon;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
children: [
if (icon != null && icon.isNotEmpty) ...[
_buildIcon(context, icon),
const HSpace(8.0),
],
Expanded(child: _buildTitle(context)),
],
),
);
}
Widget _buildTitle(BuildContext context) {
String? fontFamily = builtInFontFamily();
final documentFontFamily =
context.read<DocumentPageStyleBloc>().state.fontFamily;
if (documentFontFamily != null && fontFamily != documentFontFamily) {
fontFamily = GoogleFonts.getFont(documentFontFamily).fontFamily;
}
return TextField(
controller: textEditingController,
decoration: const InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
disabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: '',
contentPadding: EdgeInsets.zero,
),
scrollController: scrollController,
style: TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.w700,
fontFamily: fontFamily,
),
onSubmitted: (value) {
scrollController.position.jumpTo(0);
context.read<ViewBloc>().add(ViewEvent.rename(value));
},
);
}
Widget _buildIcon(BuildContext context, String icon) {
return GestureDetector(
child: EmojiIconWidget(
emoji: icon,
emojiSize: 26,
),
onTap: () async {
final result = await context.push<EmojiPickerResult>(
MobileEmojiPickerScreen.routeName,
);
if (result != null && context.mounted) {
context.read<ViewBloc>().add(ViewEvent.updateIcon(result.emoji));
}
},
);
}
Widget _buildCover(BuildContext context, DocumentImmersiveCoverState state) {
final cover = state.cover;
final type = cover.type;
final naviBarHeight = MediaQuery.of(context).padding.top;
final height = naviBarHeight + kDocumentCoverHeight;
if (type == PageStyleCoverImageType.customImage ||
type == PageStyleCoverImageType.unsplashImage) {
return SizedBox(
height: height,
width: double.infinity,
child: FlowyNetworkImage(
url: cover.value,
userProfilePB: widget.userProfilePB,
),
);
}
if (type == PageStyleCoverImageType.builtInImage) {
return SizedBox(
height: height,
width: double.infinity,
child: Image.asset(
PageStyleCoverImageType.builtInImagePath(cover.value),
fit: BoxFit.cover,
),
);
}
if (type == PageStyleCoverImageType.pureColor) {
return Container(
height: height,
width: double.infinity,
color: FlowyTint.fromId(cover.value).color(context),
);
}
if (type == PageStyleCoverImageType.gradientColor) {
return Container(
height: height,
width: double.infinity,
decoration: BoxDecoration(
gradient: FlowyGradientColor.fromId(cover.value).linear,
),
);
}
if (type == PageStyleCoverImageType.localImage) {
return SizedBox(
height: height,
width: double.infinity,
child: Image.file(
File(cover.value),
fit: BoxFit.cover,
),
);
}
return SizedBox(
height: naviBarHeight,
width: double.infinity,
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_immersive_cover_bloc.freezed.dart';
class DocumentImmersiveCoverBloc
extends Bloc<DocumentImmersiveCoverEvent, DocumentImmersiveCoverState> {
DocumentImmersiveCoverBloc({
required this.view,
}) : _viewListener = ViewListener(viewId: view.id),
super(DocumentImmersiveCoverState.initial()) {
on<DocumentImmersiveCoverEvent>(
(event, emit) async {
await event.when(
initial: () async {
add(
DocumentImmersiveCoverEvent.updateCoverAndIcon(
view.cover,
view.icon.value,
view.name,
),
);
_viewListener?.start(
onViewUpdated: (view) {
add(
DocumentImmersiveCoverEvent.updateCoverAndIcon(
view.cover,
view.icon.value,
view.name,
),
);
},
);
},
updateCoverAndIcon: (cover, icon, name) {
emit(
state.copyWith(
icon: icon,
cover: cover ?? state.cover,
name: name ?? state.name,
),
);
},
);
},
);
}
final ViewPB view;
final ViewListener? _viewListener;
@override
Future<void> close() {
_viewListener?.stop();
return super.close();
}
}
@freezed
class DocumentImmersiveCoverEvent with _$DocumentImmersiveCoverEvent {
const factory DocumentImmersiveCoverEvent.initial() = Initial;
const factory DocumentImmersiveCoverEvent.updateCoverAndIcon(
PageStyleCover? cover,
String? icon,
String? name,
) = UpdateCoverAndIcon;
}
@freezed
class DocumentImmersiveCoverState with _$DocumentImmersiveCoverState {
const factory DocumentImmersiveCoverState({
@Default(null) String? icon,
required PageStyleCover cover,
@Default('') String name,
}) = _DocumentImmersiveCoverState;
factory DocumentImmersiveCoverState.initial() => DocumentImmersiveCoverState(
cover: PageStyleCover.none(),
);
}

View File

@ -0,0 +1,164 @@
import 'dart:io';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
/// This is a transitional component that can be removed once the desktop
/// supports immersive widgets, allowing for the exclusive use of the DocumentImmersiveCover component.
class DesktopCover extends StatefulWidget {
const DesktopCover({
super.key,
required this.view,
required this.editorState,
required this.node,
required this.coverType,
this.coverDetails,
});
final ViewPB view;
final Node node;
final EditorState editorState;
final CoverType coverType;
final String? coverDetails;
@override
State<DesktopCover> createState() => _DesktopCoverState();
}
class _DesktopCoverState extends State<DesktopCover> {
CoverType get coverType => CoverType.fromString(
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
);
String? get coverDetails =>
widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];
@override
Widget build(BuildContext context) {
if (widget.view.extra.isEmpty) {
return _buildCoverImageV1();
}
return _buildCoverImageV2();
}
// version > 0.5.5
Widget _buildCoverImageV2() {
return BlocProvider(
create: (context) => DocumentImmersiveCoverBloc(view: widget.view)
..add(const DocumentImmersiveCoverEvent.initial()),
child:
BlocBuilder<DocumentImmersiveCoverBloc, DocumentImmersiveCoverState>(
builder: (context, state) {
final cover = state.cover;
final type = state.cover.type;
const height = kCoverHeight;
if (type == PageStyleCoverImageType.customImage ||
type == PageStyleCoverImageType.unsplashImage) {
final userProfilePB =
context.read<DocumentBloc>().state.userProfilePB;
return SizedBox(
height: height,
width: double.infinity,
child: FlowyNetworkImage(
url: cover.value,
userProfilePB: userProfilePB,
),
);
}
if (type == PageStyleCoverImageType.builtInImage) {
return SizedBox(
height: height,
width: double.infinity,
child: Image.asset(
PageStyleCoverImageType.builtInImagePath(cover.value),
fit: BoxFit.cover,
),
);
}
if (type == PageStyleCoverImageType.pureColor) {
return Container(
height: height,
width: double.infinity,
color: FlowyTint.fromId(cover.value).color(context),
);
}
if (type == PageStyleCoverImageType.gradientColor) {
return Container(
height: height,
width: double.infinity,
decoration: BoxDecoration(
gradient: FlowyGradientColor.fromId(cover.value).linear,
),
);
}
if (type == PageStyleCoverImageType.localImage) {
return SizedBox(
height: height,
width: double.infinity,
child: Image.file(
File(cover.value),
fit: BoxFit.cover,
),
);
}
return const SizedBox.shrink();
},
),
);
}
// version <= 0.5.5
Widget _buildCoverImageV1() {
final detail = coverDetails;
if (detail == null) {
return const SizedBox.shrink();
}
switch (widget.coverType) {
case CoverType.file:
if (isURL(detail)) {
final userProfilePB =
context.read<DocumentBloc>().state.userProfilePB;
return FlowyNetworkImage(
url: detail,
userProfilePB: userProfilePB,
errorWidgetBuilder: (context, url, error) =>
const SizedBox.shrink(),
);
}
final imageFile = File(detail);
if (!imageFile.existsSync()) {
return const SizedBox.shrink();
}
return Image.file(
imageFile,
fit: BoxFit.cover,
);
case CoverType.asset:
return Image.asset(
widget.coverDetails!,
fit: BoxFit.cover,
);
case CoverType.color:
final color = widget.coverDetails?.tryToColor() ?? Colors.white;
return Container(color: color);
case CoverType.none:
return const SizedBox.shrink();
}
}
}

View File

@ -1,17 +1,17 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/desktop_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
@ -21,6 +21,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:string_validator/string_validator.dart';
@ -31,6 +32,7 @@ const double kCoverHeight = 250.0;
const double kIconHeight = 60.0;
const double kToolbarHeight = 40.0; // with padding to the top
// Remove this widget if the desktop support immersive cover.
class DocumentHeaderBlockKeys {
const DocumentHeaderBlockKeys._();
@ -39,6 +41,7 @@ class DocumentHeaderBlockKeys {
static const String icon = 'selected_icon';
}
// for the version under 0.5.5, including 0.5.5
enum CoverType {
none,
color,
@ -56,8 +59,8 @@ enum CoverType {
}
}
class DocumentHeaderNodeWidget extends StatefulWidget {
const DocumentHeaderNodeWidget({
class DocumentCoverWidget extends StatefulWidget {
const DocumentCoverWidget({
super.key,
required this.node,
required this.editorState,
@ -71,11 +74,10 @@ class DocumentHeaderNodeWidget extends StatefulWidget {
final ViewPB view;
@override
State<DocumentHeaderNodeWidget> createState() =>
_DocumentHeaderNodeWidgetState();
State<DocumentCoverWidget> createState() => _DocumentCoverWidgetState();
}
class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
CoverType get coverType => CoverType.fromString(
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
);
@ -130,6 +132,7 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
),
if (hasCover)
DocumentCover(
view: widget.view,
editorState: widget.editorState,
node: widget.node,
coverType: coverType,
@ -189,8 +192,16 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
widget.onIconChanged(icon);
}
// compatible with version <= 0.5.5.
transaction.updateNode(widget.node, attributes);
await widget.editorState.apply(transaction);
// compatible with version > 0.5.5.
EditorMigration.migrateCoverIfNeeded(
widget.view,
widget.editorState,
overwrite: true,
);
}
}
@ -366,6 +377,7 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
class DocumentCover extends StatefulWidget {
const DocumentCover({
super.key,
required this.view,
required this.node,
required this.editorState,
required this.coverType,
@ -373,6 +385,7 @@ class DocumentCover extends StatefulWidget {
required this.onChangeCover,
});
final ViewPB view;
final Node node;
final EditorState editorState;
final CoverType coverType;
@ -407,7 +420,13 @@ class DocumentCoverState extends State<DocumentCover> {
SizedBox(
height: double.infinity,
width: double.infinity,
child: _buildCoverImage(),
child: DesktopCover(
view: widget.view,
editorState: widget.editorState,
node: widget.node,
coverType: widget.coverType,
coverDetails: widget.coverDetails,
),
),
if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context),
],

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';

View File

@ -1,28 +1,42 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:unsplash_client/unsplash_client.dart';
const _accessKeyA = 'YyD-LbW5bVolHWZBq5fWRM_';
const _accessKeyB = '3ezkG2XchRFjhNTnK9TE';
const _secretKeyA = '5z4EnxaXjWjWMnuBhc0Ku0u';
const _secretKeyB = 'YW2bsYCZlO-REZaqmV6A';
enum UnsplashImageType {
// the creator name is under the image
halfScreen,
// the creator name is on the image
fullScreen,
}
typedef OnSelectUnsplashImage = void Function(String url);
class UnsplashImageWidget extends StatefulWidget {
const UnsplashImageWidget({
super.key,
this.type = UnsplashImageType.halfScreen,
required this.onSelectUnsplashImage,
});
final void Function(String url) onSelectUnsplashImage;
final UnsplashImageType type;
final OnSelectUnsplashImage onSelectUnsplashImage;
@override
State<UnsplashImageWidget> createState() => _UnsplashImageWidgetState();
}
class _UnsplashImageWidgetState extends State<UnsplashImageWidget> {
final client = UnsplashClient(
final unsplash = UnsplashClient(
settings: const ClientSettings(
credentials: AppCredentials(
// TODO: there're the demo keys, we should replace them with the production keys when releasing and inject them with env file.
accessKey: 'YyD-LbW5bVolHWZBq5fWRM_3ezkG2XchRFjhNTnK9TE',
secretKey: '5z4EnxaXjWjWMnuBhc0Ku0uYW2bsYCZlO-REZaqmV6A',
accessKey: _accessKeyA + _accessKeyB,
secretKey: _secretKeyA + _secretKeyB,
),
),
);
@ -35,14 +49,14 @@ class _UnsplashImageWidgetState extends State<UnsplashImageWidget> {
void initState() {
super.initState();
randomPhotos = client.photos
randomPhotos = unsplash.photos
.random(count: 18, orientation: PhotoOrientation.landscape)
.goAndGet();
}
@override
void dispose() {
client.close();
unsplash.close();
super.dispose();
}
@ -52,25 +66,12 @@ class _UnsplashImageWidgetState extends State<UnsplashImageWidget> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: FlowyTextField(
hintText: LocaleKeys.document_imageBlock_searchForAnImage.tr(),
onChanged: (value) => query = value,
onEditingComplete: _search,
),
),
const HSpace(4.0),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.search_label.tr(),
),
onTap: _search,
),
],
SizedBox(
height: 44,
child: FlowyMobileSearchTextField(
onChanged: (keyword) => query = keyword,
onSubmitted: (_) => _search(),
),
),
const VSpace(12.0),
Expanded(
@ -86,21 +87,10 @@ class _UnsplashImageWidgetState extends State<UnsplashImageWidget> {
child: CircularProgressIndicator.adaptive(),
);
}
return GridView.count(
crossAxisCount: 3,
mainAxisSpacing: 16.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4 / 3,
children: data
.map(
(photo) => _UnsplashImage(
photo: photo,
onTap: () => widget.onSelectUnsplashImage(
photo.urls.regular.toString(),
),
),
)
.toList(),
return _UnsplashImages(
type: widget.type,
photos: data,
onSelectUnsplashImage: widget.onSelectUnsplashImage,
);
},
),
@ -111,7 +101,7 @@ class _UnsplashImageWidgetState extends State<UnsplashImageWidget> {
void _search() {
setState(() {
randomPhotos = client.photos
randomPhotos = unsplash.photos
.random(
count: 18,
orientation: PhotoOrientation.landscape,
@ -122,35 +112,113 @@ class _UnsplashImageWidgetState extends State<UnsplashImageWidget> {
}
}
class _UnsplashImages extends StatelessWidget {
const _UnsplashImages({
required this.type,
required this.photos,
required this.onSelectUnsplashImage,
});
final UnsplashImageType type;
final List<Photo> photos;
final OnSelectUnsplashImage onSelectUnsplashImage;
@override
Widget build(BuildContext context) {
final crossAxisCount = switch (type) {
UnsplashImageType.halfScreen => 3,
UnsplashImageType.fullScreen => 2,
};
final mainAxisSpacing = switch (type) {
UnsplashImageType.halfScreen => 16.0,
UnsplashImageType.fullScreen => 8.0,
};
return GridView.count(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: 10.0,
childAspectRatio: 4 / 3,
children: photos
.map(
(photo) => _UnsplashImage(
type: type,
photo: photo,
onTap: () => onSelectUnsplashImage(
photo.urls.regular.toString(),
),
),
)
.toList(),
);
}
}
class _UnsplashImage extends StatelessWidget {
const _UnsplashImage({
required this.type,
required this.photo,
required this.onTap,
});
final UnsplashImageType type;
final Photo photo;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final child = switch (type) {
UnsplashImageType.halfScreen => _buildHalfScreenImage(context),
UnsplashImageType.fullScreen => _buildFullScreenImage(context),
};
return GestureDetector(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Image.network(
child: child,
);
}
Widget _buildHalfScreenImage(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Image.network(
photo.urls.thumb.toString(),
fit: BoxFit.cover,
),
),
const HSpace(2.0),
FlowyText(
'by ${photo.name}',
fontSize: 10.0,
),
],
);
}
Widget _buildFullScreenImage(BuildContext context) {
return Stack(
children: [
LayoutBuilder(
builder: (context, constraints) {
return Image.network(
photo.urls.thumb.toString(),
fit: BoxFit.cover,
),
),
const HSpace(2.0),
FlowyText(
'by ${photo.name}',
width: constraints.maxWidth,
height: constraints.maxHeight,
);
},
),
Positioned(
bottom: 6,
left: 6,
child: FlowyText.medium(
photo.name,
fontSize: 10.0,
color: Colors.white,
),
],
),
),
],
);
}
}

View File

@ -1,9 +1,15 @@
import 'dart:convert';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.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';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:collection/collection.dart';
import 'package:string_validator/string_validator.dart';
class EditorMigration {
// AppFlowy 0.1.x -> 0.2
@ -153,4 +159,80 @@ class EditorMigration {
}
return attributes;
}
// Before version 0.5.5, the cover is stored in the document root.
// Now, the cover is stored in the view.ext.
static void migrateCoverIfNeeded(
ViewPB view,
EditorState editorState, {
bool overwrite = false,
}) async {
if (view.extra.isNotEmpty && !overwrite) {
return;
}
final root = editorState.document.root;
final coverType = CoverType.fromString(
root.attributes[DocumentHeaderBlockKeys.coverType],
);
final coverDetails = root.attributes[DocumentHeaderBlockKeys.coverDetails];
if (coverType == CoverType.none ||
coverDetails == null ||
coverDetails is! String) {
return;
}
Map extra = {};
switch (coverType) {
case CoverType.asset:
// The new version does not support the asset cover.
break;
case CoverType.color:
extra = {
ViewExtKeys.coverKey: {
ViewExtKeys.coverTypeKey:
PageStyleCoverImageType.pureColor.toString(),
ViewExtKeys.coverValueKey: coverDetails,
},
};
break;
case CoverType.file:
if (isURL(coverDetails)) {
if (coverDetails.contains('unsplash')) {
extra = {
ViewExtKeys.coverKey: {
ViewExtKeys.coverTypeKey:
PageStyleCoverImageType.unsplashImage.toString(),
ViewExtKeys.coverValueKey: coverDetails,
},
};
} else {
extra = {
ViewExtKeys.coverKey: {
ViewExtKeys.coverTypeKey:
PageStyleCoverImageType.customImage.toString(),
ViewExtKeys.coverValueKey: coverDetails,
},
};
}
}
break;
default:
}
if (extra.isEmpty) {
return;
}
try {
final current = view.extra.isNotEmpty ? jsonDecode(view.extra) : {};
final merged = mergeMaps(current, extra);
await ViewBackendService.updateView(
viewId: view.id,
extra: jsonEncode(merged),
);
} catch (e) {
Log.error('Failed to migrating cover: $e');
}
}
}

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';

View File

@ -0,0 +1,111 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:numerus/roman/roman.dart';
class NumberedListIcon extends StatelessWidget {
const NumberedListIcon({
super.key,
required this.node,
required this.textDirection,
});
final Node node;
final TextDirection textDirection;
@override
Widget build(BuildContext context) {
final textStyle =
context.read<EditorState>().editorStyle.textStyleConfiguration.text;
return Container(
constraints: const BoxConstraints(
minWidth: 22,
minHeight: 22,
),
margin: const EdgeInsets.only(right: 8.0),
alignment: Alignment.center,
child: Center(
child: Text(
node.levelString,
style: textStyle,
textDirection: textDirection,
),
),
);
}
}
extension on Node {
String get levelString {
final builder = _NumberedListIconBuilder(node: this);
final indexInRootLevel = builder.indexInRootLevel;
final indexInSameLevel = builder.indexInSameLevel;
final level = indexInRootLevel % 3;
final levelString = switch (level) {
1 => indexInSameLevel.latin,
2 => indexInSameLevel.roman,
_ => '$indexInSameLevel',
};
return '$levelString.';
}
}
class _NumberedListIconBuilder {
_NumberedListIconBuilder({
required this.node,
});
final Node node;
// the level of the current node
int get indexInRootLevel {
var level = 0;
var parent = node.parent;
while (parent != null) {
if (parent.type == NumberedListBlockKeys.type) {
level++;
}
parent = parent.parent;
}
return level;
}
// the index of the current level
int get indexInSameLevel {
int level = 1;
Node? previous = node.previous;
// if the previous one is not a numbered list, then it is the first one
if (previous == null || previous.type != NumberedListBlockKeys.type) {
return node.attributes[NumberedListBlockKeys.number] ?? level;
}
int? startNumber;
while (previous != null && previous.type == NumberedListBlockKeys.type) {
startNumber = previous.attributes[NumberedListBlockKeys.number] as int?;
level++;
previous = previous.previous;
}
if (startNumber != null) {
return startNumber + level - 1;
}
return level;
}
}
extension on int {
String get latin {
String result = '';
int number = this;
while (number > 0) {
final int remainder = (number - 1) % 26;
result = String.fromCharCode(remainder + 65) + result;
number = (number - 1) ~/ 26;
}
return result.toLowerCase();
}
String get roman {
return toRomanNumeralString() ?? '$this';
}
}

View File

@ -0,0 +1,287 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';
import 'package:appflowy/shared/feedback_gesture_detector.dart';
import 'package:appflowy/shared/flowy_gradient_colors.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';
class PageCoverBottomSheet extends StatelessWidget {
const PageCoverBottomSheet({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(8.0),
// pure colors
FlowyText(
LocaleKeys.pageStyle_colors.tr(),
color: context.pageStyleTextColor,
fontSize: 14.0,
),
const VSpace(8.0),
_buildPureColors(context, state),
const VSpace(20.0),
// gradient colors
FlowyText(
LocaleKeys.pageStyle_gradient.tr(),
color: context.pageStyleTextColor,
fontSize: 14.0,
),
const VSpace(8.0),
_buildGradientColors(context, state),
const VSpace(20.0),
// built-in images
FlowyText(
LocaleKeys.pageStyle_backgroundImage.tr(),
color: context.pageStyleTextColor,
fontSize: 14.0,
),
const VSpace(8.0),
_buildBuiltImages(context, state),
],
),
);
},
);
}
Widget _buildPureColors(
BuildContext context,
DocumentPageStyleState state,
) {
return SizedBox(
height: 42.0,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: FlowyTint.values.length,
separatorBuilder: (context, index) => const HSpace(12.0),
itemBuilder: (context, index) => _buildColorButton(
context,
state,
FlowyTint.values[index],
),
),
);
}
Widget _buildGradientColors(
BuildContext context,
DocumentPageStyleState state,
) {
return SizedBox(
height: 42.0,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: FlowyGradientColor.values.length,
separatorBuilder: (context, index) => const HSpace(12.0),
itemBuilder: (context, index) => _buildGradientButton(
context,
state,
FlowyGradientColor.values[index],
),
),
);
}
Widget _buildColorButton(
BuildContext context,
DocumentPageStyleState state,
FlowyTint tint,
) {
final isSelected =
state.coverImage.isPureColor && state.coverImage.value == tint.id;
final child = !isSelected
? Container(
width: 42,
height: 42,
decoration: ShapeDecoration(
color: tint.color(context),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(21),
),
),
)
: Container(
width: 42,
height: 42,
decoration: ShapeDecoration(
color: Colors.transparent,
shape: RoundedRectangleBorder(
side: BorderSide(
width: 1.50,
color: Theme.of(context).colorScheme.primary,
),
borderRadius: BorderRadius.circular(21),
),
),
alignment: Alignment.center,
child: Container(
width: 34,
height: 34,
decoration: ShapeDecoration(
color: tint.color(context),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(17),
),
),
),
);
return FeedbackGestureDetector(
onTap: () {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover(
type: PageStyleCoverImageType.pureColor,
value: tint.id,
),
),
);
},
child: child,
);
}
Widget _buildGradientButton(
BuildContext context,
DocumentPageStyleState state,
FlowyGradientColor gradientColor,
) {
final isSelected = state.coverImage.isGradient &&
state.coverImage.value == gradientColor.id;
final child = !isSelected
? Container(
width: 42,
height: 42,
decoration: ShapeDecoration(
gradient: gradientColor.linear,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(21),
),
),
)
: Container(
width: 42,
height: 42,
decoration: ShapeDecoration(
color: Colors.transparent,
shape: RoundedRectangleBorder(
side: BorderSide(
width: 1.50,
color: Theme.of(context).colorScheme.primary,
),
borderRadius: BorderRadius.circular(21),
),
),
alignment: Alignment.center,
child: Container(
width: 34,
height: 34,
decoration: ShapeDecoration(
gradient: gradientColor.linear,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(17),
),
),
),
);
return FeedbackGestureDetector(
onTap: () {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover(
type: PageStyleCoverImageType.gradientColor,
value: gradientColor.id,
),
),
);
},
child: child,
);
}
Widget _buildBuiltImages(
BuildContext context,
DocumentPageStyleState state,
) {
final imageNames = ['1', '2', '3', '4', '5', '6'];
return GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 16.0 / 9.0,
),
itemCount: imageNames.length,
itemBuilder: (context, index) => _buildBuiltInImage(
context,
state,
imageNames[index],
),
);
}
Widget _buildBuiltInImage(
BuildContext context,
DocumentPageStyleState state,
String imageName,
) {
final asset = PageStyleCoverImageType.builtInImagePath(imageName);
final isSelected =
state.coverImage.isBuiltInImage && state.coverImage.value == imageName;
final image = ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.asset(
asset,
fit: BoxFit.cover,
),
);
final child = !isSelected
? image
: Container(
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(width: 1.50, color: Color(0xFF00BCF0)),
borderRadius: BorderRadius.circular(6),
),
),
padding: const EdgeInsets.all(2.0),
child: image,
);
return FeedbackGestureDetector(
onTap: () {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover(
type: PageStyleCoverImageType.builtInImage,
value: imageName,
),
),
);
},
child: child,
);
}
}

View File

@ -0,0 +1,310 @@
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/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/user/application/user_service.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
class PageStyleCoverImage extends StatelessWidget {
PageStyleCoverImage({
super.key,
});
late final ImagePicker _imagePicker = ImagePicker();
@override
Widget build(BuildContext context) {
final backgroundColor = context.pageStyleBackgroundColor;
return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, state) {
return Row(
children: [
_buildOptionGroup(
context,
backgroundColor,
state,
),
],
);
},
);
}
Widget _buildOptionGroup(
BuildContext context,
Color backgroundColor,
DocumentPageStyleState state,
) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(12),
right: Radius.circular(12),
),
),
padding: const EdgeInsets.all(4.0),
child: Row(
children: [
_CoverOptionButton(
showLeftCorner: true,
showRightCorner: false,
selected: state.coverImage.isPresets,
onTap: () => _showPresets(context),
child: const _PresetCover(),
),
_CoverOptionButton(
showLeftCorner: false,
showRightCorner: false,
selected: state.coverImage.isPhoto,
onTap: () => _pickImage(context),
child: const _PhotoCover(),
),
_CoverOptionButton(
showLeftCorner: false,
showRightCorner: true,
selected: state.coverImage.isUnsplashImage,
onTap: () => _showUnsplash(context),
child: const _UnsplashCover(),
),
],
),
),
);
}
void _showPresets(BuildContext context) {
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
showRemoveButton: true,
onRemove: () {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover.none(),
),
);
},
title: LocaleKeys.pageStyle_pageCover.tr(),
barrierColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) {
return BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
child: const PageCoverBottomSheet(),
);
},
);
}
Future<void> _pickImage(BuildContext context) async {
final result = await _imagePicker.pickImage(
source: ImageSource.gallery,
);
final path = result?.path;
if (path != null && context.mounted) {
final String? result;
final userProfile = await UserBackendService.getCurrentUserProfile().fold(
(s) => s,
(f) => null,
);
final isAppFlowyCloud =
userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud;
final PageStyleCoverImageType type;
if (!isAppFlowyCloud) {
result = await saveImageToLocalStorage(path);
type = PageStyleCoverImageType.localImage;
} else {
// else we should save the image to cloud storage
(result, _) = await saveImageToCloudStorage(path);
type = PageStyleCoverImageType.customImage;
}
if (!context.mounted) {
return;
}
if (result == null) {
showSnapBar(
context,
LocaleKeys.document_plugins_image_imageUploadFailed,
);
return;
}
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover(
type: type,
value: result,
),
),
);
}
}
void _showUnsplash(BuildContext context) {
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
showRemoveButton: true,
title: LocaleKeys.pageStyle_coverImage.tr(),
barrierColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
onRemove: () {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover.none(),
),
);
},
builder: (_) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.6,
minHeight: 80,
),
child: BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: UnsplashImageWidget(
type: UnsplashImageType.fullScreen,
onSelectUnsplashImage: (url) {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover(
type: PageStyleCoverImageType.unsplashImage,
value: url,
),
),
);
},
),
),
),
);
},
);
}
}
class _UnsplashCover extends StatelessWidget {
const _UnsplashCover();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(FlowySvgs.m_page_style_unsplash_m),
const VSpace(4.0),
FlowyText(
LocaleKeys.pageStyle_unsplash.tr(),
fontSize: 12.0,
),
],
);
}
}
class _PhotoCover extends StatelessWidget {
const _PhotoCover();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(FlowySvgs.m_page_style_photo_m),
const VSpace(4.0),
FlowyText(
LocaleKeys.pageStyle_photo.tr(),
fontSize: 12.0,
),
],
);
}
}
class _PresetCover extends StatelessWidget {
const _PresetCover();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(
FlowySvgs.m_page_style_presets_m,
blendMode: null,
),
const VSpace(4.0),
FlowyText(
LocaleKeys.pageStyle_presets.tr(),
fontSize: 12.0,
),
],
);
}
}
class _CoverOptionButton extends StatelessWidget {
const _CoverOptionButton({
required this.showLeftCorner,
required this.showRightCorner,
required this.child,
required this.onTap,
required this.selected,
});
final Widget child;
final bool showLeftCorner;
final bool showRightCorner;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Expanded(
child: FeedbackGestureDetector(
feedbackType: HapticFeedbackType.medium,
onTap: onTap,
child: AnimatedContainer(
height: 64,
duration: Durations.medium1,
decoration: selected
? ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.50,
color: Color(0xFF1AC3F2),
),
borderRadius: BorderRadius.circular(12),
),
)
: null,
alignment: Alignment.center,
child: child,
),
),
);
}
}

View File

@ -0,0 +1,238 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
class PageStyleIcon extends StatelessWidget {
const PageStyleIcon({
super.key,
required this.view,
});
final ViewPB view;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => PageStyleIconBloc(view: view)
..add(const PageStyleIconEvent.initial()),
child: BlocBuilder<PageStyleIconBloc, PageStyleIconState>(
builder: (context, state) {
final icon = state.icon ?? '';
return GestureDetector(
onTap: () => _showIconSelector(context, icon),
behavior: HitTestBehavior.opaque,
child: Container(
height: 52,
decoration: BoxDecoration(
color: context.pageStyleBackgroundColor,
borderRadius: BorderRadius.circular(12.0),
),
child: Row(
children: [
const HSpace(16.0),
FlowyText(LocaleKeys.document_plugins_emoji.tr()),
const Spacer(),
FlowyText(
icon.isNotEmpty ? icon : LocaleKeys.pageStyle_none.tr(),
color: icon.isEmpty ? context.pageStyleTextColor : null,
fontSize: icon.isNotEmpty ? 22.0 : 16.0,
),
const HSpace(6.0),
const FlowySvg(FlowySvgs.m_page_style_arrow_right_s),
const HSpace(12.0),
],
),
),
);
},
),
);
}
void _showIconSelector(BuildContext context, String selectedIcon) {
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
title: LocaleKeys.titleBar_pageIcon.tr(),
barrierColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
isScrollControlled: true,
enableDraggableScrollable: true,
minChildSize: 0.6,
initialChildSize: 0.61,
showRemoveButton: true,
onRemove: () {
context.read<PageStyleIconBloc>().add(
const PageStyleIconEvent.updateIcon('', true),
);
},
scrollableWidgetBuilder: (_, controller) {
return BlocProvider.value(
value: context.read<PageStyleIconBloc>(),
child: Expanded(
child: Scrollbar(
controller: controller,
child: _IconSelector(
scrollController: controller,
),
),
),
);
},
builder: (_) => const SizedBox.shrink(),
);
}
}
class _IconSelector extends StatefulWidget {
const _IconSelector({
required this.scrollController,
});
final ScrollController scrollController;
@override
State<_IconSelector> createState() => _IconSelectorState();
}
class _IconSelectorState extends State<_IconSelector> {
EmojiData? emojiData;
List<String> availableEmojis = [];
@override
void initState() {
super.initState();
// load the emoji data from cache if it's available
if (kCachedEmojiData != null) {
emojiData = kCachedEmojiData;
availableEmojis = _setupAvailableEmojis(emojiData!);
} else {
EmojiData.builtIn().then(
(value) {
kCachedEmojiData = value;
setState(() {
emojiData = value;
availableEmojis = _setupAvailableEmojis(value);
});
},
);
}
}
@override
Widget build(BuildContext context) {
if (emojiData == null) {
return const Center(child: CircularProgressIndicator());
}
return RepaintBoundary(
child: BlocBuilder<PageStyleIconBloc, PageStyleIconState>(
builder: (_, state) => Column(
children: [
_buildSearchBar(context),
Expanded(
child: GridView.count(
crossAxisCount: _getEmojiPerLine(context),
controller: widget.scrollController,
children: [
for (final emoji in availableEmojis)
_buildEmoji(context, emoji, state.icon),
],
),
),
],
),
),
);
}
Widget _buildEmoji(
BuildContext context,
String emoji,
String? selectedEmoji,
) {
Widget child = Center(
child: FlowyText.emoji(
emoji,
fontSize: 24,
),
);
if (emoji == selectedEmoji) {
child = Container(
margin: const EdgeInsets.all(8.0),
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.50,
strokeAlign: BorderSide.strokeAlignOutside,
color: Color(0xFF00BCF0),
),
borderRadius: BorderRadius.circular(9),
),
),
child: child,
);
}
return GestureDetector(
onTap: () {
context.read<PageStyleIconBloc>().add(
PageStyleIconEvent.updateIcon(emoji, true),
);
},
child: child,
);
}
List<String> _setupAvailableEmojis(EmojiData emojiData) {
final categories = emojiData.categories;
availableEmojis = categories
.map((e) => e.emojiIds.map((e) => emojiData.getEmojiById(e)))
.expand((e) => e)
.toList();
return availableEmojis;
}
int _getEmojiPerLine(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width ~/ 48.0; // the size of the emoji
}
Widget _buildSearchBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: FlowyMobileSearchTextField(
onChanged: (keyword) {
if (emojiData == null) {
return;
}
final filtered = emojiData!.filterByKeyword(keyword);
final availableEmojis = _setupAvailableEmojis(filtered);
setState(() {
this.availableEmojis = availableEmojis;
});
},
),
);
}
}

View File

@ -0,0 +1,79 @@
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part '_page_style_icon_bloc.freezed.dart';
class PageStyleIconBloc extends Bloc<PageStyleIconEvent, PageStyleIconState> {
PageStyleIconBloc({
required this.view,
}) : _viewListener = ViewListener(viewId: view.id),
super(PageStyleIconState.initial()) {
on<PageStyleIconEvent>(
(event, emit) async {
await event.when(
initial: () async {
add(
PageStyleIconEvent.updateIcon(
view.icon.value,
false,
),
);
_viewListener?.start(
onViewUpdated: (view) {
add(
PageStyleIconEvent.updateIcon(
view.icon.value,
false,
),
);
},
);
},
updateIcon: (icon, shouldUpdateRemote) async {
emit(
state.copyWith(
icon: icon,
),
);
if (shouldUpdateRemote && icon != null) {
await ViewBackendService.updateViewIcon(
viewId: view.id,
viewIcon: icon,
);
}
},
);
},
);
}
final ViewPB view;
final ViewListener? _viewListener;
@override
Future<void> close() {
_viewListener?.stop();
return super.close();
}
}
@freezed
class PageStyleIconEvent with _$PageStyleIconEvent {
const factory PageStyleIconEvent.initial() = Initial;
const factory PageStyleIconEvent.updateIcon(
String? icon,
bool shouldUpdateRemote,
) = UpdateIconInner;
}
@freezed
class PageStyleIconState with _$PageStyleIconState {
const factory PageStyleIconState({
@Default(null) String? icon,
}) = _PageStyleIconState;
factory PageStyleIconState.initial() => const PageStyleIconState();
}

View File

@ -0,0 +1,235 @@
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/show_mobile_bottom_sheet.dart';
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/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';
import 'package:flutter_bloc/flutter_bloc.dart';
const kPageStyleLayoutHeight = 52.0;
class PageStyleLayout extends StatelessWidget {
const PageStyleLayout({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, state) {
return Column(
children: [
Row(
children: [
_OptionGroup<PageStyleFontLayout>(
options: const [
PageStyleFontLayout.small,
PageStyleFontLayout.normal,
PageStyleFontLayout.large,
],
selectedOption: state.fontLayout,
onTap: (option) => context
.read<DocumentPageStyleBloc>()
.add(DocumentPageStyleEvent.updateFont(option)),
),
const HSpace(14),
_OptionGroup<PageStyleLineHeightLayout>(
options: const [
PageStyleLineHeightLayout.small,
PageStyleLineHeightLayout.normal,
PageStyleLineHeightLayout.large,
],
selectedOption: state.lineHeightLayout,
onTap: (option) => context
.read<DocumentPageStyleBloc>()
.add(DocumentPageStyleEvent.updateLineHeight(option)),
),
],
),
const VSpace(12.0),
const _FontButton(),
],
);
},
);
}
}
class _OptionGroup<T> extends StatelessWidget {
const _OptionGroup({
required this.options,
required this.selectedOption,
required this.onTap,
});
final List<T> options;
final T selectedOption;
final void Function(T option) onTap;
@override
Widget build(BuildContext context) {
return Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: context.pageStyleBackgroundColor,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(12),
right: Radius.circular(12),
),
),
child: Row(
children: options.map((option) {
final child = _buildSvg(option);
final showLeftCorner = option == options.first;
final showRightCorner = option == options.last;
return _buildOptionButton(
child,
showLeftCorner,
showRightCorner,
selectedOption == option,
() => onTap(option),
);
}).toList(),
),
),
);
}
Widget _buildOptionButton(
Widget child,
bool showLeftCorner,
bool showRightCorner,
bool selected,
VoidCallback onTap,
) {
return Expanded(
child: FeedbackGestureDetector(
feedbackType: HapticFeedbackType.medium,
onTap: onTap,
child: AnimatedContainer(
height: kPageStyleLayoutHeight,
duration: Durations.medium1,
decoration: selected
? ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.50,
color: Color(0xFF1AC3F2),
),
borderRadius: BorderRadius.circular(12),
),
)
: null,
alignment: Alignment.center,
child: child,
),
),
);
}
Widget _buildSvg(dynamic option) {
if (option is PageStyleFontLayout) {
return switch (option) {
PageStyleFontLayout.small =>
const FlowySvg(FlowySvgs.m_font_size_small_s),
PageStyleFontLayout.normal =>
const FlowySvg(FlowySvgs.m_font_size_normal_s),
PageStyleFontLayout.large =>
const FlowySvg(FlowySvgs.m_font_size_large_s),
};
} else if (option is PageStyleLineHeightLayout) {
return switch (option) {
PageStyleLineHeightLayout.small =>
const FlowySvg(FlowySvgs.m_layout_small_s),
PageStyleLineHeightLayout.normal =>
const FlowySvg(FlowySvgs.m_layout_normal_s),
PageStyleLineHeightLayout.large =>
const FlowySvg(FlowySvgs.m_layout_large_s),
};
}
throw ArgumentError('Invalid option type');
}
}
class _FontButton extends StatelessWidget {
const _FontButton();
@override
Widget build(BuildContext context) {
return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, state) {
return GestureDetector(
onTap: () => _showFontSelector(context),
behavior: HitTestBehavior.opaque,
child: Container(
height: kPageStyleLayoutHeight,
decoration: BoxDecoration(
color: context.pageStyleBackgroundColor,
borderRadius: BorderRadius.circular(12.0),
),
child: Row(
children: [
const HSpace(16.0),
FlowyText(LocaleKeys.titleBar_font.tr()),
const Spacer(),
FlowyText(state.fontFamily ?? builtInFontFamily()),
const HSpace(6.0),
const FlowySvg(FlowySvgs.m_page_style_arrow_right_s),
const HSpace(12.0),
],
),
),
);
},
);
}
void _showFontSelector(BuildContext context) {
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
title: LocaleKeys.titleBar_font.tr(),
barrierColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
isScrollControlled: true,
enableDraggableScrollable: true,
minChildSize: 0.6,
initialChildSize: 0.61,
scrollableWidgetBuilder: (_, controller) {
return BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
child: BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, state) {
return Expanded(
child: Scrollbar(
controller: controller,
child: FontSelector(
scrollController: controller,
selectedFontFamilyName:
state.fontFamily ?? builtInFontFamily(),
onFontFamilySelected: (fontFamilyName) {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateFontFamily(
fontFamilyName,
),
);
},
),
),
);
},
),
);
},
builder: (_) => const SizedBox.shrink(),
);
}
}

View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
extension PageStyleUtil on BuildContext {
Color get pageStyleBackgroundColor {
final themeMode = Theme.of(this).brightness;
return themeMode == Brightness.light
? const Color(0xFFF5F5F8)
: const Color(0xFF303030);
}
Color get pageStyleTextColor {
final themeMode = Theme.of(this).brightness;
return themeMode == Brightness.light
? const Color(0x7F1F2225)
: Colors.white54;
}
}

View File

@ -0,0 +1,58 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class PageStyleBottomSheet extends StatelessWidget {
const PageStyleBottomSheet({
super.key,
required this.view,
});
final ViewPB view;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// cover image
FlowyText(
LocaleKeys.pageStyle_backgroundImage.tr(),
color: context.pageStyleTextColor,
fontSize: 14.0,
),
const VSpace(8.0),
PageStyleCoverImage(),
const VSpace(20.0),
// layout: font size, line height and font family.
FlowyText(
LocaleKeys.pageStyle_layout.tr(),
color: context.pageStyleTextColor,
fontSize: 14.0,
),
const VSpace(8.0),
const PageStyleLayout(),
const VSpace(20.0),
// icon
FlowyText(
LocaleKeys.pageStyle_pageIcon.tr(),
color: context.pageStyleTextColor,
fontSize: 14.0,
),
const VSpace(8.0),
PageStyleIcon(
view: view,
),
],
),
);
}
}

View File

@ -2,6 +2,7 @@ export 'actions/block_action_list.dart';
export 'actions/option_action.dart';
export 'align_toolbar_item/align_toolbar_item.dart';
export 'base/toolbar_extension.dart';
export 'bulleted_list/bulleted_list_icon.dart';
export 'callout/callout_block_component.dart';
export 'code_block/code_block_language_selector.dart';
export 'context_menu/custom_context_menu.dart';
@ -40,6 +41,7 @@ export 'mobile_toolbar_v3/more_toolbar_item.dart';
export 'mobile_toolbar_v3/toolbar_item_builder.dart';
export 'mobile_toolbar_v3/undo_redo_toolbar_item.dart';
export 'mobile_toolbar_v3/util.dart';
export 'numbered_list/numbered_list_icon.dart';
export 'openai/widgets/auto_completion_node_widget.dart';
export 'openai/widgets/smart_edit_node_widget.dart';
export 'openai/widgets/smart_edit_toolbar_item.dart';
@ -47,5 +49,6 @@ export 'outline/outline_block_component.dart';
export 'parsers/markdown_parsers.dart';
export 'table/table_menu.dart';
export 'table/table_option_action.dart';
export 'todo_list/todo_list_icon.dart';
export 'toggle/toggle_block_component.dart';
export 'toggle/toggle_block_shortcut_event.dart';

View File

@ -0,0 +1,43 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class TodoListIcon extends StatelessWidget {
const TodoListIcon({
super.key,
required this.node,
required this.onCheck,
});
final Node node;
final VoidCallback onCheck;
@override
Widget build(BuildContext context) {
final iconPadding = context.read<DocumentPageStyleBloc>().state.iconPadding;
final checked = node.attributes[TodoListBlockKeys.checked] ?? false;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
HapticFeedback.lightImpact();
onCheck();
},
child: Container(
constraints: const BoxConstraints(
minWidth: 22,
minHeight: 22,
),
margin: EdgeInsets.only(top: iconPadding, right: 8.0),
child: FlowySvg(
checked
? FlowySvgs.m_todo_list_checked_s
: FlowySvgs.m_todo_list_unchecked_s,
blendMode: checked ? null : BlendMode.srcIn,
),
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'dart:math';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
@ -37,7 +38,7 @@ class EditorStyleCustomizer {
}
static EdgeInsets get documentPadding => PlatformExtension.isMobile
? const EdgeInsets.only(left: 20, right: 20)
? const EdgeInsets.only(left: 24, right: 24)
: const EdgeInsets.only(left: 40, right: 40 + 44);
EditorStyle desktop() {
@ -91,39 +92,42 @@ class EditorStyleCustomizer {
}
EditorStyle mobile() {
final pageStyle = context.read<DocumentPageStyleBloc>().state;
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
final fontSize = pageStyle.fontLayout.fontSize;
final lineHeight = pageStyle.lineHeightLayout.lineHeight;
final fontFamily = pageStyle.fontFamily ?? builtInFontFamily();
final defaultTextDirection =
context.read<DocumentAppearanceCubit>().state.defaultTextDirection;
final baseTextStyle = this.baseTextStyle(fontFamily);
final codeFontSize = max(0.0, fontSize - 2);
return EditorStyle.mobile(
padding: padding,
defaultTextDirection: defaultTextDirection,
textStyleConfiguration: TextStyleConfiguration(
text: baseTextStyle(fontFamily).copyWith(
text: baseTextStyle.copyWith(
fontSize: fontSize,
color: theme.colorScheme.onBackground,
height: 1.5,
height: lineHeight,
),
bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith(
bold: baseTextStyle.copyWith(
fontWeight: FontWeight.w600,
),
italic: baseTextStyle(fontFamily).copyWith(
italic: baseTextStyle.copyWith(
fontStyle: FontStyle.italic,
),
underline: baseTextStyle(fontFamily).copyWith(
underline: baseTextStyle.copyWith(
decoration: TextDecoration.underline,
),
strikethrough: baseTextStyle(fontFamily).copyWith(
strikethrough: baseTextStyle.copyWith(
decoration: TextDecoration.lineThrough,
),
href: baseTextStyle(fontFamily).copyWith(
href: baseTextStyle.copyWith(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
code: GoogleFonts.robotoMono(
textStyle: baseTextStyle(fontFamily).copyWith(
textStyle: baseTextStyle.copyWith(
fontSize: codeFontSize,
fontWeight: FontWeight.normal,
fontStyle: FontStyle.italic,
@ -131,6 +135,8 @@ class EditorStyleCustomizer {
backgroundColor: Colors.grey.withOpacity(0.3),
),
),
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
),
textSpanDecorator: customizeAttributeDecorator,
mobileDragHandleBallSize: const Size.square(12.0),
@ -141,18 +147,29 @@ class EditorStyleCustomizer {
}
TextStyle headingStyleBuilder(int level) {
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontSizes = [
fontSize + 16,
fontSize + 12,
fontSize + 8,
fontSize + 4,
fontSize + 2,
fontSize,
];
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
return baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith(
fontWeight: FontWeight.w600,
final String? fontFamily;
final List<double> fontSizes;
final double fontSize;
final FontWeight fontWeight =
level <= 2 ? FontWeight.w700 : FontWeight.w600;
if (PlatformExtension.isMobile) {
final state = context.read<DocumentPageStyleBloc>().state;
fontFamily = state.fontFamily;
fontSize = state.fontLayout.fontSize;
fontSizes = state.fontLayout.headingFontSizes;
} else {
fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
fontSizes = [
fontSize + 16,
fontSize + 12,
fontSize + 8,
fontSize + 4,
fontSize + 2,
fontSize,
];
}
return baseTextStyle(fontFamily, fontWeight: fontWeight).copyWith(
fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize,
);
}
@ -173,7 +190,7 @@ class EditorStyleCustomizer {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return TextStyle(
fontFamily: builtInFontFamily,
fontFamily: builtInFontFamily(),
fontSize: fontSize,
height: 1.5,
color: theme.colorScheme.onBackground.withOpacity(0.6),
@ -211,23 +228,30 @@ class EditorStyleCustomizer {
}
TextStyle baseTextStyle(
String fontFamily, {
String? fontFamily, {
FontWeight? fontWeight,
}) {
if (fontFamily == null) {
return TextStyle(
fontWeight: fontWeight,
);
}
try {
return GoogleFonts.getFont(
fontFamily,
fontWeight: fontWeight,
);
} on Exception {
if ([builtInFontFamily, builtInCodeFontFamily].contains(fontFamily)) {
if ([builtInFontFamily(), builtInCodeFontFamily].contains(fontFamily)) {
return TextStyle(
fontFamily: fontFamily,
fontWeight: fontWeight,
);
}
return GoogleFonts.getFont(builtInFontFamily);
return TextStyle(
fontWeight: fontWeight,
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
enum HapticFeedbackType {
light,
medium,
heavy,
selection,
vibrate;
void call() {
switch (this) {
case HapticFeedbackType.light:
HapticFeedback.lightImpact();
break;
case HapticFeedbackType.medium:
HapticFeedback.mediumImpact();
break;
case HapticFeedbackType.heavy:
HapticFeedback.heavyImpact();
break;
case HapticFeedbackType.selection:
HapticFeedback.selectionClick();
break;
case HapticFeedbackType.vibrate:
HapticFeedback.vibrate();
break;
}
}
}
class FeedbackGestureDetector extends GestureDetector {
FeedbackGestureDetector({
super.key,
HitTestBehavior behavior = HitTestBehavior.opaque,
HapticFeedbackType feedbackType = HapticFeedbackType.light,
required Widget child,
required VoidCallback onTap,
}) : super(
behavior: behavior,
onTap: () {
feedbackType.call();
onTap();
},
child: child,
);
}

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
enum FlowyGradientColor {
gradient1,
gradient2,
gradient3,
gradient4,
gradient5,
gradient6,
gradient7;
static FlowyGradientColor fromId(String id) {
return FlowyGradientColor.values.firstWhere(
(element) => element.id == id,
orElse: () => FlowyGradientColor.gradient1,
);
}
String get id {
// DON'T change this name because it's saved in the database!
switch (this) {
case FlowyGradientColor.gradient1:
return 'appflowy_them_color_gradient1';
case FlowyGradientColor.gradient2:
return 'appflowy_them_color_gradient2';
case FlowyGradientColor.gradient3:
return 'appflowy_them_color_gradient3';
case FlowyGradientColor.gradient4:
return 'appflowy_them_color_gradient4';
case FlowyGradientColor.gradient5:
return 'appflowy_them_color_gradient5';
case FlowyGradientColor.gradient6:
return 'appflowy_them_color_gradient6';
case FlowyGradientColor.gradient7:
return 'appflowy_them_color_gradient7';
}
}
LinearGradient get linear {
switch (this) {
case FlowyGradientColor.gradient1:
return const LinearGradient(
begin: Alignment(-0.35, -0.94),
end: Alignment(0.35, 0.94),
colors: [Color(0xFF34BDAF), Color(0xFFB682D4)],
);
case FlowyGradientColor.gradient2:
return const LinearGradient(
begin: Alignment(0.00, -1.00),
end: Alignment(0, 1),
colors: [Color(0xFF4CC2CC), Color(0xFFE17570)],
);
case FlowyGradientColor.gradient3:
return const LinearGradient(
begin: Alignment(0.00, -1.00),
end: Alignment(0, 1),
colors: [Color(0xFFAF70E0), Color(0xFFED7196)],
);
case FlowyGradientColor.gradient4:
return const LinearGradient(
begin: Alignment(0.00, -1.00),
end: Alignment(0, 1),
colors: [Color(0xFFA348D6), Color(0xFF44A7DE)],
);
case FlowyGradientColor.gradient5:
return const LinearGradient(
begin: Alignment(0.38, -0.93),
end: Alignment(-0.38, 0.93),
colors: [Color(0xFF5749C9), Color(0xFFBB4997)],
);
case FlowyGradientColor.gradient6:
return const LinearGradient(
begin: Alignment(0.00, -1.00),
end: Alignment(0, 1),
colors: [Color(0xFF036FFA), Color(0xFF00B8E5)],
);
case FlowyGradientColor.gradient7:
return const LinearGradient(
begin: Alignment(0.62, -0.79),
end: Alignment(-0.62, 0.79),
colors: [Color(0xFFF0C6CF), Color(0xFFDECCE2), Color(0xFFCAD3F9)],
);
}
}
}

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';

View File

@ -1,11 +1,28 @@
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
const builtInFontFamily = 'Poppins';
String builtInFontFamily() {
if (PlatformExtension.isDesktopOrWeb) {
return 'Poppins';
}
if (Platform.isIOS) {
return 'San Francisco';
}
if (Platform.isAndroid) {
return 'Roboto';
}
return 'Roboto';
}
// 'Poppins';
const builtInCodeFontFamily = 'RobotoMono';
abstract class BaseAppearance {
@ -35,13 +52,13 @@ abstract class BaseAppearance {
fontSize: fontSize,
color: fontColor,
fontWeight: fontWeight,
fontFamilyFallback: const [builtInFontFamily],
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 == builtInFontFamily()) {
return textStyle;
}

View File

@ -20,7 +20,6 @@ class MobileAppearance extends BaseAppearance {
String fontFamily,
String codeFontFamily,
) {
assert(fontFamily.isNotEmpty);
assert(codeFontFamily.isNotEmpty);
final fontStyle = getFontStyle(
@ -92,6 +91,7 @@ class MobileAppearance extends BaseAppearance {
disabledColor: colorTheme.outline,
scaffoldBackgroundColor: colorTheme.background,
appBarTheme: AppBarTheme(
toolbarHeight: 44.0,
foregroundColor: colorTheme.onBackground,
backgroundColor: colorTheme.background,
centerTitle: false,

View File

@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/recent/cached_recent_service.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/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
@ -215,6 +216,12 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
value.isPublic,
);
},
updateIcon: (value) async {
await ViewBackendService.updateViewIcon(
viewId: view.id,
viewIcon: value.icon ?? '',
);
},
);
},
);
@ -376,8 +383,11 @@ class ViewEvent with _$ViewEvent {
) = ViewDidUpdate;
const factory ViewEvent.viewUpdateChildView(ViewPB result) =
ViewUpdateChildView;
const factory ViewEvent.updateViewVisibility(ViewPB view, bool isPublic) =
UpdateViewVisibility;
const factory ViewEvent.updateViewVisibility(
ViewPB view,
bool isPublic,
) = UpdateViewVisibility;
const factory ViewEvent.updateIcon(String? icon) = UpdateIcon;
}
@freezed

View File

@ -1,4 +1,7 @@
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
@ -15,6 +18,22 @@ class PluginArgumentKeys {
static String rowId = "row_id";
}
class ViewExtKeys {
// used for customizing the font family.
static String fontKey = 'font';
// used for customizing the font layout.
static String fontLayoutKey = 'font_layout';
// used for customizing the line height layout.
static String lineHeightLayoutKey = 'line_height_layout';
// cover keys
static String coverKey = 'cover';
static String coverTypeKey = 'type';
static String coverValueKey = 'value';
}
extension ViewExtension on ViewPB {
Widget defaultIcon() => FlowySvg(
switch (layout) {
@ -76,6 +95,51 @@ extension ViewExtension on ViewPB {
};
FlowySvgData get iconData => layout.icon;
PageStyleCover? get cover {
if (layout != ViewLayoutPB.Document) {
return null;
}
try {
final ext = jsonDecode(extra);
final cover = ext[ViewExtKeys.coverKey] ?? {};
final coverType = cover[ViewExtKeys.coverTypeKey] ??
PageStyleCoverImageType.none.toString();
final coverValue = cover[ViewExtKeys.coverValueKey] ?? '';
return PageStyleCover(
type: PageStyleCoverImageType.fromString(coverType),
value: coverValue,
);
} catch (e) {
return null;
}
}
PageStyleLineHeightLayout get lineHeightLayout {
if (layout != ViewLayoutPB.Document) {
return PageStyleLineHeightLayout.normal;
}
try {
final ext = jsonDecode(extra);
final lineHeight = ext[ViewExtKeys.lineHeightLayoutKey];
return PageStyleLineHeightLayout.fromString(lineHeight);
} catch (e) {
return PageStyleLineHeightLayout.normal;
}
}
PageStyleFontLayout get fontLayout {
if (layout != ViewLayoutPB.Document) {
return PageStyleFontLayout.normal;
}
try {
final ext = jsonDecode(extra);
final fontLayout = ext[ViewExtKeys.fontLayoutKey];
return PageStyleFontLayout.fromString(fontLayout);
} catch (e) {
return PageStyleFontLayout.normal;
}
}
}
extension ViewLayoutExtension on ViewLayoutPB {

View File

@ -148,6 +148,7 @@ class ViewBackendService {
required String viewId,
String? name,
bool? isFavorite,
String? extra,
}) {
final payload = UpdateViewPayloadPB.create()..viewId = viewId;
@ -159,6 +160,10 @@ class ViewBackendService {
payload.isFavorite = isFavorite;
}
if (extra != null) {
payload.extra = extra;
}
return FolderEventUpdateView(payload).send();
}

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';

View File

@ -182,6 +182,13 @@ enum FlowyTint {
}
}
static FlowyTint fromId(String id) {
return FlowyTint.values.firstWhere(
(element) => element.id == id,
orElse: () => FlowyTint.tint1,
);
}
Color color(BuildContext context) {
switch (this) {
case FlowyTint.tint1:

View File

@ -1210,7 +1210,7 @@ packages:
source: hosted
version: "2.0.2"
numerus:
dependency: transitive
dependency: "direct main"
description:
name: numerus
sha256: "49cd96fe774dd1f574fc9117ed67e8a2b06a612f723e87ef3119456a7729d837"

View File

@ -131,6 +131,7 @@ dependencies:
sheet:
file: ^7.0.0
avatar_stack: ^1.2.0
numerus: ^2.1.2
flutter_animate: ^4.5.0
dev_dependencies:
@ -236,6 +237,7 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
- assets/images/built_in_cover_images/
- assets/flowy_icons/
- assets/flowy_icons/16x/
- assets/flowy_icons/24x/

View File

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

View File

@ -1,8 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -28,7 +27,7 @@ void main() {
test('Initial state', () {
expect(cubit.state.fontSize, 16.0);
expect(cubit.state.fontFamily, builtInFontFamily);
expect(cubit.state.fontFamily, builtInFontFamily());
});
test('Fetch document appearance from SharedPreferences', () async {

View File

@ -1,10 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:mocktail/mocktail.dart';
class MockDocumentAppearanceCubit extends Mock
@ -34,14 +31,14 @@ void main() {
});
test(
'baseTextStyle should return the default TextStyle when an exception occurs',
'baseTextStyle should return the null TextStyle when an exception occurs',
() {
const garbage = 'Garbage';
final result = editorStyleCustomizer.baseTextStyle(garbage);
expect(result, isA<TextStyle>());
expect(
result.fontFamily,
GoogleFonts.getFont(builtInFontFamily).fontFamily,
null,
);
});
});

View File

@ -1,10 +1,9 @@
import 'package:flutter/material.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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -57,9 +56,9 @@ void main() {
value: documentAppearanceCubit,
),
],
child: const Scaffold(
child: Scaffold(
body: ThemeFontFamilySetting(
currentFontFamily: builtInFontFamily,
currentFontFamily: builtInFontFamily(),
),
),
),
@ -72,7 +71,7 @@ void main() {
await tester.pumpAndSettle();
// Verify the initial font family
expect(find.text(builtInFontFamily), findsAtLeastNWidgets(1));
expect(find.text(builtInFontFamily()), findsAtLeastNWidgets(1));
when(() => appearanceSettingsCubit.setFontFamily(any<String>()))
.thenAnswer((_) async {});
verifyNever(() => appearanceSettingsCubit.setFontFamily(any<String>()));

View File

@ -860,7 +860,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"async-trait",
@ -884,7 +884,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"async-trait",
@ -914,7 +914,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"collab",
@ -933,7 +933,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"bytes",
@ -948,7 +948,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"chrono",
@ -986,7 +986,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"async-stream",
@ -1064,7 +1064,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"collab",

View File

@ -97,10 +97,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "e2f
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }

View File

@ -65,10 +65,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "e2f
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }

View File

@ -843,7 +843,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"async-trait",
@ -867,7 +867,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"async-trait",
@ -897,7 +897,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"collab",
@ -916,7 +916,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"bytes",
@ -931,7 +931,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"chrono",
@ -969,7 +969,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"async-stream",
@ -1047,7 +1047,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=85580a5c0e95b5dae4787336faa751da44365760#85580a5c0e95b5dae4787336faa751da44365760"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dda9ad4#dda9ad49b78ef4e2cae7b5cbe41806b982ca6a44"
dependencies = [
"anyhow",
"collab",

View File

@ -96,10 +96,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "e2f
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "85580a5c0e95b5dae4787336faa751da44365760" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dda9ad4" }

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="2.75" fill="#454545"/>
</svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="2.75" stroke="#454545"/>
</svg>

After

Width:  |  Height:  |  Size: 153 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="5" height="5" transform="translate(6.5 6.5)" fill="#454545"/>
</svg>

After

Width:  |  Height:  |  Size: 178 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5998 10.45C3.7473 10.45 3.0498 11.1475 3.0498 12C3.0498 12.8525 3.7473 13.55 4.5998 13.55C5.4523 13.55 6.1498 12.8525 6.1498 12C6.1498 11.1475 5.4523 10.45 4.5998 10.45Z" fill="#171717"/>
<path d="M12.0002 10.45C11.1477 10.45 10.4502 11.1475 10.4502 12C10.4502 12.8525 11.1477 13.55 12.0002 13.55C12.8527 13.55 13.5502 12.8525 13.5502 12C13.5502 11.1475 12.8527 10.45 12.0002 10.45Z" fill="#171717"/>
<path d="M19.3996 10.45C18.5471 10.45 17.8496 11.1475 17.8496 12C17.8496 12.8525 18.5471 13.55 19.3996 13.55C20.2521 13.55 20.9496 12.8525 20.9496 12C20.9496 11.1475 20.2521 10.45 19.3996 10.45Z" fill="#171717"/>
</svg>

After

Width:  |  Height:  |  Size: 729 B

View File

@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.2375 12.2143L16.8827 18.4834C17.0646 18.9145 17.5591 19.1154 17.9871 18.9322C18.4151 18.749 18.6146 18.251 18.4327 17.8198L11.6625 1.77463C11.2267 0.741795 9.77331 0.741784 9.33751 1.77463L2.56731 17.8198C2.3854 18.251 2.58491 18.749 3.01293 18.9322C3.44094 19.1154 3.93539 18.9145 4.11729 18.4834L6.76248 12.2143H14.2375ZM13.5218 10.518H7.47824L10.5 3.35651L13.5218 10.518Z" fill="#1E2022"/>
</svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5367 9.84687L13.6859 14.7924C13.8337 15.1325 14.2355 15.2911 14.5832 15.1465C14.931 15.002 15.0931 14.6091 14.9453 14.269L9.44452 1.6111C9.09044 0.796305 7.90957 0.796296 7.55548 1.6111L2.05469 14.269C1.90689 14.6091 2.06899 15.002 2.41675 15.1465C2.76452 15.2911 3.16625 15.1325 3.31405 14.7924L5.46326 9.84687H11.5367ZM10.9552 8.50864H6.04482L8.5 2.85902L10.9552 8.50864Z" fill="#1E2022"/>
</svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@ -0,0 +1,3 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.71934 7.5L10.29 11.1956C10.398 11.4497 10.6916 11.5682 10.9457 11.4602C11.1998 11.3522 11.3183 11.0586 11.2103 10.8044L7.19038 1.3458C6.93161 0.736949 6.06865 0.736942 5.80988 1.3458L1.78996 10.8044C1.68195 11.0586 1.80042 11.3522 2.05456 11.4602C2.3087 11.5682 2.60228 11.4497 2.71029 11.1956L4.28091 7.5H8.71934ZM8.29434 6.5H4.70591L6.50013 2.27832L8.29434 6.5Z" fill="#1E2022"/>
</svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="magic-star">
<g id="Group">
<path id="Vector" d="M16.8019 4.94194L16.7382 8.38587C16.7292 8.85839 17.0291 9.4854 17.4107 9.7671L19.6643 11.4754C21.1091 12.5659 20.8728 13.9016 19.1463 14.4469L16.2112 15.3646C15.7205 15.5191 15.2026 16.0553 15.0754 16.555L14.3757 19.2266C13.8214 21.3347 12.4401 21.5438 11.2952 19.69L9.69587 17.1002C9.40508 16.6277 8.71448 16.2733 8.16927 16.3006L5.13426 16.4551C2.96248 16.5641 2.34456 15.3101 3.76213 13.6563L5.56132 11.5663C5.89754 11.1756 6.05201 10.4486 5.89753 9.95792L4.9798 7.02283C4.44367 5.29631 5.40687 4.34219 7.1243 4.90558L9.80496 5.78702C10.2593 5.93241 10.9408 5.83244 11.3225 5.55075L14.1213 3.53344C15.6297 2.44301 16.8382 3.07911 16.8019 4.94194Z" stroke="#171717" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M20.9994 21.1711L18.2461 18.4177" stroke="#171717" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,8 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 4H20" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 6.10986V12.1099" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.3299 7.83L12.4999 5L9.66992 7.83" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 18.1099V12.1099" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.67008 16.3897L12.5001 19.2197L15.3301 16.3897" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 20.22H20" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

View File

@ -0,0 +1,5 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 6H20" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 6V18" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 18H20" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@ -0,0 +1,8 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4199 6.94531V2.44531" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.75 5.25518L12.5 8.05518L15.25 5.25518" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 10.5552H20" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 13.5552H20" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5801 17.165V21.665" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.25 18.8552L12.5 16.0552L9.75 18.8552" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -0,0 +1,8 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_left_outlined">
<g id="Union">
<path d="M3.85355 10.8536C3.65829 10.6583 3.65829 10.3417 3.85355 10.1464L8 6L3.85355 1.85355C3.65829 1.65829 3.65829 1.34171 3.85355 1.14645C4.04882 0.951184 4.3654 0.951184 4.56066 1.14645L8.70711 5.29289C9.09763 5.68342 9.09763 6.31658 8.70711 6.70711L4.56066 10.8536C4.3654 11.0488 4.04882 11.0488 3.85355 10.8536Z" fill="#1E2022" fill-opacity="0.5"/>
<path d="M3.78284 10.0757C3.54853 10.3101 3.54853 10.6899 3.78284 10.9243C4.01716 11.1586 4.39706 11.1586 4.63137 10.9243L8.77782 6.77782C9.2074 6.34824 9.20739 5.65176 8.77782 5.22218L4.63137 1.07574C4.39706 0.841421 4.01716 0.841421 3.78284 1.07574C3.54853 1.31005 3.54853 1.68995 3.78284 1.92426L7.85858 6L3.78284 10.0757Z" stroke="#1E2022" stroke-opacity="0.5" stroke-width="0.2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 945 B

View File

@ -0,0 +1,6 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_todo_outlined">
<path id="Vector" d="M8.3 20H13.7C18.2 20 20 18.2 20 13.7V8.3C20 3.8 18.2 2 13.7 2H8.3C3.8 2 2 3.8 2 8.3V13.7C2 18.2 3.8 20 8.3 20Z" fill="#00BCF0"/>
<path id="Vector_2" d="M7.25 11L9.94231 13.5L14.75 8.5" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 429 B

View File

@ -0,0 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_todo_outlined" opacity="0.4">
<path id="Vector" d="M8.45666 19.4793H13.5442C17.7837 19.4793 19.4796 17.7835 19.4796 13.5439V8.45641C19.4796 4.21683 17.7837 2.521 13.5442 2.521H8.45666C4.21707 2.521 2.52124 4.21683 2.52124 8.45641V13.5439C2.52124 17.7835 4.21707 19.4793 8.45666 19.4793Z" stroke="#141618" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 495 B

View File

@ -0,0 +1,5 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 24H17.5C22.5 24 24.5 22 24.5 17V11C24.5 6 22.5 4 17.5 4H11.5C6.5 4 4.5 6 4.5 11V17C4.5 22 6.5 24 11.5 24Z" stroke="#1E2022" stroke-width="1.39709" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.168 12C19.2725 12 20.168 11.1046 20.168 10C20.168 8.89543 19.2725 8 18.168 8C17.0634 8 16.168 8.89543 16.168 10C16.168 11.1046 17.0634 12 18.168 12Z" stroke="#1E2022" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23.9443 20.3968L19.0143 17.0868C18.2243 16.5568 17.0843 16.6168 16.3743 17.2268L16.0443 17.5168C15.2643 18.1868 14.0043 18.1868 13.2243 17.5168L9.06434 13.9468C8.28434 13.2768 7.02434 13.2768 6.24434 13.9468L4.61434 15.3468" stroke="#1E2022" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 882 B

Some files were not shown because too many files have changed in this diff Show More