Merge branch 'upstream-main' into feat/tauri-kanban

# Conflicts:
#	frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts
#	frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts
#	frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx
#	frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx
This commit is contained in:
ascarbek 2023-03-15 11:34:21 +06:00
commit 6e028c4f64
44 changed files with 1461 additions and 345 deletions

View File

@ -118,11 +118,11 @@ jobs:
fi
shell: bash
- uses: codecov/codecov-action@v3
with:
name: appflowy
flags: appflowy
env_vars: ${{ matrix.os }}
fail_ci_if_error: true
verbose: true
# - uses: codecov/codecov-action@v3
# with:
# name: appflowy
# flags: appflowy
# env_vars: ${{ matrix.os }}
# fail_ci_if_error: true
# verbose: true

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 15V6.9C3.5 6.05992 3.5 5.63988 3.66349 5.31901C3.8073 5.03677 4.03677 4.8073 4.31901 4.66349C4.63988 4.5 5.05992 4.5 5.9 4.5H8.47237C8.84808 4.5 9.03594 4.5 9.20646 4.55179C9.35741 4.59763 9.49785 4.6728 9.61972 4.77298C9.75739 4.88614 9.86159 5.04245 10.07 5.35507L10.93 6.64533C11.1384 6.95795 11.2426 7.11426 11.3803 7.22742C11.5022 7.3276 11.6426 7.40277 11.7935 7.44861C11.9641 7.5004 12.1519 7.5004 12.5276 7.5004H16.5004C16.965 7.5004 17.1973 7.5004 17.3879 7.55143C17.9058 7.69008 18.3103 8.09459 18.449 8.61248C18.5 8.80308 18.5 9.03539 18.5 9.5V9.5M10.5 13.5H16.5" stroke="#222222" stroke-linecap="round"/>
<path d="M4.5 18.5L16.7701 18.5004C17.3922 18.5004 17.7032 18.5004 17.9679 18.3963C18.2016 18.3044 18.4084 18.1553 18.5695 17.9626C18.752 17.7445 18.8503 17.4494 19.047 16.8593L20.4471 12.6592C20.8026 11.5927 20.9803 11.0595 20.8737 10.635C20.7804 10.2635 20.5485 9.94171 20.2255 9.7357C19.8566 9.50035 19.2945 9.50033 18.1703 9.5003L10.2299 9.50005C9.60784 9.50003 9.29681 9.50002 9.03216 9.60411C8.79846 9.69601 8.59157 9.84513 8.43047 10.0378C8.24804 10.2559 8.14968 10.551 7.95298 11.1411L5.7649 17.7057C5.60671 18.1803 5.16255 18.5004 4.66227 18.5004V18.5004C4.02037 18.5004 3.5 17.98 3.5 17.3381V14.5" stroke="#222222"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -344,23 +344,38 @@
"referencedGrid": "Referenced Grid",
"autoCompletionMenuItemName": "Auto Completion",
"autoGeneratorMenuItemName": "Auto Generator",
"autoGeneratorTitleName": "Open AI: Auto Generator",
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
"autoGeneratorLearnMore": "Learn more",
"autoGeneratorGenerate": "Generate",
"autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
"smartEditTitleName": "Open AI: Smart Edit",
"smartEdit": "Smart Edit",
"smartEditTitleName": "OpenAI: Smart Edit",
"smartEditFixSpelling": "Fix spelling",
"smartEditSummarize": "Summarize",
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings",
"cover": {
"changeCover": "Change Cover",
"colors": "Colors",
"images": "Images",
"abstract": "Abstract",
"addCover": "Add Cover",
"addLocalImage": "Add local image"
"addLocalImage": "Add local image",
"invalidImageUrl": "Invalid image URL",
"failedToAddImageToGallery": "Failed to add image to gallery",
"enterImageUrl": "Enter image URL",
"add": "Add",
"back": "Back",
"saveToGallery": "Save to gallery",
"removeIcon": "Remove Icon",
"pasteImageUrl": "Paste image URL",
"or": "OR",
"pickFromFiles": "Pick from files",
"couldNotFetchImage": "Could not fetch image",
"imageSavingFailed": "Image Saving Failed",
"addIcon": "Add Icon"
}
}
},

View File

