feat: immersive page style on mobile (#5135)
After Width: | Height: | Size: 2.2 MiB |
After Width: | Height: | Size: 731 KiB |
After Width: | Height: | Size: 465 KiB |
After Width: | Height: | Size: 526 KiB |
After Width: | Height: | Size: 293 KiB |
After Width: | Height: | Size: 765 KiB |
@ -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('⭐️'),
|
||||
),
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
@ -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();
|
||||
final cover = getCoverV2();
|
||||
if (cover != null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
name: view.name,
|
||||
icon: view.icon.value,
|
||||
coverType: coverType,
|
||||
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;
|
||||
|
||||
|
@ -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(
|
@ -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,
|
@ -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,13 +154,15 @@ 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;
|
||||
return Scaffold(
|
||||
appBar: FlowyAppBar(
|
||||
title: Row(
|
||||
final title = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null && icon.isNotEmpty)
|
||||
@ -153,15 +178,85 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (isImmersive) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size(
|
||||
double.infinity,
|
||||
AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight,
|
||||
),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _appBarOpacity,
|
||||
builder: (_, opacity, __) => FlowyAppBar(
|
||||
backgroundColor:
|
||||
AppBarTheme.of(context).backgroundColor?.withOpacity(opacity),
|
||||
showDivider: false,
|
||||
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
|
||||
actions: actions,
|
||||
),
|
||||
body: SafeArea(child: child),
|
||||
),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
_rebuildScrollNotificationObserver(context);
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: FlowyAppBar(
|
||||
title: title,
|
||||
actions: actions,
|
||||
),
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,16 +122,22 @@ 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,
|
||||
scrollableWidgetBuilder?.call(
|
||||
context,
|
||||
scrollController,
|
||||
) ??
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
@ -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,
|
||||
|
@ -66,6 +66,7 @@ Future<T?> showTransitionMobileBottomSheet<T>(
|
||||
showCloseButton: showCloseButton,
|
||||
showBackButton: showBackButton,
|
||||
showDoneButton: showDoneButton,
|
||||
showRemoveButton: false,
|
||||
title: title,
|
||||
),
|
||||
if (showDivider)
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
@ -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,11 +56,74 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Scrollbar(
|
||||
child: ListView.builder(
|
||||
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,
|
||||
@ -66,7 +134,9 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
|
||||
setState(() {
|
||||
availableFonts = _availableFonts
|
||||
.where(
|
||||
(font) => font
|
||||
(font) =>
|
||||
font.isEmpty || // keep the default one always
|
||||
font
|
||||
.parseFontFamilyName()
|
||||
.toLowerCase()
|
||||
.contains(keyword.toLowerCase()),
|
||||
@ -77,21 +147,4 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
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.editorState!),
|
||||
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,9 +197,11 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
} else if (type == EditorNotificationType.redo) {
|
||||
redoCommand.execute(editorState);
|
||||
} else if (type == EditorNotificationType.exitEditing) {
|
||||
if (editorState.selection != null) {
|
||||
editorState.selection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onNotificationAction(
|
||||
BuildContext context,
|
||||
|
@ -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()],
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
],
|
||||
|
@ -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';
|
||||
|
@ -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,26 +66,13 @@ 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,
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: FlowyMobileSearchTextField(
|
||||
onChanged: (keyword) => query = keyword,
|
||||
onSubmitted: (_) => _search(),
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.search_label.tr(),
|
||||
),
|
||||
onTap: _search,
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(12.0),
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
@ -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,20 +112,73 @@ 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(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHalfScreenImage(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
@ -150,7 +193,32 @@ class _UnsplashImage extends StatelessWidget {
|
||||
fontSize: 10.0,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFullScreenImage(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Image.network(
|
||||
photo.urls.thumb.toString(),
|
||||
fit: BoxFit.cover,
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxHeight,
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
child: FlowyText.medium(
|
||||
photo.name,
|
||||
fontSize: 10.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,8 +147,20 @@ class EditorStyleCustomizer {
|
||||
}
|
||||
|
||||
TextStyle headingStyleBuilder(int level) {
|
||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||
final fontSizes = [
|
||||
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,
|
||||
@ -150,9 +168,8 @@ class EditorStyleCustomizer {
|
||||
fontSize + 2,
|
||||
fontSize,
|
||||
];
|
||||
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
|
||||
return baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
@ -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)],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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:
|
||||
|
@ -1210,7 +1210,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
numerus:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: numerus
|
||||
sha256: "49cd96fe774dd1f574fc9117ed67e8a2b06a612f723e87ef3119456a7729d837"
|
||||
|
@ -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/
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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>()));
|
||||
|
14
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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" }
|
||||
|
14
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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 |
@ -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 |
@ -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 |
5
frontend/resources/flowy_icons/16x/m_app_bar_more.svg
Normal 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 |
3
frontend/resources/flowy_icons/16x/m_font_size_large.svg
Normal 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 |
@ -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 |
3
frontend/resources/flowy_icons/16x/m_font_size_small.svg
Normal 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 |
8
frontend/resources/flowy_icons/16x/m_layout.svg
Normal 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 |
8
frontend/resources/flowy_icons/16x/m_layout_large.svg
Normal 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 |
5
frontend/resources/flowy_icons/16x/m_layout_normal.svg
Normal 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 |
8
frontend/resources/flowy_icons/16x/m_layout_small.svg
Normal 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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |