mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
commit
6e028c4f64
14
.github/workflows/integration_test.yml
vendored
14
.github/workflows/integration_test.yml
vendored
@ -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
|
||||
|
||||
|
Binary file not shown.
5
frontend/appflowy_flutter/assets/images/folder.svg
Normal file
5
frontend/appflowy_flutter/assets/images/folder.svg
Normal 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 |
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -183,9 +183,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
||||
],
|
||||
],
|
||||
toolbarItems: [
|
||||
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
|
||||
smartEditItem,
|
||||
]
|
||||
smartEditItem,
|
||||
],
|
||||
themeData: theme.copyWith(extensions: [
|
||||
...theme.extensions.values,
|
||||
|
@ -59,7 +59,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
future: AppService().getView(appID, gridID),
|
||||
future: AppBackendService().getView(appID, gridID),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -169,7 +169,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
);
|
||||
}
|
||||
},
|
||||
future: AppService().fetchViews(widget.layoutType),
|
||||
future: AppBackendService().fetchViews(widget.layoutType),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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:';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,7 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
isFocus = false;
|
||||
this.showCursor = showCursor;
|
||||
_focusNode.unfocus(disposition: disposition);
|
||||
_onFocusChange(false);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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)]
|
||||
|
@ -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)]
|
||||
|
@ -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")]
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user