@ -12,7 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:collection/collection.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'database_service.dart';
import 'database_view_service.dart';
import 'defines.dart';
import 'layout/layout_setting_listener.dart';
import 'row/row_cache.dart';
@ -67,7 +67,7 @@ class DatabaseCallbacks {
class DatabaseController {
final String viewId;
final DatabaseBackendService _databaseBackendSvc;
final DatabaseViewBackendService _databaseViewBackendSvc;
final FieldController fieldController;
late DatabaseViewCache _viewCache;
final LayoutTypePB layoutType;
@ -87,7 +87,7 @@ class DatabaseController {
DatabaseController({required ViewPB view, required this.layoutType})
: viewId = view.id,
_databaseBackendSvc = DatabaseBackendService(viewId: view.id),
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
fieldController = FieldController(viewId: view.id),
groupListener = DatabaseGroupListener(view.id),
layoutListener = DatabaseLayoutListener(view.id) {
@ -112,7 +112,7 @@ class DatabaseController {
}
Future<Either<Unit, FlowyError>> open() async {
return _databaseBackendSvc.openGrid().then((result) {
return _databaseViewBackendSvc.openGrid().then((result) {
return result.fold(
(database) async {
_databaseCallbacks?.onDatabaseChanged?.call(database);
@ -152,7 +152,7 @@ class DatabaseController {
cellDataByFieldId = rowBuilder.build();
}
return _databaseBackendSvc.createRow(
return _databaseViewBackendSvc.createRow(
startRowId: startRowId,
groupId: groupId,
cellDataByFieldId: cellDataByFieldId,
@ -161,7 +161,7 @@ class DatabaseController {
Future<Either<Unit, FlowyError>> moveRow(RowPB fromRow,
{RowPB? toRow, String? groupId}) {
return _databaseBackendSvc.moveRow(
return _databaseViewBackendSvc.moveRow(
fromRowId: fromRow.id,
toGroupId: groupId,
toRowId: toRow?.id,
@ -170,7 +170,7 @@ class DatabaseController {
Future<Either<Unit, FlowyError>> moveGroup(
{required String fromGroupId, required String toGroupId}) {
return _databaseBackendSvc.moveGroup(
return _databaseViewBackendSvc.moveGroup(
fromGroupId: fromGroupId,
toGroupId: toGroupId,
);
@ -178,7 +178,7 @@ class DatabaseController {
Future<void> updateCalenderLayoutSetting(
CalendarLayoutSettingsPB layoutSetting) async {
await _databaseBackendSvc
await _databaseViewBackendSvc
.updateLayoutSetting(calendarLayoutSetting: layoutSetting)
.then((result) {
result.fold((l) => null, (r) => Log.error(r));
@ -186,13 +186,13 @@ class DatabaseController {
}
Future<void> dispose() async {
await _databaseBackendSvc.closeView();
await _databaseViewBackendSvc.closeView();
await fieldController.dispose();
await groupListener.stop();
}
Future<void> _loadGroups() async {
final result = await _databaseBackendSvc.loadGroups();
final result = await _databaseViewBackendSvc.loadGroups();
return Future(
() => result.fold(
(groups) {
@ -204,7 +204,7 @@ class DatabaseController {
}
Future<void> _loadLayoutSetting() async {
_databaseBackendSvc.getLayoutSetting(layoutType).then((result) {
_databaseViewBackendSvc.getLayoutSetting(layoutType).then((result) {
result.fold(
(l) {
_layoutCallbacks?.onLoadLayout(l);

View File

@ -1,121 +1,13 @@
import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
import 'package:dartz/dartz.dart';
class DatabaseBackendService {
final String viewId;
DatabaseBackendService({
required this.viewId,
});
Future<Either<DatabasePB, FlowyError>> openGrid() async {
await FolderEventSetLatestView(ViewIdPB(value: viewId)).send();
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetDatabase(payload).send();
}
Future<Either<RowPB, FlowyError>> createRow({
String? startRowId,
String? groupId,
Map<String, String>? cellDataByFieldId,
}) {
var payload = CreateRowPayloadPB.create()..viewId = viewId;
if (startRowId != null) {
payload.startRowId = startRowId;
}
if (groupId != null) {
payload.groupId = groupId;
}
if (cellDataByFieldId != null && cellDataByFieldId.isNotEmpty) {
payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId);
}
return DatabaseEventCreateRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveRow({
required String fromRowId,
required String? toGroupId,
required String? toRowId,
}) {
var payload = MoveGroupRowPayloadPB.create()
..viewId = viewId
..fromRowId = fromRowId;
if (toGroupId != null) {
payload.toGroupId = toGroupId;
}
if (toRowId != null) {
payload.toRowId = toRowId;
}
return DatabaseEventMoveGroupRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveGroup({
required String fromGroupId,
required String toGroupId,
}) {
final payload = MoveGroupPayloadPB.create()
..viewId = viewId
..fromGroupId = fromGroupId
..toGroupId = toGroupId;
return DatabaseEventMoveGroup(payload).send();
}
Future<Either<List<FieldPB>, FlowyError>> getFields(
{List<FieldIdPB>? fieldIds}) {
var payload = GetFieldPayloadPB.create()..viewId = viewId;
if (fieldIds != null) {
payload.fieldIds = RepeatedFieldIdPB(items: fieldIds);
}
return DatabaseEventGetFields(payload).send().then((result) {
static Future<Either<List<DatabaseDescriptionPB>, FlowyError>>
getAllDatabase() {
return DatabaseEventGetDatabases().send().then((result) {
return result.fold((l) => left(l.items), (r) => right(r));
});
}
Future<Either<LayoutSettingPB, FlowyError>> getLayoutSetting(
LayoutTypePB layoutType) {
final payload = DatabaseLayoutIdPB.create()
..viewId = viewId
..layout = layoutType;
return DatabaseEventGetLayoutSetting(payload).send();
}
Future<Either<Unit, FlowyError>> updateLayoutSetting(
{CalendarLayoutSettingsPB? calendarLayoutSetting}) {
final layoutSetting = LayoutSettingPB.create();
if (calendarLayoutSetting != null) {
layoutSetting.calendar = calendarLayoutSetting;
}
final payload = UpdateLayoutSettingPB.create()
..viewId = viewId
..layoutSetting = layoutSetting;
return DatabaseEventSetLayoutSetting(payload).send();
}
Future<Either<Unit, FlowyError>> closeView() {
final request = ViewIdPB(value: viewId);
return FolderEventCloseView(request).send();
}
Future<Either<RepeatedGroupPB, FlowyError>> loadGroups() {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetGroups(payload).send();
}
}

View File

@ -0,0 +1,121 @@
import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
class DatabaseViewBackendService {
final String viewId;
DatabaseViewBackendService({
required this.viewId,
});
Future<Either<DatabasePB, FlowyError>> openGrid() async {
await FolderEventSetLatestView(ViewIdPB(value: viewId)).send();
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetDatabase(payload).send();
}
Future<Either<RowPB, FlowyError>> createRow({
String? startRowId,
String? groupId,
Map<String, String>? cellDataByFieldId,
}) {
var payload = CreateRowPayloadPB.create()..viewId = viewId;
if (startRowId != null) {
payload.startRowId = startRowId;
}
if (groupId != null) {
payload.groupId = groupId;
}
if (cellDataByFieldId != null && cellDataByFieldId.isNotEmpty) {
payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId);
}
return DatabaseEventCreateRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveRow({
required String fromRowId,
required String? toGroupId,
required String? toRowId,
}) {
var payload = MoveGroupRowPayloadPB.create()
..viewId = viewId
..fromRowId = fromRowId;
if (toGroupId != null) {
payload.toGroupId = toGroupId;
}
if (toRowId != null) {
payload.toRowId = toRowId;
}
return DatabaseEventMoveGroupRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveGroup({
required String fromGroupId,
required String toGroupId,
}) {
final payload = MoveGroupPayloadPB.create()
..viewId = viewId
..fromGroupId = fromGroupId
..toGroupId = toGroupId;
return DatabaseEventMoveGroup(payload).send();
}
Future<Either<List<FieldPB>, FlowyError>> getFields(
{List<FieldIdPB>? fieldIds}) {
var payload = GetFieldPayloadPB.create()..viewId = viewId;
if (fieldIds != null) {
payload.fieldIds = RepeatedFieldIdPB(items: fieldIds);
}
return DatabaseEventGetFields(payload).send().then((result) {
return result.fold((l) => left(l.items), (r) => right(r));
});
}
Future<Either<LayoutSettingPB, FlowyError>> getLayoutSetting(
LayoutTypePB layoutType) {
final payload = DatabaseLayoutIdPB.create()
..viewId = viewId
..layout = layoutType;
return DatabaseEventGetLayoutSetting(payload).send();
}
Future<Either<Unit, FlowyError>> updateLayoutSetting(
{CalendarLayoutSettingsPB? calendarLayoutSetting}) {
final layoutSetting = LayoutSettingPB.create();
if (calendarLayoutSetting != null) {
layoutSetting.calendar = calendarLayoutSetting;
}
final payload = UpdateLayoutSettingPB.create()
..viewId = viewId
..layoutSetting = layoutSetting;
return DatabaseEventSetLayoutSetting(payload).send();
}
Future<Either<Unit, FlowyError>> closeView() {
final request = ViewIdPB(value: viewId);
return FolderEventCloseView(request).send();
}
Future<Either<RepeatedGroupPB, FlowyError>> loadGroups() {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetGroups(payload).send();
}
}

View File

@ -11,7 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart';
import 'package:flutter/foundation.dart';
import '../../grid/presentation/widgets/filter/filter_info.dart';
import '../../grid/presentation/widgets/sort/sort_info.dart';
import '../database_service.dart';
import '../database_view_service.dart';
import '../filter/filter_listener.dart';
import '../filter/filter_service.dart';
import '../row/row_cache.dart';
@ -80,7 +80,7 @@ class FieldController {
final SortsListener _sortsListener;
// FFI services
final DatabaseBackendService _databaseBackendSvc;
final DatabaseViewBackendService _databaseViewBackendSvc;
final SettingBackendService _settingBackendSvc;
final FilterBackendService _filterBackendSvc;
final SortBackendService _sortBackendSvc;
@ -152,7 +152,7 @@ class FieldController {
_settingListener = DatabaseSettingListener(viewId: viewId),
_filterBackendSvc = FilterBackendService(viewId: viewId),
_filtersListener = FiltersListener(viewId: viewId),
_databaseBackendSvc = DatabaseBackendService(viewId: viewId),
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: viewId),
_sortBackendSvc = SortBackendService(viewId: viewId),
_sortsListener = SortsListener(viewId: viewId),
_settingBackendSvc = SettingBackendService(viewId: viewId) {
@ -448,7 +448,7 @@ class FieldController {
Future<Either<Unit, FlowyError>> loadFields({
required List<FieldIdPB> fieldIds,
}) async {
final result = await _databaseBackendSvc.getFields(fieldIds: fieldIds);
final result = await _databaseViewBackendSvc.getFields(fieldIds: fieldIds);
return Future(
() => result.fold(
(newFields) {

View File

@ -183,9 +183,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
],
],
toolbarItems: [
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
smartEditItem,
]
smartEditItem,
],
themeData: theme.copyWith(extensions: [
...theme.extensions.values,

View File

@ -59,7 +59,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
child: CircularProgressIndicator(),
);
},
future: AppService().getView(appID, gridID),
future: AppBackendService().getView(appID, gridID),
);
}

View File

@ -169,7 +169,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
);
}
},
future: AppService().fetchViews(widget.layoutType),
future: AppBackendService().fetchViews(widget.layoutType),
);
}

View File

@ -2,14 +2,11 @@ import 'dart:io';
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart' show FileType;
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@ -75,6 +72,7 @@ class CoverColorPicker extends StatefulWidget {
class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
late Future<List<String>>? fileImages;
bool isAddingImage = false;
@override
void initState() {
@ -87,26 +85,40 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
return Padding(
padding: const EdgeInsets.all(15),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(LocaleKeys.document_plugins_cover_colors.tr()),
const SizedBox(height: 10),
_buildColorPickerList(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_images.tr()),
const SizedBox(height: 10),
_buildFileImagePicker(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_abstract.tr()),
const SizedBox(height: 10),
_buildAbstractImagePicker(),
],
),
child: isAddingImage
? CoverImagePicker(
onBackPressed: () => setState(() {
isAddingImage = false;
}),
onFileSubmit: (List<String> path) {
setState(() {
isAddingImage = false;
});
})
: _buildCoverSelection(),
),
);
}
Widget _buildCoverSelection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(LocaleKeys.document_plugins_cover_colors.tr()),
const SizedBox(height: 10),
_buildColorPickerList(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_images.tr()),
const SizedBox(height: 10),
_buildFileImagePicker(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_abstract.tr()),
const SizedBox(height: 10),
_buildAbstractImagePicker(),
],
);
}
Widget _buildAbstractImagePicker() {
return GridView.builder(
shrinkWrap: true,
@ -196,7 +208,9 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
),
width: 20,
onPressed: () {
_pickImages();
setState(() {
isAddingImage = true;
});
},
),
);
@ -248,36 +262,6 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
prefs.setStringList(kLocalImagesKey, imageNames);
return imageNames;
}
Future<void> _pickImages() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> imageNames = prefs.getStringList(kLocalImagesKey) ?? [];
FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
allowMultiple: false,
type: FileType.image,
allowedExtensions: ['jpg', 'png', 'jpeg'],
);
if (result != null && result.files.isNotEmpty) {
final path = result.files.first.path;
if (path != null) {
final directory = await _coverPath();
final newPath = await File(path).copy(
'$directory/${path.split('/').last}',
);
imageNames.add(newPath.path);
}
}
await prefs.setStringList(kLocalImagesKey, imageNames);
setState(() {});
}
Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
return Directory('$directory/covers')
.create(recursive: true)
.then((value) => value.path);
}
}
class _CoverColorPickerState extends State<CoverColorPicker> {

View File

@ -0,0 +1,254 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
class CoverImagePicker extends StatefulWidget {
final VoidCallback onBackPressed;
final Function(List<String> paths) onFileSubmit;
const CoverImagePicker(
{super.key, required this.onBackPressed, required this.onFileSubmit});
@override
State<CoverImagePicker> createState() => _CoverImagePickerState();
}
class _CoverImagePickerState extends State<CoverImagePicker> {
TextEditingController urlController = TextEditingController();
bool get buttonDisabled => urlController.text.isEmpty;
@override
void initState() {
super.initState();
urlController.addListener(() {
setState(() {});
});
}
_buildFilePickerWidget(BuildContext ctx) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
svgWidget(
"editor/add",
size: const Size(20, 20),
),
const SizedBox(
width: 3,
),
FlowyText(
LocaleKeys.document_plugins_cover_pasteImageUrl.tr(),
),
],
),
const SizedBox(
height: 10,
),
FlowyText(
LocaleKeys.document_plugins_cover_or.tr(),
color: Colors.grey,
),
const SizedBox(
height: 10,
),
FlowyButton(
onTap: () {
ctx.read<CoverImagePickerBloc>().add(const PickFileImage());
},
useIntrinsicWidth: true,
leftIcon: svgWidget(
"file_icon",
size: const Size(25, 25),
),
text: FlowyText(
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
),
),
],
);
}
_buildImageDeleteButton(BuildContext ctx) {
return Positioned(
right: 10,
top: 10,
child: InkWell(
onTap: () {
ctx.read<CoverImagePickerBloc>().add(const DeleteImage());
},
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.onPrimary),
child: svgWidget(
"editor/close",
size: const Size(20, 20),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CoverImagePickerBloc()
..add(const CoverImagePickerEvent.initialEvent()),
child: BlocListener<CoverImagePickerBloc, CoverImagePickerState>(
listener: (context, state) {
if (state is NetworkImagePicked) {
state.successOrFail.isRight()
? showSnapBar(context,
LocaleKeys.document_plugins_cover_invalidImageUrl.tr())
: null;
}
if (state is Done) {
state.successOrFail.fold(
(l) => widget.onFileSubmit(l),
(r) => showSnapBar(
context,
LocaleKeys.document_plugins_cover_failedToAddImageToGallery
.tr()));
}
},
child: BlocBuilder<CoverImagePickerBloc, CoverImagePickerState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
state is Loading
? const SizedBox(
height: 180,
child: Center(
child: CircularProgressIndicator(),
),
)
: Stack(
children: [
Container(
height: 180,
alignment: Alignment.center,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondary,
borderRadius: Corners.s6Border,
image: state is Initial
? null
: state is NetworkImagePicked
? state.successOrFail.fold(
(path) => DecorationImage(
image: NetworkImage(path),
fit: BoxFit.cover),
(r) => null)
: state is FileImagePicked
? DecorationImage(
image: FileImage(
File(state.path)),
fit: BoxFit.cover)
: null),
child: (state is Initial)
? _buildFilePickerWidget(context)
: (state is NetworkImagePicked)
? state.successOrFail.fold(
(l) => null,
(r) => _buildFilePickerWidget(
context,
),
)
: null),
(state is FileImagePicked)
? _buildImageDeleteButton(context)
: (state is NetworkImagePicked)
? state.successOrFail.fold(
(l) => _buildImageDeleteButton(context),
(r) => Container())
: Container()
],
),
const SizedBox(
height: 10,
),
Row(
children: [
Expanded(
flex: 4,
child: FlowyTextField(
controller: urlController,
hintText: LocaleKeys
.document_plugins_cover_enterImageUrl
.tr(),
),
),
const SizedBox(
width: 5,
),
Expanded(
flex: 1,
child: RoundedTextButton(
onPressed: () {
urlController.text.isNotEmpty
? context
.read<CoverImagePickerBloc>()
.add(UrlSubmit(urlController.text))
: null;
},
hoverColor: Colors.transparent,
fillColor: buttonDisabled
? Colors.grey
: Theme.of(context).colorScheme.primary,
height: 36,
title: LocaleKeys.document_plugins_cover_add.tr(),
borderRadius: Corners.s8Border,
),
)
],
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyTextButton(
LocaleKeys.document_plugins_cover_back.tr(),
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
onPressed: () => widget.onBackPressed(),
),
FlowyTextButton(
LocaleKeys.document_plugins_cover_saveToGallery.tr(),
onPressed: () async {
context
.read<CoverImagePickerBloc>()
.add(SaveToGallery(state));
},
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
fontColor: Theme.of(context).colorScheme.primary,
),
],
)
],
);
},
),
),
);
}
}

View File

@ -0,0 +1,196 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart' as fp;
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as path;
import 'change_cover_popover.dart';
part 'cover_image_picker_bloc.freezed.dart';
class CoverImagePickerBloc
extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) {
on<CoverImagePickerEvent>(
(event, emit) async {
await event.map(
initialEvent: (InitialEvent initialEvent) {
emit(const CoverImagePickerState.initial());
},
urlSubmit: (UrlSubmit urlSubmit) async {
emit(const CoverImagePickerState.loading());
final validateImage = await _validateUrl(urlSubmit.path);
if (validateImage) {
emit(CoverImagePickerState.networkImage(left(urlSubmit.path)));
} else {
emit(
CoverImagePickerState.networkImage(
right(
FlowyError(
msg: LocaleKeys.document_plugins_cover_couldNotFetchImage
.tr(),
),
),
),
);
}
},
pickFileImage: (PickFileImage pickFileImage) async {
final imagePickerResults = await _pickImages();
if (imagePickerResults != null) {
emit(CoverImagePickerState.fileImage(imagePickerResults));
} else {
emit(const CoverImagePickerState.initial());
}
},
deleteImage: (DeleteImage deleteImage) {
emit(const CoverImagePickerState.initial());
},
saveToGallery: (SaveToGallery saveToGallery) async {
emit(const CoverImagePickerState.loading());
final saveImage = await _saveToGallery(saveToGallery.previousState);
if (saveImage != null) {
emit(CoverImagePickerState.done(left(saveImage)));
} else {
emit(
CoverImagePickerState.done(
right(
FlowyError(
msg: LocaleKeys.document_plugins_cover_imageSavingFailed
.tr()),
),
),
);
emit(const CoverImagePickerState.initial());
}
},
);
},
);
}
_saveToGallery(CoverImagePickerState state) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> imagePaths = prefs.getStringList(kLocalImagesKey) ?? [];
final directory = await _coverPath();
if (state is FileImagePicked) {
try {
final path = state.path;
final newPath = '$directory/${path.split("\\").last}';
final newFile = await File(path).copy(newPath);
imagePaths.add(newFile.path);
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} catch (e) {
return null;
}
} else if (state is NetworkImagePicked) {
try {
String? url = state.successOrFail.fold((path) => path, (r) => null);
if (url != null) {
final response = await http.get(Uri.parse(url));
final newPath =
"$directory/IMG_$_timeStampString.${_getExtention(url)}";
final imageFile = File(newPath);
await imageFile.create();
await imageFile.writeAsBytes(response.bodyBytes);
imagePaths.add(imageFile.absolute.path);
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} else {
return null;
}
} catch (e) {
return null;
}
}
}
_pickImages() async {
FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
allowMultiple: false,
type: fp.FileType.image,
allowedExtensions: ['jpg', 'png', 'jpeg'],
);
if (result != null && result.files.isNotEmpty) {
final path = result.files.first.path;
if (path != null) {
return path;
} else {
return null;
}
}
return null;
}
Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
return Directory(path.join(directory, 'covers'))
.create(recursive: true)
.then((value) => value.path);
}
String get _timeStampString =>
DateTime.now().millisecondsSinceEpoch.toString();
String? _getExtention(String path) => path.contains(".jpg")
? "jpg"
: path.contains(".png")
? "png"
: path.contains(".jpeg")
? "jpeg"
: (path.contains("auto=format") && path.contains("unsplash"))
? "jpeg"
: null;
_validateUrl(String path) async {
if (_getExtention(path) != null) {
try {
final response = await http.get(Uri.parse(path));
if (response.statusCode == 200) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
} else {
return false;
}
}
}
@freezed
class CoverImagePickerEvent with _$CoverImagePickerEvent {
const factory CoverImagePickerEvent.urlSubmit(String path) = UrlSubmit;
const factory CoverImagePickerEvent.pickFileImage() = PickFileImage;
const factory CoverImagePickerEvent.deleteImage() = DeleteImage;
const factory CoverImagePickerEvent.saveToGallery(
CoverImagePickerState previousState) = SaveToGallery;
const factory CoverImagePickerEvent.initialEvent() = InitialEvent;
}
@freezed
class CoverImagePickerState with _$CoverImagePickerState {
const factory CoverImagePickerState.initial() = Initial;
const factory CoverImagePickerState.loading() = Loading;
const factory CoverImagePickerState.networkImage(
Either<String, FlowyError> successOrFail) = NetworkImagePicked;
const factory CoverImagePickerState.fileImage(String path) = FileImagePicked;
const factory CoverImagePickerState.done(
Either<List<String>, FlowyError> successOrFail) = Done;
}

View File

@ -2,6 +2,9 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/emoji_popover.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/icon_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@ -14,10 +17,10 @@ import 'package:flutter/material.dart';
const String kCoverType = 'cover';
const String kCoverSelectionTypeAttribute = 'cover_selection_type';
const String kCoverSelectionAttribute = 'cover_selection';
const String kIconSelectionAttribute = 'selected_icon';
enum CoverSelectionType {
initial,
color,
file,
asset;
@ -68,23 +71,16 @@ class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
widget.node.attributes[kCoverSelectionTypeAttribute],
);
PopoverController iconPopoverController = PopoverController();
@override
Widget build(BuildContext context) {
if (selectionType == CoverSelectionType.initial) {
return _AddCoverButton(
onTap: () {
_insertCover(CoverSelectionType.asset, builtInAssetImages.first);
},
);
} else {
return _CoverImage(
editorState: widget.editorState,
node: widget.node,
onCoverChanged: (type, value) {
_insertCover(type, value);
},
);
}
return _CoverImage(
editorState: widget.editorState,
node: widget.node,
onCoverChanged: (type, value) {
_insertCover(type, value);
},
);
}
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
@ -92,14 +88,26 @@ class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute: type.toString(),
kCoverSelectionAttribute: cover,
kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
});
return widget.editorState.apply(transaction);
}
}
class _AddCoverButton extends StatefulWidget {
final Node node;
final EditorState editorState;
final bool hasIcon;
final CoverSelectionType selectionType;
final PopoverController iconPopoverController;
const _AddCoverButton({
required this.onTap,
required this.node,
required this.editorState,
required this.hasIcon,
required this.selectionType,
required this.iconPopoverController,
});
final VoidCallback onTap;
@ -108,8 +116,16 @@ class _AddCoverButton extends StatefulWidget {
State<_AddCoverButton> createState() => _AddCoverButtonState();
}
bool isPopoverOpen = false;
class _AddCoverButtonState extends State<_AddCoverButton> {
bool isHidden = true;
PopoverMutex mutex = PopoverMutex();
bool isPopoverOpen = false;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
@ -118,40 +134,118 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
setHidden(false);
},
onExit: (event) {
setHidden(true);
setHidden(isPopoverOpen ? false : true);
},
opaque: false,
child: Container(
height: 50.0,
height: widget.hasIcon ? 180 : 50.0,
alignment: Alignment.bottomLeft,
width: double.infinity,
padding: const EdgeInsets.only(top: 20, bottom: 5),
// color: Colors.red,
child: isHidden
? const SizedBox()
? Container()
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Add Cover Button.
FlowyButton(
leftIconSize: const Size.square(18),
onTap: widget.onTap,
useIntrinsicWidth: true,
leftIcon: svgWidget(
'editor/image',
color: Theme.of(context).colorScheme.onSurface,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addCover.tr(),
),
)
widget.selectionType != CoverSelectionType.initial
? Container()
: FlowyButton(
key: UniqueKey(),
leftIconSize: const Size.square(18),
onTap: widget.onTap,
useIntrinsicWidth: true,
leftIcon: svgWidget(
'editor/image',
color: Theme.of(context).colorScheme.onSurface,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addCover.tr(),
),
),
// Add Icon Button.
// ...
widget.hasIcon
? FlowyButton(
leftIconSize: const Size.square(18),
onTap: () {
_removeIcon();
},
useIntrinsicWidth: true,
leftIcon: Icon(
Icons.emoji_emotions_outlined,
color: Theme.of(context).colorScheme.onSurface,
size: 18,
),
text: FlowyText.regular(LocaleKeys
.document_plugins_cover_removeIcon
.tr()),
)
: AppFlowyPopover(
mutex: mutex,
asBarrier: true,
onClose: () {
isPopoverOpen = false;
setHidden(true);
},
offset: const Offset(120, 10),
controller: widget.iconPopoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints:
BoxConstraints.loose(const Size(320, 380)),
margin: EdgeInsets.zero,
child: FlowyButton(
leftIconSize: const Size.square(18),
useIntrinsicWidth: true,
leftIcon: Icon(Icons.emoji_emotions_outlined,
color: Theme.of(context).colorScheme.onSurface,
size: 18),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addIcon.tr()),
),
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return EmojiPopover(
showRemoveButton: widget.hasIcon,
removeIcon: _removeIcon,
node: widget.node,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
_insertIcon(emoji);
widget.iconPopoverController.close();
});
},
)
],
),
),
);
}
Future<void> _insertIcon(Emoji emoji) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute:
widget.node.attributes[kCoverSelectionTypeAttribute],
kCoverSelectionAttribute:
widget.node.attributes[kCoverSelectionAttribute],
kIconSelectionAttribute: emoji.emoji,
});
return widget.editorState.apply(transaction);
}
Future<void> _removeIcon() async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kIconSelectionAttribute: "",
kCoverSelectionTypeAttribute:
widget.node.attributes[kCoverSelectionTypeAttribute],
kCoverSelectionAttribute:
widget.node.attributes[kCoverSelectionAttribute],
});
return widget.editorState.apply(transaction);
}
void setHidden(bool value) {
if (isHidden == value) return;
setState(() {
@ -173,7 +267,6 @@ class _CoverImage extends StatefulWidget {
CoverSelectionType selectionType,
dynamic selection,
) onCoverChanged;
@override
State<_CoverImage> createState() => _CoverImageState();
}
@ -187,23 +280,112 @@ class _CoverImageState extends State<_CoverImage> {
Color get color =>
Color(int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ??
0xFFFFFFFF);
bool get hasIcon => widget.node.attributes[kIconSelectionAttribute] == null
? false
: widget.node.attributes[kIconSelectionAttribute].isNotEmpty;
bool isOverlayButtonsHidden = true;
PopoverController iconPopoverController = PopoverController();
bool get hasCover =>
selectionType == CoverSelectionType.initial ? false : true;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.bottomLeft,
children: [
_buildCoverImage(context, widget.editorState),
_buildCoverOverlayButtons(context),
Container(
alignment: Alignment.topCenter,
height: !hasCover
? 0
: hasIcon
? 320
: 280,
child: _buildCoverImage(context, widget.editorState),
),
hasIcon
? Positioned(
bottom: !hasCover ? 30 : 10,
child: AppFlowyPopover(
offset: const Offset(100, 0),
controller: iconPopoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(320, 380)),
margin: EdgeInsets.zero,
child: EmojiIconWidget(
emoji: widget.node.attributes[kIconSelectionAttribute],
onEmojiTapped: () {
iconPopoverController.show();
},
),
popupBuilder: (BuildContext popoverContext) {
return EmojiPopover(
node: widget.node,
showRemoveButton: hasIcon,
removeIcon: _removeIcon,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
_insertIcon(emoji);
iconPopoverController.close();
});
},
),
)
: Container(),
hasIcon && selectionType != CoverSelectionType.initial
? Container()
: _AddCoverButton(
onTap: () {
_insertCover(
CoverSelectionType.asset, builtInAssetImages.first);
},
node: widget.node,
editorState: widget.editorState,
hasIcon: hasIcon,
selectionType: selectionType,
iconPopoverController: iconPopoverController,
),
],
);
}
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute: type.toString(),
kCoverSelectionAttribute: cover,
kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
});
return widget.editorState.apply(transaction);
}
Future<void> _insertIcon(Emoji emoji) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute:
widget.node.attributes[kCoverSelectionTypeAttribute],
kCoverSelectionAttribute:
widget.node.attributes[kCoverSelectionAttribute],
kIconSelectionAttribute: emoji.emoji,
});
return widget.editorState.apply(transaction);
}
Future<void> _removeIcon() async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kIconSelectionAttribute: "",
kCoverSelectionTypeAttribute:
widget.node.attributes[kCoverSelectionTypeAttribute],
kCoverSelectionAttribute:
widget.node.attributes[kCoverSelectionAttribute],
});
return widget.editorState.apply(transaction);
}
Widget _buildCoverOverlayButtons(BuildContext context) {
return Positioned(
bottom: 22,
right: 12,
bottom: 20,
right: 260,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -253,7 +435,7 @@ class _CoverImageState extends State<_CoverImage> {
Widget _buildCoverImage(BuildContext context, EditorState editorState) {
final screenSize = MediaQuery.of(context).size;
const height = 200.0;
const height = 250.0;
final Widget coverImage;
switch (selectionType) {
case CoverSelectionType.file:
@ -278,7 +460,7 @@ class _CoverImageState extends State<_CoverImage> {
);
break;
case CoverSelectionType.initial:
coverImage = const SizedBox(); // just an empty sizebox
coverImage = const SizedBox();
break;
}
//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an erorr
@ -286,11 +468,16 @@ class _CoverImageState extends State<_CoverImage> {
height: height,
child: OverflowBox(
maxWidth: screenSize.width,
child: Container(
padding: const EdgeInsets.only(bottom: 10),
height: double.infinity,
width: double.infinity,
child: coverImage,
child: Stack(
children: [
Container(
padding: const EdgeInsets.only(bottom: 10),
height: double.infinity,
width: double.infinity,
child: coverImage,
),
hasCover ? _buildCoverOverlayButtons(context) : const SizedBox()
],
),
),
);

View File

@ -0,0 +1,92 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
class EmojiPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final void Function(Emoji emoji) onEmojiChanged;
final VoidCallback removeIcon;
final bool showRemoveButton;
const EmojiPopover({
super.key,
required this.editorState,
required this.node,
required this.onEmojiChanged,
required this.removeIcon,
required this.showRemoveButton,
});
@override
State<EmojiPopover> createState() => _EmojiPopoverState();
}
class _EmojiPopoverState extends State<EmojiPopover> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(15),
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
customWidget: (Config config, EmojiViewState state) {
return Stack(
alignment: Alignment.topRight,
children: [
Container(
padding: EdgeInsets.only(top: widget.showRemoveButton ? 25 : 0),
child: DefaultEmojiPickerView(config, state),
),
_buildDeleteButtonIfNeed(),
],
);
},
config: const Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Colors.grey,
iconColorSelected: Color(0xff333333),
indicatorColor: Color(0xff333333),
progressIndicatorColor: Color(0xff333333),
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
);
}
Widget _buildDeleteButtonIfNeed() {
if (!widget.showRemoveButton) {
return const SizedBox();
}
return FlowyButton(
onTap: () => widget.removeIcon(),
useIntrinsicWidth: true,
hoverColor: Theme.of(context).colorScheme.onPrimary,
text: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
svgWidget("editor/delete"),
const SizedBox(
width: 5,
),
FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
color: Colors.grey,
),
],
),
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmojiIconWidget extends StatefulWidget {
final String? emoji;
final void Function() onEmojiTapped;
const EmojiIconWidget({
super.key,
required this.emoji,
required this.onEmojiTapped,
});
@override
State<EmojiIconWidget> createState() => _EmojiIconWidgetState();
}
class _EmojiIconWidgetState extends State<EmojiIconWidget> {
bool hover = true;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setHidden(false);
},
onExit: (event) {
setHidden(true);
},
child: Container(
height: 130,
width: 130,
margin: const EdgeInsets.only(top: 18),
decoration: BoxDecoration(
color: !hover
? Theme.of(context).colorScheme.secondary
: Colors.transparent,
borderRadius: Corners.s8Border,
),
alignment: Alignment.center,
child: Stack(
clipBehavior: Clip.none,
children: [
FlowyText(
widget.emoji.toString(),
fontSize: 80,
),
],
),
),
);
}
void setHidden(bool value) {
if (hover == value) return;
setState(() {
hover = value;
});
}
}

View File

@ -10,9 +10,9 @@ enum SmartEditAction {
String get toInstruction {
switch (this) {
case SmartEditAction.summarize:
return 'Make it shorter';
return 'Make this shorter and more concise:';
case SmartEditAction.fixSpelling:
return 'Fix all the spelling mistakes';
return 'Correct this to standard English:';
}
}
}

View File

@ -140,9 +140,12 @@ class _SmartEditInputState extends State<_SmartEditInput> {
}
Widget _buildResultWidget(BuildContext context) {
final loading = SizedBox.fromSize(
size: const Size.square(14),
child: const CircularProgressIndicator(),
final loading = Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: SizedBox.fromSize(
size: const Size.square(14),
child: const CircularProgressIndicator(),
),
);
if (result == null) {
return loading;
@ -222,7 +225,6 @@ class _SmartEditInputState extends State<_SmartEditInput> {
final texts = result!.asRight().choices.first.text.split('\n')
..removeWhere((element) => element.isEmpty);
assert(texts.length == selectedNodes.length);
final transaction = widget.editorState.transaction;
transaction.replaceTexts(
selectedNodes.toList(growable: false),
@ -254,7 +256,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
final edits = await openAIRepository.getEdits(
input: input,
instruction: instruction,
n: input.split('\n').length,
n: 1,
);
return edits.fold((error) async {
return dartz.Left(

View File

@ -1,10 +1,14 @@
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
ToolbarItem smartEditItem = ToolbarItem(
id: 'appflowy.toolbar.smart_edit',
@ -33,6 +37,20 @@ class _SmartEditWidget extends StatefulWidget {
}
class _SmartEditWidgetState extends State<_SmartEditWidget> {
bool isOpenAIEnabled = false;
@override
void initState() {
super.initState();
UserBackendService.getCurrentUserProfile().then((value) {
setState(() {
isOpenAIEnabled =
value.fold((l) => l.openaiKey.isNotEmpty, (r) => false);
});
});
}
@override
Widget build(BuildContext context) {
return PopoverActionList<SmartEditActionWrapper>(
@ -43,7 +61,9 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
buildChild: (controller) {
return FlowyIconButton(
hoverColor: Colors.transparent,
tooltipText: 'Smart Edit',
tooltipText: isOpenAIEnabled
? LocaleKeys.document_plugins_smartEdit.tr()
: LocaleKeys.document_plugins_smartEditDisabled.tr(),
preferBelow: false,
icon: const Icon(
Icons.lightbulb_outline,
@ -51,7 +71,11 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
color: Colors.white,
),
onPressed: () {
controller.show();
if (isOpenAIEnabled) {
controller.show();
} else {
_showError(LocaleKeys.document_plugins_smartEditDisabled.tr());
}
},
);
},
@ -97,4 +121,18 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
withUpdateCursor: false,
);
}
Future<void> _showError(String message) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
action: SnackBarAction(
label: LocaleKeys.button_Cancel.tr(),
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
content: FlowyText(message),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import '../startup.dart';
@ -35,11 +36,11 @@ Future<Directory> appFlowyDocumentDirectory() async {
switch (integrationEnv()) {
case IntegrationMode.develop:
Directory documentsDir = await getApplicationDocumentsDirectory();
return Directory('${documentsDir.path}/flowy_dev').create();
return Directory(path.join(documentsDir.path, 'flowy_dev')).create();
case IntegrationMode.release:
Directory documentsDir = await getApplicationDocumentsDirectory();
return Directory('${documentsDir.path}/flowy').create();
return Directory(path.join(documentsDir.path, 'flowy')).create();
case IntegrationMode.test:
return Directory("${Directory.current.path}/.sandbox");
return Directory(path.join(Directory.current.path, '.sandbox'));
}
}

View File

@ -18,11 +18,11 @@ import 'package:dartz/dartz.dart';
part 'app_bloc.freezed.dart';
class AppBloc extends Bloc<AppEvent, AppState> {
final AppService appService;
final AppBackendService appService;
final AppListener appListener;
AppBloc({required AppPB app})
: appService = AppService(),
: appService = AppBackendService(),
appListener = AppListener(appId: app.id),
super(AppState.initial(app)) {
on<AppEvent>((event, emit) async {

View File

@ -8,7 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
class AppService {
class AppBackendService {
Future<Either<AppPB, FlowyError>> readApp({required String appId}) {
final payload = AppIdPB.create()..value = appId;
@ -21,9 +21,18 @@ class AppService {
String? desc,
required ViewLayoutTypePB layoutType,
/// The initial data should be the JSON of the doucment
/// For example: {"document":{"type":"editor","children":[]}}
/// The initial data should be the JSON of the document.
/// Currently, only support create document with initial data.
///
/// The initial data must be follow this format as shown below.
/// {"document":{"type":"editor","children":[]}}
String? initialData,
/// The [ext] is used to pass through the custom configuration
/// to the backend.
/// Linking the view to the existing database, it needs to pass
/// the database id. For example: "database_id": "xxx"
///
Map<String, String> ext = const {},
}) {
final payload = CreateViewPayloadPB.create()

View File

@ -13,11 +13,11 @@ class ViewSectionBloc extends Bloc<ViewSectionEvent, ViewSectionState> {
void Function()? _viewsListener;
void Function()? _selectedViewlistener;
final AppViewDataContext _appViewData;
late final AppService _appService;
late final AppBackendService _appService;
ViewSectionBloc({
required AppViewDataContext appViewData,
}) : _appService = AppService(),
}) : _appService = AppBackendService(),
_appViewData = appViewData,
super(ViewSectionState.initial(appViewData)) {
on<ViewSectionEvent>((event, emit) async {

View File

@ -57,7 +57,10 @@ extension CommandExtension on EditorState {
Selection selection,
) {
List<String> res = [];
if (!selection.isCollapsed) {
if (selection.isSingle) {
final plainText = textNodes.first.toPlainText();
res.add(plainText.substring(selection.startIndex, selection.endIndex));
} else if (!selection.isCollapsed) {
for (var i = 0; i < textNodes.length; i++) {
final plainText = textNodes[i].toPlainText();
if (i == 0) {

View File

@ -27,10 +27,10 @@ class NodeIterator implements Iterator<Node> {
return true;
}
final node = _currentNode;
if (node == null) {
if (_currentNode == null) {
return false;
}
Node node = _currentNode!;
if (endNode != null && endNode == node) {
_currentNode = null;
@ -38,16 +38,19 @@ class NodeIterator implements Iterator<Node> {
}
if (node.children.isNotEmpty) {
_currentNode = _findLeadingChild(node);
_currentNode = node.children.first;
} else if (node.next != null) {
_currentNode = node.next!;
} else {
final parent = node.parent!;
final nextOfParent = parent.next;
if (nextOfParent == null) {
_currentNode = null;
} else {
_currentNode = nextOfParent;
while (node.parent != null) {
node = node.parent!;
final nextOfParent = node.next;
if (nextOfParent == null) {
_currentNode = null;
} else {
_currentNode = nextOfParent;
break;
}
}
}
@ -61,11 +64,4 @@ class NodeIterator implements Iterator<Node> {
}
return result;
}
Node _findLeadingChild(Node node) {
while (node.children.isNotEmpty) {
node = node.children.first;
}
return node;
}
}

View File

@ -291,46 +291,125 @@ extension TextTransaction on Transaction {
Selection selection,
List<String> texts,
) {
if (textNodes.isEmpty) {
if (textNodes.isEmpty || texts.isEmpty) {
return;
}
if (selection.isSingle) {
assert(textNodes.length == 1 && texts.length == 1);
replaceText(
textNodes.first,
selection.startIndex,
selection.length,
texts.first,
);
} else {
if (textNodes.length == texts.length) {
final length = textNodes.length;
for (var i = 0; i < length; i++) {
if (length == 1) {
replaceText(
textNodes.first,
selection.startIndex,
selection.endIndex - selection.startIndex,
texts.first,
);
return;
}
for (var i = 0; i < textNodes.length; i++) {
final textNode = textNodes[i];
final text = texts[i];
if (i == 0) {
replaceText(
textNode,
selection.startIndex,
textNode.toPlainText().length,
text,
texts.first,
);
} else if (i == length - 1) {
replaceText(
textNode,
0,
selection.endIndex,
text,
texts.last,
);
} else {
replaceText(
textNode,
0,
textNode.toPlainText().length,
text,
texts[i],
);
}
}
return;
}
if (textNodes.length > texts.length) {
final length = textNodes.length;
for (var i = 0; i < textNodes.length; i++) {
final textNode = textNodes[i];
if (i == 0) {
replaceText(
textNode,
selection.startIndex,
textNode.toPlainText().length,
texts.first,
);
} else if (i == length - 1) {
replaceText(
textNode,
0,
selection.endIndex,
texts.last,
);
} else {
if (i < texts.length - 1) {
replaceText(
textNode,
0,
textNode.toPlainText().length,
texts[i],
);
} else {
deleteNode(textNode);
}
}
}
afterSelection = null;
return;
}
if (textNodes.length < texts.length) {
final length = texts.length;
for (var i = 0; i < texts.length; i++) {
final text = texts[i];
if (i == 0) {
replaceText(
textNodes.first,
selection.startIndex,
textNodes.first.toPlainText().length,
text,
);
} else if (i == length - 1) {
replaceText(
textNodes.last,
0,
selection.endIndex,
text,
);
} else {
if (i < textNodes.length - 1) {
replaceText(
textNodes[i],
0,
textNodes[i].toPlainText().length,
text,
);
} else {
var path = textNodes.first.path;
var j = i - textNodes.length + length - 1;
while (j > 0) {
path = path.next;
j--;
}
insertNode(path, TextNode(delta: Delta()..insert(text)));
}
}
}
afterSelection = null;
return;
}
}
}

View File

@ -112,6 +112,7 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
isFocus = false;
this.showCursor = showCursor;
_focusNode.unfocus(disposition: disposition);
_onFocusChange(false);
}
@override

View File

@ -347,8 +347,9 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
void _onPanStart(DragStartDetails details) {
clearSelection();
_clearToolbar();
_panStartOffset = details.globalPosition;
_panStartOffset = details.globalPosition.translate(-3.0, 0);
_panStartScrollDy = editorState.service.scrollService?.dy;
_enableInteraction();

View File

@ -28,5 +28,29 @@ void main() async {
}
expect(nodes.moveNext(), false);
});
test('toList - when we have at least three level nested nodes (children)',
() {
final root = Node(type: 'root'),
n1 = Node(type: 'node_1'),
n2 = Node(type: 'node_2');
root.insert(n1);
root.insert(n2);
n1.insert(Node(type: 'node_1_1'));
n1.insert(Node(type: 'node_1_2'));
n1.childAtIndex(0)?.insert(Node(type: 'node_1_1_1'));
n1.childAtIndex(1)?.insert(Node(type: 'node_1_2_1'));
final nodes = NodeIterator(
document: Document(root: root),
startNode: root.childAtPath([0])!,
endNode: root.childAtPath([1]),
).toList();
expect(nodes[0].id, n1.id);
expect(nodes[1].id, n1.childAtIndex(0)!.id);
expect(nodes[nodes.length - 1].id, n2.id);
});
});
}

View File

@ -0,0 +1,132 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
Document createEmptyDocument() {
return Document(
root: Node(
type: 'editor',
),
);
}
void main() async {
group('transaction.dart', () {
testWidgets('test replaceTexts, textNodes.length == texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
final editor = tester.editor
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789');
await editor.startTesting();
await tester.pumpAndSettle();
expect(editor.documentLength, 4);
final selection = Selection(
start: Position(path: [0], offset: 4),
end: Position(path: [3], offset: 4),
);
final transaction = editor.editorState.transaction;
var textNodes = [0, 1, 2, 3]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
transaction.replaceTexts(textNodes, selection, texts);
editor.editorState.apply(transaction);
await tester.pumpAndSettle();
expect(editor.documentLength, 4);
textNodes = [0, 1, 2, 3]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
expect(textNodes[0].toPlainText(), '0123ABC');
expect(textNodes[1].toPlainText(), 'ABC');
expect(textNodes[2].toPlainText(), 'ABC');
expect(textNodes[3].toPlainText(), 'ABC456789');
});
testWidgets('test replaceTexts, textNodes.length > texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
final editor = tester.editor
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789');
await editor.startTesting();
await tester.pumpAndSettle();
expect(editor.documentLength, 5);
final selection = Selection(
start: Position(path: [0], offset: 4),
end: Position(path: [4], offset: 4),
);
final transaction = editor.editorState.transaction;
var textNodes = [0, 1, 2, 3, 4]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
transaction.replaceTexts(textNodes, selection, texts);
editor.editorState.apply(transaction);
await tester.pumpAndSettle();
expect(editor.documentLength, 4);
textNodes = [0, 1, 2, 3]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
expect(textNodes[0].toPlainText(), '0123ABC');
expect(textNodes[1].toPlainText(), 'ABC');
expect(textNodes[2].toPlainText(), 'ABC');
expect(textNodes[3].toPlainText(), 'ABC456789');
});
testWidgets('test replaceTexts, textNodes.length < texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
final editor = tester.editor
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789');
await editor.startTesting();
await tester.pumpAndSettle();
expect(editor.documentLength, 3);
final selection = Selection(
start: Position(path: [0], offset: 4),
end: Position(path: [2], offset: 4),
);
final transaction = editor.editorState.transaction;
var textNodes = [0, 1, 2]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
transaction.replaceTexts(textNodes, selection, texts);
editor.editorState.apply(transaction);
await tester.pumpAndSettle();
expect(editor.documentLength, 4);
textNodes = [0, 1, 2, 3]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
expect(textNodes[0].toPlainText(), '0123ABC');
expect(textNodes[1].toPlainText(), 'ABC');
expect(textNodes[2].toPlainText(), 'ABC');
expect(textNodes[3].toPlainText(), 'ABC456789');
});
});
}

View File

@ -830,7 +830,7 @@ packages:
source: hosted
version: "1.0.5"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
url: "https://pub.dartlang.org"

View File

@ -89,12 +89,14 @@ dependencies:
google_fonts: ^3.0.1
file_picker: <=5.0.0
percent_indicator: ^4.0.1
appflowy_editor_plugins:
path: packages/appflowy_editor_plugins
calendar_view: ^1.0.1
window_manager: ^0.3.0
http: ^0.13.5
json_annotation: ^4.7.0
path: ^1.8.2
dev_dependencies:
flutter_lints: ^2.0.1

View File

@ -30,7 +30,7 @@ class AppFlowyBoardTest {
Future<BoardTestContext> createTestBoard() async {
final app = await unitTest.createTestApp();
final builder = BoardPluginBuilder();
return AppService()
return AppBackendService()
.createView(
appId: app.id,
name: "Test Board",

View File

@ -8,7 +8,7 @@ import '../util.dart';
Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
final app = await gridTest.unitTest.createTestApp();
final builder = GridPluginBuilder();
final context = await AppService()
final context = await AppBackendService()
.createView(
appId: app.id,
name: "Filter Grid",

View File

@ -165,7 +165,7 @@ class AppFlowyGridTest {
Future<GridTestContext> createTestGrid() async {
final app = await unitTest.createTestApp();
final builder = GridPluginBuilder();
final context = await AppService()
final context = await AppBackendService()
.createView(
appId: app.id,
name: "Test Grid",

View File

@ -155,7 +155,7 @@ impl TryInto<MoveGroupRowParams> for MoveGroupRowPayloadPB {
}
#[derive(Debug, Default, ProtoBuf)]
pub struct DatabaseDescPB {
pub struct DatabaseDescriptionPB {
#[pb(index = 1)]
pub name: String,
@ -164,9 +164,9 @@ pub struct DatabaseDescPB {
}
#[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedDatabaseDescPB {
pub struct RepeatedDatabaseDescriptionPB {
#[pb(index = 1)]
pub items: Vec<DatabaseDescPB>,
pub items: Vec<DatabaseDescriptionPB>,
}
#[derive(Debug, Clone, Default, ProtoBuf)]

View File

@ -575,17 +575,17 @@ pub(crate) async fn move_group_row_handler(
#[tracing::instrument(level = "debug", skip(manager), err)]
pub(crate) async fn get_databases_handler(
manager: AFPluginState<Arc<DatabaseManager>>,
) -> DataResult<RepeatedDatabaseDescPB, FlowyError> {
) -> DataResult<RepeatedDatabaseDescriptionPB, FlowyError> {
let items = manager
.get_databases()
.await?
.into_iter()
.map(|database_info| DatabaseDescPB {
.map(|database_info| DatabaseDescriptionPB {
name: database_info.name,
database_id: database_info.database_id,
})
.collect::<Vec<DatabaseDescPB>>();
data_result_ok(RepeatedDatabaseDescPB { items })
.collect::<Vec<DatabaseDescriptionPB>>();
data_result_ok(RepeatedDatabaseDescriptionPB { items })
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]

View File

@ -238,7 +238,8 @@ pub enum DatabaseEvent {
#[event(input = "MoveGroupRowPayloadPB")]
GroupByField = 113,
#[event(output = "RepeatedDatabaseDescPB")]
/// Returns all the databases
#[event(output = "RepeatedDatabaseDescriptionPB")]
GetDatabases = 114,
#[event(input = "UpdateLayoutSettingPB")]

View File

@ -7,7 +7,9 @@ use crate::services::database_view::{
make_database_view_rev_manager, make_database_view_revision_pad, DatabaseViewEditor,
};
use crate::services::persistence::block_index::BlockRowIndexer;
use crate::services::persistence::database_ref::{DatabaseInfo, DatabaseRef, DatabaseRefIndexer};
use crate::services::persistence::database_ref::{
DatabaseInfo, DatabaseRefIndexer, DatabaseViewRef,
};
use crate::services::persistence::kv::DatabaseKVPersistence;
use crate::services::persistence::migration::DatabaseMigration;
use crate::services::persistence::rev_sqlite::{
@ -192,7 +194,10 @@ impl DatabaseManager {
self.database_ref_indexer.get_all_databases()
}
pub async fn get_database_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> {
pub async fn get_database_ref_views(
&self,
database_id: &str,
) -> FlowyResult<Vec<DatabaseViewRef>> {
self
.database_ref_indexer
.get_ref_views_with_database(database_id)
@ -425,13 +430,13 @@ pub async fn create_new_database(
}
impl DatabaseRefIndexerQuery for DatabaseRefIndexer {
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> {
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>> {
self.get_ref_views_with_database(database_id)
}
}
impl DatabaseRefIndexerQuery for Arc<DatabaseRefIndexer> {
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> {
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>> {
(**self).get_ref_views(database_id)
}
}

View File

@ -18,7 +18,7 @@ use crate::services::database_view::{
};
use crate::services::filter::FilterType;
use crate::services::persistence::block_index::BlockRowIndexer;
use crate::services::persistence::database_ref::DatabaseRef;
use crate::services::persistence::database_ref::DatabaseViewRef;
use crate::services::row::{DatabaseBlockRow, DatabaseBlockRowRevision, RowRevisionBuilder};
use bytes::Bytes;
use database_model::*;
@ -42,7 +42,7 @@ use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
pub trait DatabaseRefIndexerQuery: Send + Sync + 'static {
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>>;
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>>;
}
pub struct DatabaseEditor {

View File

@ -45,7 +45,10 @@ impl DatabaseRefIndexer {
Ok(())
}
pub fn get_ref_views_with_database(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> {
pub fn get_ref_views_with_database(
&self,
database_id: &str,
) -> FlowyResult<Vec<DatabaseViewRef>> {
let conn = self.database.get_db_connection()?;
let views = dsl::database_refs
.filter(database_refs::database_id.like(database_id))
@ -93,12 +96,12 @@ struct DatabaseRefRecord {
database_id: String,
}
pub struct DatabaseRef {
pub struct DatabaseViewRef {
pub view_id: String,
pub name: String,
pub database_id: String,
}
impl std::convert::From<DatabaseRefRecord> for DatabaseRef {
impl std::convert::From<DatabaseRefRecord> for DatabaseViewRef {
fn from(record: DatabaseRefRecord) -> Self {
Self {
view_id: record.view_id,

View File

@ -2,12 +2,12 @@ use crate::database::block_test::util::DatabaseRowTestBuilder;
use crate::database::database_editor::DatabaseEditorTest;
use database_model::RowRevision;
use flowy_database::services::database::DatabaseEditor;
use flowy_database::services::persistence::database_ref::{DatabaseInfo, DatabaseRef};
use flowy_database::services::persistence::database_ref::{DatabaseInfo, DatabaseViewRef};
use std::collections::HashMap;
use std::sync::Arc;
pub enum DatabaseRefScript {
LinkGridToDatabase {
pub enum LinkDatabaseTestScript {
CreateGridViewAndLinkToDatabase {
database_id: String,
},
#[allow(dead_code)]
@ -28,17 +28,17 @@ pub enum DatabaseRefScript {
},
}
pub struct DatabaseRefTest {
pub struct LinkDatabaseTest {
inner: DatabaseEditorTest,
}
impl DatabaseRefTest {
impl LinkDatabaseTest {
pub async fn new() -> Self {
let inner = DatabaseEditorTest::new_grid().await;
Self { inner }
}
pub async fn run_scripts(&mut self, scripts: Vec<DatabaseRefScript>) {
pub async fn run_scripts(&mut self, scripts: Vec<LinkDatabaseTestScript>) {
for script in scripts {
self.run_script(script).await;
}
@ -61,7 +61,7 @@ impl DatabaseRefTest {
.unwrap()
}
pub async fn all_database_ref_views(&self, database_id: &str) -> Vec<DatabaseRef> {
pub async fn all_database_ref_views(&self, database_id: &str) -> Vec<DatabaseViewRef> {
self
.inner
.sdk
@ -87,9 +87,9 @@ impl DatabaseRefTest {
DatabaseRowTestBuilder::new(self.block_id(view_id).await, field_revs)
}
pub async fn run_script(&mut self, script: DatabaseRefScript) {
pub async fn run_script(&mut self, script: LinkDatabaseTestScript) {
match script {
DatabaseRefScript::LinkGridToDatabase { database_id } => {
LinkDatabaseTestScript::CreateGridViewAndLinkToDatabase { database_id } => {
let mut ext = HashMap::new();
ext.insert("database_id".to_owned(), database_id);
self
@ -99,7 +99,7 @@ impl DatabaseRefTest {
.create_test_grid_view(&self.inner.app_id, "test link grid", ext)
.await;
},
DatabaseRefScript::LinkBoardToDatabase { database_id } => {
LinkDatabaseTestScript::LinkBoardToDatabase { database_id } => {
let mut ext = HashMap::new();
ext.insert("database_id".to_owned(), database_id);
self
@ -109,7 +109,7 @@ impl DatabaseRefTest {
.create_test_board_view(&self.inner.app_id, "test link board", ext)
.await;
},
DatabaseRefScript::CreateNewGrid => {
LinkDatabaseTestScript::CreateNewGrid => {
self
.inner
.sdk
@ -117,15 +117,15 @@ impl DatabaseRefTest {
.create_test_grid_view(&self.inner.app_id, "Create test grid", HashMap::new())
.await;
},
DatabaseRefScript::AssertNumberOfDatabase { expected } => {
LinkDatabaseTestScript::AssertNumberOfDatabase { expected } => {
let databases = self.all_databases().await;
assert_eq!(databases.len(), expected);
},
DatabaseRefScript::CreateRow { view_id, row_rev } => {
LinkDatabaseTestScript::CreateRow { view_id, row_rev } => {
let editor = self.get_database_editor(&view_id).await;
let _ = editor.insert_rows(vec![row_rev]).await.unwrap();
},
DatabaseRefScript::AssertNumberOfRows { view_id, expected } => {
LinkDatabaseTestScript::AssertNumberOfRows { view_id, expected } => {
let editor = self.get_database_editor(&view_id).await;
let rows = editor.get_all_row_revs(&view_id).await.unwrap();
assert_eq!(rows.len(), expected);

View File

@ -1,11 +1,12 @@
use crate::database::database_ref_test::script::DatabaseRefScript::*;
use crate::database::database_ref_test::script::DatabaseRefTest;
use crate::database::database_ref_test::script::LinkDatabaseTest;
use crate::database::database_ref_test::script::LinkDatabaseTestScript::*;
#[tokio::test]
async fn database_ref_number_of_database_test() {
let mut test = DatabaseRefTest::new().await;
async fn number_of_database_test() {
let mut test = LinkDatabaseTest::new().await;
test
.run_scripts(vec![
// After the LinkDatabaseTest initialize, it will create a grid.
AssertNumberOfDatabase { expected: 1 },
CreateNewGrid,
AssertNumberOfDatabase { expected: 2 },
@ -14,12 +15,12 @@ async fn database_ref_number_of_database_test() {
}
#[tokio::test]
async fn database_ref_link_with_existing_database_test() {
let mut test = DatabaseRefTest::new().await;
async fn database_view_link_with_existing_database_test() {
let mut test = LinkDatabaseTest::new().await;
let database = test.all_databases().await.pop().unwrap();
test
.run_scripts(vec![
LinkGridToDatabase {
CreateGridViewAndLinkToDatabase {
database_id: database.database_id,
},
AssertNumberOfDatabase { expected: 1 },
@ -28,36 +29,48 @@ async fn database_ref_link_with_existing_database_test() {
}
#[tokio::test]
async fn database_ref_link_with_existing_database_row_test() {
let mut test = DatabaseRefTest::new().await;
async fn check_number_of_rows_in_linked_database_view() {
let mut test = LinkDatabaseTest::new().await;
let database = test.all_databases().await.pop().unwrap();
let view = test
.all_database_ref_views(&database.database_id)
.await
.remove(0);
test
.run_scripts(vec![
LinkGridToDatabase {
CreateGridViewAndLinkToDatabase {
database_id: database.database_id,
},
AssertNumberOfDatabase { expected: 1 },
// The initial number of rows is 6
AssertNumberOfRows {
view_id: view.view_id.clone(),
expected: 6,
},
])
.await;
}
#[tokio::test]
async fn database_ref_create_new_row_test() {
let mut test = DatabaseRefTest::new().await;
async fn multiple_views_share_database_rows() {
let mut test = LinkDatabaseTest::new().await;
// After the LinkDatabaseTest initialize, it will create a default database
// with Grid layout.
let database = test.all_databases().await.pop().unwrap();
let database_views = test.all_database_ref_views(&database.database_id).await;
let mut database_views = test.all_database_ref_views(&database.database_id).await;
assert_eq!(database_views.len(), 1);
let view_id_1 = database_views.get(0).unwrap().view_id.clone();
let view = database_views.remove(0);
test
.run_scripts(vec![
AssertNumberOfRows {
view_id: view_id_1.clone(),
view_id: view.view_id.clone(),
expected: 6,
},
LinkGridToDatabase {
CreateGridViewAndLinkToDatabase {
database_id: database.database_id.clone(),
},
AssertNumberOfDatabase { expected: 1 },
])
.await;
@ -84,6 +97,7 @@ async fn database_ref_create_new_row_test() {
view_id: view_id_2,
expected: 7,
},
AssertNumberOfDatabase { expected: 1 },
])
.await;
}