mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: media type option
This commit is contained in:
parent
b77fdb8424
commit
4793d29de2
@ -0,0 +1,199 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
|
import 'package:flowy_infra/uuid.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'media_cell_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class MediaCellBloc extends Bloc<MediaCellEvent, MediaCellState> {
|
||||||
|
MediaCellBloc({
|
||||||
|
required this.cellController,
|
||||||
|
}) : super(MediaCellState.initial(cellController)) {
|
||||||
|
_dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
final MediaCellController cellController;
|
||||||
|
void Function()? _onCellChangedFn;
|
||||||
|
|
||||||
|
late UserProfilePB _userProfile;
|
||||||
|
|
||||||
|
UserProfilePB get userProfile => _userProfile;
|
||||||
|
|
||||||
|
String get databaseId => cellController.viewId;
|
||||||
|
|
||||||
|
bool get wrapContent => cellController.fieldInfo.wrapCellContent ?? false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
if (_onCellChangedFn != null) {
|
||||||
|
cellController.removeListener(
|
||||||
|
onCellChanged: _onCellChangedFn!,
|
||||||
|
onFieldChanged: _onFieldChangedListener,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await cellController.dispose();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _dispatch() {
|
||||||
|
on<MediaCellEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
initial: () async {
|
||||||
|
_startListening();
|
||||||
|
|
||||||
|
// Fetch user profile
|
||||||
|
final userProfileResult =
|
||||||
|
await UserBackendService.getCurrentUserProfile();
|
||||||
|
userProfileResult.fold(
|
||||||
|
(userProfile) => _userProfile = userProfile,
|
||||||
|
(l) => Log.error(l),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch the files from cellController
|
||||||
|
final data = cellController.getCellData();
|
||||||
|
if (data != null) {
|
||||||
|
emit(state.copyWith(files: data.files));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
didUpdateCell: (files) {
|
||||||
|
emit(state.copyWith(files: files));
|
||||||
|
},
|
||||||
|
didUpdateField: (fieldName) {
|
||||||
|
emit(state.copyWith(fieldName: fieldName));
|
||||||
|
},
|
||||||
|
addFile: (url, name, uploadType, fileType) async {
|
||||||
|
final newFile = MediaFilePB(
|
||||||
|
id: uuid(),
|
||||||
|
url: url,
|
||||||
|
name: name,
|
||||||
|
uploadType: uploadType,
|
||||||
|
fileType: fileType,
|
||||||
|
);
|
||||||
|
|
||||||
|
final payload = MediaCellChangesetPB(
|
||||||
|
viewId: cellController.viewId,
|
||||||
|
cellId: CellIdPB(
|
||||||
|
viewId: cellController.viewId,
|
||||||
|
fieldId: cellController.fieldId,
|
||||||
|
rowId: cellController.rowId,
|
||||||
|
),
|
||||||
|
insertedFiles: [newFile],
|
||||||
|
removedIds: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await DatabaseEventUpdateMediaCell(payload).send();
|
||||||
|
result.fold((l) => null, (err) => Log.error(err));
|
||||||
|
},
|
||||||
|
removeFile: (id) async {
|
||||||
|
final payload = MediaCellChangesetPB(
|
||||||
|
viewId: cellController.viewId,
|
||||||
|
cellId: CellIdPB(
|
||||||
|
viewId: cellController.viewId,
|
||||||
|
fieldId: cellController.fieldId,
|
||||||
|
rowId: cellController.rowId,
|
||||||
|
),
|
||||||
|
insertedFiles: [],
|
||||||
|
removedIds: [id],
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await DatabaseEventUpdateMediaCell(payload).send();
|
||||||
|
result.fold((l) => null, (err) => Log.error(err));
|
||||||
|
},
|
||||||
|
reorderFiles: (from, to) async {
|
||||||
|
final files = List<MediaFilePB>.from(state.files);
|
||||||
|
if (from < to) {
|
||||||
|
to--;
|
||||||
|
}
|
||||||
|
|
||||||
|
files.insert(to, files.removeAt(from));
|
||||||
|
|
||||||
|
// We emit the new state first to update the UI
|
||||||
|
emit(state.copyWith(files: files));
|
||||||
|
|
||||||
|
final payload = MediaCellChangesetPB(
|
||||||
|
viewId: cellController.viewId,
|
||||||
|
cellId: CellIdPB(
|
||||||
|
viewId: cellController.viewId,
|
||||||
|
fieldId: cellController.fieldId,
|
||||||
|
rowId: cellController.rowId,
|
||||||
|
),
|
||||||
|
insertedFiles: files,
|
||||||
|
// In the backend we remove all files by id before we do inserts.
|
||||||
|
// So this will effectively reorder the files.
|
||||||
|
removedIds: files.map((file) => file.id).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await DatabaseEventUpdateMediaCell(payload).send();
|
||||||
|
result.fold((l) => null, (err) => Log.error(err));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startListening() {
|
||||||
|
_onCellChangedFn = cellController.addListener(
|
||||||
|
onCellChanged: (cellData) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(MediaCellEvent.didUpdateCell(cellData?.files ?? const []));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFieldChanged: _onFieldChangedListener,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFieldChangedListener(FieldInfo fieldInfo) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(MediaCellEvent.didUpdateField(fieldInfo.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class MediaCellEvent with _$MediaCellEvent {
|
||||||
|
const factory MediaCellEvent.initial() = _Initial;
|
||||||
|
|
||||||
|
const factory MediaCellEvent.didUpdateCell(List<MediaFilePB> files) =
|
||||||
|
_DidUpdateCell;
|
||||||
|
|
||||||
|
const factory MediaCellEvent.didUpdateField(String fieldName) =
|
||||||
|
_DidUpdateField;
|
||||||
|
|
||||||
|
const factory MediaCellEvent.addFile({
|
||||||
|
required String url,
|
||||||
|
required String name,
|
||||||
|
required MediaUploadTypePB uploadType,
|
||||||
|
required MediaFileTypePB fileType,
|
||||||
|
}) = _AddFile;
|
||||||
|
|
||||||
|
const factory MediaCellEvent.removeFile({
|
||||||
|
required String fileId,
|
||||||
|
}) = _RemoveFile;
|
||||||
|
|
||||||
|
const factory MediaCellEvent.reorderFiles({
|
||||||
|
required int from,
|
||||||
|
required int to,
|
||||||
|
}) = _ReorderFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class MediaCellState with _$MediaCellState {
|
||||||
|
const factory MediaCellState({
|
||||||
|
required String fieldName,
|
||||||
|
@Default([]) List<MediaFilePB> files,
|
||||||
|
}) = _MediaCellState;
|
||||||
|
|
||||||
|
factory MediaCellState.initial(MediaCellController cellController) {
|
||||||
|
return MediaCellState(fieldName: cellController.fieldInfo.field.name);
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ typedef RelationCellController = CellController<RelationCellDataPB, String>;
|
|||||||
typedef SummaryCellController = CellController<String, String>;
|
typedef SummaryCellController = CellController<String, String>;
|
||||||
typedef TimeCellController = CellController<TimeCellDataPB, String>;
|
typedef TimeCellController = CellController<TimeCellDataPB, String>;
|
||||||
typedef TranslateCellController = CellController<String, String>;
|
typedef TranslateCellController = CellController<String, String>;
|
||||||
|
typedef MediaCellController = CellController<MediaCellDataPB, String>;
|
||||||
|
|
||||||
CellController makeCellController(
|
CellController makeCellController(
|
||||||
DatabaseController databaseController,
|
DatabaseController databaseController,
|
||||||
@ -170,6 +171,18 @@ CellController makeCellController(
|
|||||||
),
|
),
|
||||||
cellDataPersistence: TextCellDataPersistence(),
|
cellDataPersistence: TextCellDataPersistence(),
|
||||||
);
|
);
|
||||||
|
case FieldType.Media:
|
||||||
|
return MediaCellController(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldController: fieldController,
|
||||||
|
cellContext: cellContext,
|
||||||
|
rowCache: rowCache,
|
||||||
|
cellDataLoader: CellDataLoader(
|
||||||
|
parser: MediaCellDataParser(),
|
||||||
|
reloadOnFieldChange: true,
|
||||||
|
),
|
||||||
|
cellDataPersistence: TextCellDataPersistence(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw UnimplementedError;
|
throw UnimplementedError;
|
||||||
}
|
}
|
||||||
|
@ -196,3 +196,19 @@ class TimeCellDataParser implements CellDataParser<TimeCellDataPB> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MediaCellDataParser implements CellDataParser<MediaCellDataPB> {
|
||||||
|
@override
|
||||||
|
MediaCellDataPB? parserData(List<int> data) {
|
||||||
|
if (data.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return MediaCellDataPB.fromBuffer(data);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error("Failed to parse media cell data: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -65,6 +65,7 @@ class FieldInfo with _$FieldInfo {
|
|||||||
case FieldType.Checklist:
|
case FieldType.Checklist:
|
||||||
case FieldType.URL:
|
case FieldType.URL:
|
||||||
case FieldType.Time:
|
case FieldType.Time:
|
||||||
|
case FieldType.Media:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
@ -281,6 +281,30 @@ class FilterBackendService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<FlowyResult<void, FlowyError>> insertMediaFilter({
|
||||||
|
required String fieldId,
|
||||||
|
String? filterId,
|
||||||
|
required MediaFilterConditionPB condition,
|
||||||
|
String content = "",
|
||||||
|
}) {
|
||||||
|
final filter = MediaFilterPB()
|
||||||
|
..condition = condition
|
||||||
|
..content = content;
|
||||||
|
|
||||||
|
return filterId == null
|
||||||
|
? insertFilter(
|
||||||
|
fieldId: fieldId,
|
||||||
|
fieldType: FieldType.Media,
|
||||||
|
data: filter.writeToBuffer(),
|
||||||
|
)
|
||||||
|
: updateFilter(
|
||||||
|
filterId: filterId,
|
||||||
|
fieldId: fieldId,
|
||||||
|
fieldType: FieldType.Media,
|
||||||
|
data: filter.writeToBuffer(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<FlowyResult<void, FlowyError>> deleteFilter({
|
Future<FlowyResult<void, FlowyError>> deleteFilter({
|
||||||
required String fieldId,
|
required String fieldId,
|
||||||
required String filterId,
|
required String filterId,
|
||||||
|
@ -3,13 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||||
import 'package:appflowy/plugins/database/domain/filter_service.dart';
|
import 'package:appflowy/plugins/database/domain/filter_service.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbenum.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbenum.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -149,6 +143,11 @@ class GridCreateFilterBloc
|
|||||||
fieldId: fieldId,
|
fieldId: fieldId,
|
||||||
condition: TextFilterConditionPB.TextContains,
|
condition: TextFilterConditionPB.TextContains,
|
||||||
);
|
);
|
||||||
|
case FieldType.Media:
|
||||||
|
return _filterBackendSvc.insertMediaFilter(
|
||||||
|
fieldId: fieldId,
|
||||||
|
condition: MediaFilterConditionPB.MediaIsNotEmpty,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
|
||||||
@ -15,7 +17,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ import '../../application/row/row_controller.dart';
|
|||||||
import '../../tab_bar/tab_bar_view.dart';
|
import '../../tab_bar/tab_bar_view.dart';
|
||||||
import '../../widgets/row/row_detail.dart';
|
import '../../widgets/row/row_detail.dart';
|
||||||
import '../application/grid_bloc.dart';
|
import '../application/grid_bloc.dart';
|
||||||
|
|
||||||
import 'grid_scroll.dart';
|
import 'grid_scroll.dart';
|
||||||
import 'layout/layout.dart';
|
import 'layout/layout.dart';
|
||||||
import 'layout/sizes.dart';
|
import 'layout/sizes.dart';
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../editable_cell_builder.dart';
|
import '../editable_cell_builder.dart';
|
||||||
|
|
||||||
import 'card_cell.dart';
|
import 'card_cell.dart';
|
||||||
|
|
||||||
class TextCardCellStyle extends CardCellStyle {
|
class TextCardCellStyle extends CardCellStyle {
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';
|
||||||
|
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||||
|
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class DekstopGridMediaCellSkin extends IEditableMediaCellSkin {
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
CellContainerNotifier cellContainerNotifier,
|
||||||
|
PopoverController popoverController,
|
||||||
|
MediaCellBloc bloc,
|
||||||
|
) {
|
||||||
|
return AppFlowyPopover(
|
||||||
|
controller: popoverController,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 250,
|
||||||
|
maxWidth: 250,
|
||||||
|
maxHeight: 400,
|
||||||
|
),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
|
popupBuilder: (_) => BlocProvider.value(
|
||||||
|
value: context.read<MediaCellBloc>(),
|
||||||
|
child: const MediaCellEditor(),
|
||||||
|
),
|
||||||
|
onClose: () => cellContainerNotifier.isFocus = false,
|
||||||
|
child: BlocBuilder<MediaCellBloc, MediaCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final wrapContent = context.read<MediaCellBloc>().wrapContent;
|
||||||
|
if (wrapContent) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: state.files
|
||||||
|
.map((file) => _FilePreviewRender(file: file))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FlowyTooltip(
|
||||||
|
message: '${state.files.length} files - click to view',
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: SeparatedRow(
|
||||||
|
separatorBuilder: () => const HSpace(6),
|
||||||
|
children: state.files
|
||||||
|
.map((file) => _FilePreviewRender(file: file))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FilePreviewRender extends StatelessWidget {
|
||||||
|
const _FilePreviewRender({required this.file});
|
||||||
|
|
||||||
|
final MediaFilePB file;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (file.fileType == MediaFileTypePB.Image) {
|
||||||
|
if (file.uploadType == MediaUploadTypePB.NetworkMedia) {
|
||||||
|
return Image.network(file.url, height: 32);
|
||||||
|
} else if (file.uploadType == MediaUploadTypePB.LocalMedia) {
|
||||||
|
return Image.file(File(file.url), height: 32);
|
||||||
|
} else {
|
||||||
|
// Cloud
|
||||||
|
return FlowyNetworkImage(
|
||||||
|
url: file.url,
|
||||||
|
userProfilePB: context.read<MediaCellBloc>().userProfile,
|
||||||
|
height: 32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AFThemeExtension.of(context).greyHover,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: FlowyText(
|
||||||
|
file.name,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||||
|
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
|
||||||
@ -134,6 +135,12 @@ class EditableCellBuilder {
|
|||||||
skin: IEditableTranslateCellSkin.fromStyle(style),
|
skin: IEditableTranslateCellSkin.fromStyle(style),
|
||||||
key: key,
|
key: key,
|
||||||
),
|
),
|
||||||
|
FieldType.Media => EditableMediaCell(
|
||||||
|
databaseController: databaseController,
|
||||||
|
cellContext: cellContext,
|
||||||
|
skin: IEditableMediaCellSkin.fromStyle(style),
|
||||||
|
key: key,
|
||||||
|
),
|
||||||
_ => throw UnimplementedError(),
|
_ => throw UnimplementedError(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -226,6 +233,12 @@ class EditableCellBuilder {
|
|||||||
skin: skinMap.timeSkin!,
|
skin: skinMap.timeSkin!,
|
||||||
key: key,
|
key: key,
|
||||||
),
|
),
|
||||||
|
FieldType.Media => EditableMediaCell(
|
||||||
|
databaseController: databaseController,
|
||||||
|
cellContext: cellContext,
|
||||||
|
skin: skinMap.mediaSkin!,
|
||||||
|
key: key,
|
||||||
|
),
|
||||||
_ => throw UnimplementedError(),
|
_ => throw UnimplementedError(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -382,6 +395,7 @@ class EditableCellSkinMap {
|
|||||||
this.urlSkin,
|
this.urlSkin,
|
||||||
this.relationSkin,
|
this.relationSkin,
|
||||||
this.timeSkin,
|
this.timeSkin,
|
||||||
|
this.mediaSkin,
|
||||||
});
|
});
|
||||||
|
|
||||||
final IEditableCheckboxCellSkin? checkboxSkin;
|
final IEditableCheckboxCellSkin? checkboxSkin;
|
||||||
@ -394,6 +408,7 @@ class EditableCellSkinMap {
|
|||||||
final IEditableURLCellSkin? urlSkin;
|
final IEditableURLCellSkin? urlSkin;
|
||||||
final IEditableRelationCellSkin? relationSkin;
|
final IEditableRelationCellSkin? relationSkin;
|
||||||
final IEditableTimeCellSkin? timeSkin;
|
final IEditableTimeCellSkin? timeSkin;
|
||||||
|
final IEditableMediaCellSkin? mediaSkin;
|
||||||
|
|
||||||
bool has(FieldType fieldType) {
|
bool has(FieldType fieldType) {
|
||||||
return switch (fieldType) {
|
return switch (fieldType) {
|
||||||
@ -410,6 +425,7 @@ class EditableCellSkinMap {
|
|||||||
FieldType.RichText => textSkin != null,
|
FieldType.RichText => textSkin != null,
|
||||||
FieldType.URL => urlSkin != null,
|
FieldType.URL => urlSkin != null,
|
||||||
FieldType.Time => timeSkin != null,
|
FieldType.Time => timeSkin != null,
|
||||||
|
FieldType.Media => mediaSkin != null,
|
||||||
_ => throw UnimplementedError(),
|
_ => throw UnimplementedError(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||||
|
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart';
|
||||||
|
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../application/cell/cell_controller_builder.dart';
|
||||||
|
|
||||||
|
abstract class IEditableMediaCellSkin {
|
||||||
|
const IEditableMediaCellSkin();
|
||||||
|
|
||||||
|
factory IEditableMediaCellSkin.fromStyle(EditableCellStyle style) {
|
||||||
|
return switch (style) {
|
||||||
|
EditableCellStyle.desktopGrid => DekstopGridMediaCellSkin(),
|
||||||
|
// TODO(Mathias): Implement the rest of the styles
|
||||||
|
EditableCellStyle.desktopRowDetail => DekstopGridMediaCellSkin(),
|
||||||
|
EditableCellStyle.mobileGrid => DekstopGridMediaCellSkin(),
|
||||||
|
EditableCellStyle.mobileRowDetail => DekstopGridMediaCellSkin(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
CellContainerNotifier cellContainerNotifier,
|
||||||
|
PopoverController popoverController,
|
||||||
|
MediaCellBloc bloc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditableMediaCell extends EditableCellWidget {
|
||||||
|
EditableMediaCell({
|
||||||
|
super.key,
|
||||||
|
required this.databaseController,
|
||||||
|
required this.cellContext,
|
||||||
|
required this.skin,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DatabaseController databaseController;
|
||||||
|
final CellContext cellContext;
|
||||||
|
final IEditableMediaCellSkin skin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridEditableTextCell<EditableMediaCell> createState() =>
|
||||||
|
_EditableMediaCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditableMediaCellState extends GridEditableTextCell<EditableMediaCell> {
|
||||||
|
final PopoverController popoverController = PopoverController();
|
||||||
|
|
||||||
|
late final cellBloc = MediaCellBloc(
|
||||||
|
cellController: makeCellController(
|
||||||
|
widget.databaseController,
|
||||||
|
widget.cellContext,
|
||||||
|
).as(),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: cellBloc..add(const MediaCellEvent.initial()),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => widget.skin.build(
|
||||||
|
context,
|
||||||
|
widget.cellContainerNotifier,
|
||||||
|
popoverController,
|
||||||
|
cellBloc,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequestFocus() => popoverController.show();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? onCopy() => null;
|
||||||
|
}
|
@ -0,0 +1,469 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
|
||||||
|
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||||
|
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
|
class MediaCellEditor extends StatefulWidget {
|
||||||
|
const MediaCellEditor({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MediaCellEditor> createState() => _MediaCellEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaCellEditorState extends State<MediaCellEditor> {
|
||||||
|
final addFilePopoverController = PopoverController();
|
||||||
|
final itemMutex = PopoverMutex();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
addFilePopoverController.close();
|
||||||
|
itemMutex.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<MediaCellBloc, MediaCellState>(
|
||||||
|
builder: (_, state) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (state.files.isNotEmpty) ...[
|
||||||
|
ReorderableListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
buildDefaultDragHandles: false,
|
||||||
|
itemBuilder: (_, index) => BlocProvider.value(
|
||||||
|
key: Key(state.files[index].id),
|
||||||
|
value: context.read<MediaCellBloc>(),
|
||||||
|
child: _RenderMedia(
|
||||||
|
file: state.files[index],
|
||||||
|
index: index,
|
||||||
|
enableReordering: state.files.length > 1,
|
||||||
|
mutex: itemMutex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
itemCount: state.files.length,
|
||||||
|
onReorder: (from, to) => context
|
||||||
|
.read<MediaCellBloc>()
|
||||||
|
.add(MediaCellEvent.reorderFiles(from: from, to: to)),
|
||||||
|
proxyDecorator: (child, index, animation) => Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 8),
|
||||||
|
],
|
||||||
|
AppFlowyPopover(
|
||||||
|
controller: addFilePopoverController,
|
||||||
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 250,
|
||||||
|
maxWidth: 250,
|
||||||
|
),
|
||||||
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
popupBuilder: (popoverContext) => FileUploadMenu(
|
||||||
|
onInsertLocalFile: (file) async {
|
||||||
|
if (file.path.isEmpty) return;
|
||||||
|
|
||||||
|
final fileType = file.mimeType?.startsWith('image/') ??
|
||||||
|
false || imgExtensionRegex.hasMatch(file.name)
|
||||||
|
? MediaFileTypePB.Image
|
||||||
|
: MediaFileTypePB.Other;
|
||||||
|
|
||||||
|
final mediaCellBloc = context.read<MediaCellBloc>();
|
||||||
|
|
||||||
|
// Check upload type
|
||||||
|
final userProfile = mediaCellBloc.userProfile;
|
||||||
|
final isLocalMode =
|
||||||
|
userProfile.authenticator == AuthenticatorPB.Local;
|
||||||
|
|
||||||
|
String? path;
|
||||||
|
String? errorMsg;
|
||||||
|
if (isLocalMode) {
|
||||||
|
path = await saveFileToLocalStorage(file.path);
|
||||||
|
} else {
|
||||||
|
(path, errorMsg) = await saveFileToCloudStorage(
|
||||||
|
file.path,
|
||||||
|
mediaCellBloc.databaseId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMsg != null) {
|
||||||
|
return showSnackBarMessage(context, errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaCellBloc.isClosed || path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaCellBloc.add(
|
||||||
|
MediaCellEvent.addFile(
|
||||||
|
url: path,
|
||||||
|
name: file.name,
|
||||||
|
uploadType: isLocalMode
|
||||||
|
? MediaUploadTypePB.LocalMedia
|
||||||
|
: MediaUploadTypePB.CloudMedia,
|
||||||
|
fileType: fileType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
addFilePopoverController.close();
|
||||||
|
},
|
||||||
|
onInsertNetworkFile: (url) {
|
||||||
|
if (url.isEmpty) return;
|
||||||
|
|
||||||
|
final uri = Uri.tryParse(url);
|
||||||
|
if (uri == null) {
|
||||||
|
return showSnackBarMessage(
|
||||||
|
context,
|
||||||
|
'Invalid URL - Please try again',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = uri.pathSegments.last;
|
||||||
|
if (name.isEmpty && uri.pathSegments.length > 1) {
|
||||||
|
name = uri.pathSegments[uri.pathSegments.length - 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
context.read<MediaCellBloc>().add(
|
||||||
|
MediaCellEvent.addFile(
|
||||||
|
url: url,
|
||||||
|
name: name,
|
||||||
|
uploadType: MediaUploadTypePB.NetworkMedia,
|
||||||
|
fileType: MediaFileTypePB.Other,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
addFilePopoverController.close();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: addFilePopoverController.show,
|
||||||
|
child: const FlowyHover(
|
||||||
|
resetHoverOnRebuild: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(4.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
FlowySvg(FlowySvgs.add_s),
|
||||||
|
HSpace(8),
|
||||||
|
FlowyText('Add a file or image'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ToCustomImageType on MediaUploadTypePB {
|
||||||
|
CustomImageType toCustomImageType() => switch (this) {
|
||||||
|
MediaUploadTypePB.NetworkMedia => CustomImageType.external,
|
||||||
|
MediaUploadTypePB.CloudMedia => CustomImageType.internal,
|
||||||
|
_ => CustomImageType.local,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenderMedia extends StatefulWidget {
|
||||||
|
const _RenderMedia({
|
||||||
|
required this.index,
|
||||||
|
required this.file,
|
||||||
|
required this.enableReordering,
|
||||||
|
required this.mutex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final MediaFilePB file;
|
||||||
|
final bool enableReordering;
|
||||||
|
final PopoverMutex mutex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RenderMedia> createState() => __RenderMediaState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __RenderMediaState extends State<_RenderMedia> {
|
||||||
|
bool isHovering = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => isHovering = true),
|
||||||
|
onExit: (_) => setState(() => isHovering = false),
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: isHovering
|
||||||
|
? AFThemeExtension.of(context).greyHover
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ReorderableDragStartListener(
|
||||||
|
index: widget.index,
|
||||||
|
enabled: widget.enableReordering,
|
||||||
|
child: const FlowySvg(FlowySvgs.drag_element_s),
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
if (widget.file.fileType == MediaFileTypePB.Image &&
|
||||||
|
widget.file.uploadType == MediaUploadTypePB.CloudMedia) ...[
|
||||||
|
Expanded(
|
||||||
|
child: _openInteractiveViewer(
|
||||||
|
context,
|
||||||
|
file: widget.file,
|
||||||
|
child: FlowyNetworkImage(
|
||||||
|
url: widget.file.url,
|
||||||
|
userProfilePB:
|
||||||
|
context.read<MediaCellBloc>().userProfile,
|
||||||
|
height: 64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else if (widget.file.fileType == MediaFileTypePB.Image) ...[
|
||||||
|
Expanded(
|
||||||
|
child: _openInteractiveViewer(
|
||||||
|
context,
|
||||||
|
file: widget.file,
|
||||||
|
child: widget.file.uploadType ==
|
||||||
|
MediaUploadTypePB.NetworkMedia
|
||||||
|
? Image.network(
|
||||||
|
widget.file.url,
|
||||||
|
height: 64,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
)
|
||||||
|
: Image.file(
|
||||||
|
File(widget.file.url),
|
||||||
|
height: 64,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => afLaunchUrlString(widget.file.url),
|
||||||
|
child: FlowyText(
|
||||||
|
widget.file.name,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: AppFlowyPopover(
|
||||||
|
mutex: widget.mutex,
|
||||||
|
asBarrier: true,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 150),
|
||||||
|
direction: PopoverDirection.bottomWithRightAligned,
|
||||||
|
popupBuilder: (_) => BlocProvider.value(
|
||||||
|
value: context.read<MediaCellBloc>(),
|
||||||
|
child: _MediaItemMenu(file: widget.file),
|
||||||
|
),
|
||||||
|
child: FlowyIconButton(
|
||||||
|
width: 24,
|
||||||
|
icon: FlowySvg(
|
||||||
|
FlowySvgs.three_dots_s,
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _openInteractiveViewer(
|
||||||
|
BuildContext context, {
|
||||||
|
required MediaFilePB file,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => InteractiveImageViewer(
|
||||||
|
userProfile: context.read<MediaCellBloc>().userProfile,
|
||||||
|
imageProvider: AFBlockImageProvider(
|
||||||
|
images: [
|
||||||
|
ImageBlockData(
|
||||||
|
url: file.url,
|
||||||
|
type: file.uploadType.toCustomImageType(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onDeleteImage: (_) => context
|
||||||
|
.read<MediaCellBloc>()
|
||||||
|
.add(MediaCellEvent.removeFile(fileId: file.id)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaItemMenu extends StatelessWidget {
|
||||||
|
const _MediaItemMenu({
|
||||||
|
required this.file,
|
||||||
|
});
|
||||||
|
|
||||||
|
final MediaFilePB file;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SeparatedColumn(
|
||||||
|
separatorBuilder: () => const VSpace(4),
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (file.fileType == MediaFileTypePB.Image) ...[
|
||||||
|
FlowyButton(
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => InteractiveImageViewer(
|
||||||
|
userProfile: context.read<MediaCellBloc>().userProfile,
|
||||||
|
imageProvider: AFBlockImageProvider(
|
||||||
|
images: [
|
||||||
|
ImageBlockData(
|
||||||
|
url: file.url,
|
||||||
|
type: file.uploadType.toCustomImageType(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onDeleteImage: (_) => context
|
||||||
|
.read<MediaCellBloc>()
|
||||||
|
.add(MediaCellEvent.removeFile(fileId: file.id)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leftIcon: FlowySvg(
|
||||||
|
FlowySvgs.full_view_s,
|
||||||
|
color: Theme.of(context).iconTheme.color,
|
||||||
|
size: const Size.square(18),
|
||||||
|
),
|
||||||
|
text: FlowyText.regular(
|
||||||
|
LocaleKeys.settings_files_open.tr(),
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
leftIconSize: const Size(18, 18),
|
||||||
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
FlowyButton(
|
||||||
|
onTap: () async {
|
||||||
|
if ([MediaUploadTypePB.NetworkMedia, MediaUploadTypePB.LocalMedia]
|
||||||
|
.contains(file.uploadType)) {
|
||||||
|
/// When the file is a network file or a local file, we can directly open the file.
|
||||||
|
await afLaunchUrl(Uri.parse(file.url));
|
||||||
|
} else {
|
||||||
|
final userProfile = context.read<MediaCellBloc>().userProfile;
|
||||||
|
final uri = Uri.parse(file.url);
|
||||||
|
final imgFile = File(uri.pathSegments.last);
|
||||||
|
final savePath = await FilePicker().saveFile(
|
||||||
|
fileName: basename(imgFile.path),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (savePath != null) {
|
||||||
|
final uri = Uri.parse(file.url);
|
||||||
|
|
||||||
|
final token = jsonDecode(userProfile.token)['access_token'];
|
||||||
|
final response = await http.get(
|
||||||
|
uri,
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final imgFile = File(savePath);
|
||||||
|
await imgFile.writeAsBytes(response.bodyBytes);
|
||||||
|
} else if (context.mounted) {
|
||||||
|
showSnapBar(
|
||||||
|
context,
|
||||||
|
LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leftIcon: FlowySvg(
|
||||||
|
FlowySvgs.download_s,
|
||||||
|
color: Theme.of(context).iconTheme.color,
|
||||||
|
size: const Size.square(18),
|
||||||
|
),
|
||||||
|
text: FlowyText.regular(
|
||||||
|
'Download',
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
leftIconSize: const Size(18, 18),
|
||||||
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
),
|
||||||
|
FlowyButton(
|
||||||
|
onTap: () => context.read<MediaCellBloc>().add(
|
||||||
|
MediaCellEvent.removeFile(
|
||||||
|
fileId: file.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leftIcon: FlowySvg(
|
||||||
|
FlowySvgs.delete_s,
|
||||||
|
color: Theme.of(context).iconTheme.color,
|
||||||
|
size: const Size.square(18),
|
||||||
|
),
|
||||||
|
text: FlowyText.regular(
|
||||||
|
LocaleKeys.button_delete.tr(),
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
leftIconSize: const Size(18, 18),
|
||||||
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ const List<FieldType> _supportedFieldTypes = [
|
|||||||
FieldType.Summary,
|
FieldType.Summary,
|
||||||
// FieldType.Time,
|
// FieldType.Time,
|
||||||
FieldType.Translate,
|
FieldType.Translate,
|
||||||
|
FieldType.Media,
|
||||||
];
|
];
|
||||||
|
|
||||||
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/media.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart';
|
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
@ -38,6 +39,7 @@ abstract class TypeOptionEditorFactory {
|
|||||||
FieldType.Summary => const SummaryTypeOptionEditorFactory(),
|
FieldType.Summary => const SummaryTypeOptionEditorFactory(),
|
||||||
FieldType.Time => const TimeTypeOptionEditorFactory(),
|
FieldType.Time => const TimeTypeOptionEditorFactory(),
|
||||||
FieldType.Translate => const TranslateTypeOptionEditorFactory(),
|
FieldType.Translate => const TranslateTypeOptionEditorFactory(),
|
||||||
|
FieldType.Media => const MediaTypeOptionEditorFactory(),
|
||||||
_ => throw UnimplementedError(),
|
_ => throw UnimplementedError(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
|
||||||
|
class MediaTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||||
|
const MediaTypeOptionEditorFactory();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build({
|
||||||
|
required BuildContext context,
|
||||||
|
required String viewId,
|
||||||
|
required FieldPB field,
|
||||||
|
required PopoverMutex popoverMutex,
|
||||||
|
required TypeOptionDataCallback onTypeOptionUpdated,
|
||||||
|
}) =>
|
||||||
|
null;
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart';
|
import 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart';
|
import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_collab_adapter.dart';
|
import 'package:appflowy/plugins/document/application/document_collab_adapter.dart';
|
||||||
@ -32,7 +34,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
|||||||
Position,
|
Position,
|
||||||
paragraphNode;
|
paragraphNode;
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
@ -117,17 +117,19 @@ class DocumentService {
|
|||||||
required String documentId,
|
required String documentId,
|
||||||
}) async {
|
}) async {
|
||||||
final workspace = await FolderEventReadCurrentWorkspace().send();
|
final workspace = await FolderEventReadCurrentWorkspace().send();
|
||||||
return workspace.fold((l) async {
|
return workspace.fold(
|
||||||
final payload = UploadFileParamsPB(
|
(l) async {
|
||||||
workspaceId: l.id,
|
final payload = UploadFileParamsPB(
|
||||||
localFilePath: localFilePath,
|
workspaceId: l.id,
|
||||||
documentId: documentId,
|
localFilePath: localFilePath,
|
||||||
);
|
documentId: documentId,
|
||||||
final result = await DocumentEventUploadFile(payload).send();
|
);
|
||||||
return result;
|
return DocumentEventUploadFile(payload).send();
|
||||||
}, (r) async {
|
},
|
||||||
return FlowyResult.failure(FlowyError(msg: 'Workspace not found'));
|
(r) async {
|
||||||
});
|
return FlowyResult.failure(FlowyError(msg: 'Workspace not found'));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a file from the cloud storage.
|
/// Download a file from the cloud storage.
|
||||||
|
@ -10,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_
|
|||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:cross_file/cross_file.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
@ -237,7 +238,7 @@ class FileBlockComponentState extends State<FileBlockComponent>
|
|||||||
},
|
},
|
||||||
onDragDone: (details) {
|
onDragDone: (details) {
|
||||||
if (dropManagerState.isDropEnabled) {
|
if (dropManagerState.isDropEnabled) {
|
||||||
insertFileFromLocal(details.files.first.path);
|
insertFileFromLocal(details.files.first);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: AppFlowyPopover(
|
child: AppFlowyPopover(
|
||||||
@ -359,7 +360,8 @@ class FileBlockComponentState extends State<FileBlockComponent>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> insertFileFromLocal(String path) async {
|
Future<void> insertFileFromLocal(XFile file) async {
|
||||||
|
final path = file.path;
|
||||||
final documentBloc = context.read<DocumentBloc>();
|
final documentBloc = context.read<DocumentBloc>();
|
||||||
final isLocalMode = documentBloc.isLocalMode;
|
final isLocalMode = documentBloc.isLocalMode;
|
||||||
final urlType = isLocalMode ? FileUrlType.local : FileUrlType.cloud;
|
final urlType = isLocalMode ? FileUrlType.local : FileUrlType.cloud;
|
||||||
@ -382,12 +384,11 @@ class FileBlockComponentState extends State<FileBlockComponent>
|
|||||||
// Remove the file block from the drop state manager
|
// Remove the file block from the drop state manager
|
||||||
dropManagerState.remove(FileBlockKeys.type);
|
dropManagerState.remove(FileBlockKeys.type);
|
||||||
|
|
||||||
final name = Uri.tryParse(path)?.pathSegments.last ?? url;
|
|
||||||
final transaction = editorState.transaction;
|
final transaction = editorState.transaction;
|
||||||
transaction.updateNode(widget.node, {
|
transaction.updateNode(widget.node, {
|
||||||
FileBlockKeys.url: url,
|
FileBlockKeys.url: url,
|
||||||
FileBlockKeys.urlType: urlType.toIntValue(),
|
FileBlockKeys.urlType: urlType.toIntValue(),
|
||||||
FileBlockKeys.name: name,
|
FileBlockKeys.name: file.name,
|
||||||
FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch,
|
FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch,
|
||||||
});
|
});
|
||||||
await editorState.apply(transaction);
|
await editorState.apply(transaction);
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:cross_file/cross_file.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:dotted_border/dotted_border.dart';
|
import 'package:dotted_border/dotted_border.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -21,7 +22,7 @@ class FileUploadMenu extends StatefulWidget {
|
|||||||
required this.onInsertNetworkFile,
|
required this.onInsertNetworkFile,
|
||||||
});
|
});
|
||||||
|
|
||||||
final void Function(String path) onInsertLocalFile;
|
final void Function(XFile file) onInsertLocalFile;
|
||||||
final void Function(String url) onInsertNetworkFile;
|
final void Function(String url) onInsertNetworkFile;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -61,9 +62,9 @@ class _FileUploadMenuState extends State<FileUploadMenu> {
|
|||||||
const Divider(height: 4),
|
const Divider(height: 4),
|
||||||
if (currentTab == 0) ...[
|
if (currentTab == 0) ...[
|
||||||
_FileUploadLocal(
|
_FileUploadLocal(
|
||||||
onFilePicked: (path) {
|
onFilePicked: (file) {
|
||||||
if (path != null) {
|
if (file != null) {
|
||||||
widget.onInsertLocalFile(path);
|
widget.onInsertLocalFile(file);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -98,7 +99,7 @@ class _Tab extends StatelessWidget {
|
|||||||
class _FileUploadLocal extends StatefulWidget {
|
class _FileUploadLocal extends StatefulWidget {
|
||||||
const _FileUploadLocal({required this.onFilePicked});
|
const _FileUploadLocal({required this.onFilePicked});
|
||||||
|
|
||||||
final void Function(String?) onFilePicked;
|
final void Function(XFile?) onFilePicked;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FileUploadLocal> createState() => _FileUploadLocalState();
|
State<_FileUploadLocal> createState() => _FileUploadLocalState();
|
||||||
@ -117,7 +118,7 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
|||||||
child: DropTarget(
|
child: DropTarget(
|
||||||
onDragEntered: (_) => setState(() => isDragging = true),
|
onDragEntered: (_) => setState(() => isDragging = true),
|
||||||
onDragExited: (_) => setState(() => isDragging = false),
|
onDragExited: (_) => setState(() => isDragging = false),
|
||||||
onDragDone: (details) => widget.onFilePicked(details.files.first.path),
|
onDragDone: (details) => widget.onFilePicked(details.files.first),
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@ -181,7 +182,7 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
|||||||
|
|
||||||
Future<void> _uploadFile(BuildContext context) async {
|
Future<void> _uploadFile(BuildContext context) async {
|
||||||
final result = await getIt<FilePickerService>().pickFiles(dialogTitle: '');
|
final result = await getIt<FilePickerService>().pickFiles(dialogTitle: '');
|
||||||
widget.onFilePicked(result?.files.first.path);
|
widget.onFilePicked(result?.files.first.xFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +231,7 @@ class _ImageBrowserLayoutState extends State<ImageBrowserLayout> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const FlowySvg(
|
const FlowySvg(
|
||||||
FlowySvgs.import_s,
|
FlowySvgs.download_s,
|
||||||
size: Size.square(28),
|
size: Size.square(28),
|
||||||
),
|
),
|
||||||
const HSpace(12),
|
const HSpace(12),
|
||||||
|
@ -24,6 +24,7 @@ extension FieldTypeExtension on FieldType {
|
|||||||
FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(),
|
FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(),
|
||||||
FieldType.Time => LocaleKeys.grid_field_timeFieldName.tr(),
|
FieldType.Time => LocaleKeys.grid_field_timeFieldName.tr(),
|
||||||
FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(),
|
FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(),
|
||||||
|
FieldType.Media => LocaleKeys.grid_field_mediaFieldName.tr(),
|
||||||
_ => throw UnimplementedError(),
|
_ => throw UnimplementedError(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ extension FieldTypeExtension on FieldType {
|
|||||||
FieldType.Summary => FlowySvgs.ai_summary_s,
|
FieldType.Summary => FlowySvgs.ai_summary_s,
|
||||||
FieldType.Time => FlowySvgs.timer_start_s,
|
FieldType.Time => FlowySvgs.timer_start_s,
|
||||||
FieldType.Translate => FlowySvgs.ai_translate_s,
|
FieldType.Translate => FlowySvgs.ai_translate_s,
|
||||||
|
FieldType.Media => FlowySvgs.media_s,
|
||||||
_ => throw UnimplementedError(),
|
_ => throw UnimplementedError(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ class InteractiveImageToolbar extends StatelessWidget {
|
|||||||
? currentImage.isLocal
|
? currentImage.isLocal
|
||||||
? FlowySvgs.folder_m
|
? FlowySvgs.folder_m
|
||||||
: FlowySvgs.m_aa_link_s
|
: FlowySvgs.m_aa_link_s
|
||||||
: FlowySvgs.import_s,
|
: FlowySvgs.download_s,
|
||||||
onTap: () => _locateOrDownloadImage(context),
|
onTap: () => _locateOrDownloadImage(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flowy_svg/flowy_svg.dart';
|
import 'package:flowy_svg/flowy_svg.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class FlowyIconButton extends StatelessWidget {
|
class FlowyIconButton extends StatelessWidget {
|
||||||
final double width;
|
final double width;
|
||||||
@ -82,7 +83,6 @@ class FlowyIconButton extends StatelessWidget {
|
|||||||
preferBelow: preferBelow,
|
preferBelow: preferBelow,
|
||||||
message: tooltipMessage,
|
message: tooltipMessage,
|
||||||
richMessage: richTooltipText,
|
richMessage: richTooltipText,
|
||||||
showDuration: Duration.zero,
|
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
|
@ -8,7 +8,6 @@ class FlowyTooltip extends StatelessWidget {
|
|||||||
this.message,
|
this.message,
|
||||||
this.richMessage,
|
this.richMessage,
|
||||||
this.preferBelow,
|
this.preferBelow,
|
||||||
this.showDuration,
|
|
||||||
this.margin,
|
this.margin,
|
||||||
this.verticalOffset,
|
this.verticalOffset,
|
||||||
this.child,
|
this.child,
|
||||||
@ -17,7 +16,6 @@ class FlowyTooltip extends StatelessWidget {
|
|||||||
final String? message;
|
final String? message;
|
||||||
final InlineSpan? richMessage;
|
final InlineSpan? richMessage;
|
||||||
final bool? preferBelow;
|
final bool? preferBelow;
|
||||||
final Duration? showDuration;
|
|
||||||
final EdgeInsetsGeometry? margin;
|
final EdgeInsetsGeometry? margin;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
final double? verticalOffset;
|
final double? verticalOffset;
|
||||||
|
@ -622,10 +622,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flex_seed_scheme
|
name: flex_seed_scheme
|
||||||
sha256: "6c595e545b0678e1fe17e8eec3d1fbca7237482da194fadc20ad8607dc7a7f3d"
|
sha256: cb5b7ec4ba525d9846d8992858a1c6cfc88f9466d96b8850e2a061aa5f682539
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.1.1"
|
||||||
flowy_infra:
|
flowy_infra:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
1
frontend/resources/flowy_icons/16x/download.svg
Normal file
1
frontend/resources/flowy_icons/16x/download.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" id="Download--Streamline-Lucide.svg" height="16" width="16"><desc>Download Streamline Icon: https://streamlinehq.com</desc><path d="M13.125 9.375v2.5a1.25 1.25 0 0 1 -1.25 1.25H3.125a1.25 1.25 0 0 1 -1.25 -1.25v-2.5" stroke-width="1"></path><path d="m4.375 6.25 3.125 3.125 3.125 -3.125" stroke-width="1"></path><path d="m7.5 9.375 0 -7.5" stroke-width="1"></path></svg>
|
After Width: | Height: | Size: 512 B |
1
frontend/resources/flowy_icons/16x/media.svg
Normal file
1
frontend/resources/flowy_icons/16x/media.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" id="Paperclip--Streamline-Lucide.svg" height="16" width="16"><desc>Paperclip Streamline Icon: https://streamlinehq.com</desc><path d="m13.4 6.90625 -5.7437499999999995 5.7437499999999995a3.75 3.75 0 0 1 -5.30625 -5.30625l5.35625 -5.35625A2.5 2.5 0 1 1 11.25 5.525l-5.36875 5.35625a1.25 1.25 0 0 1 -1.76875 -1.76875l5.30625 -5.300000000000001" stroke-width="1"></path></svg>
|
After Width: | Height: | Size: 515 B |
@ -1313,6 +1313,7 @@
|
|||||||
"relationFieldName": "Relation",
|
"relationFieldName": "Relation",
|
||||||
"summaryFieldName": "AI Summary",
|
"summaryFieldName": "AI Summary",
|
||||||
"timeFieldName": "Time",
|
"timeFieldName": "Time",
|
||||||
|
"mediaFieldName": "Files & media",
|
||||||
"translateFieldName": "AI Translate",
|
"translateFieldName": "AI Translate",
|
||||||
"translateTo": "Translate to",
|
"translateTo": "Translate to",
|
||||||
"numberFormat": "Number format",
|
"numberFormat": "Number format",
|
||||||
|
@ -668,6 +668,12 @@ impl<'a> TestRowBuilder<'a> {
|
|||||||
time_field.id.clone()
|
time_field.id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_media_cell(&mut self, media: String) -> String {
|
||||||
|
let media_field = self.field_with_type(&FieldType::Media);
|
||||||
|
self.cell_build.insert_text_cell(&media_field.id, media);
|
||||||
|
media_field.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn field_with_type(&self, field_type: &FieldType) -> Field {
|
pub fn field_with_type(&self, field_type: &FieldType) -> Field {
|
||||||
self
|
self
|
||||||
.fields
|
.fields
|
||||||
|
@ -451,6 +451,7 @@ pub enum FieldType {
|
|||||||
Summary = 11,
|
Summary = 11,
|
||||||
Translate = 12,
|
Translate = 12,
|
||||||
Time = 13,
|
Time = 13,
|
||||||
|
Media = 14,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for FieldType {
|
impl Display for FieldType {
|
||||||
@ -493,6 +494,7 @@ impl FieldType {
|
|||||||
FieldType::Summary => "Summarize",
|
FieldType::Summary => "Summarize",
|
||||||
FieldType::Translate => "Translate",
|
FieldType::Translate => "Translate",
|
||||||
FieldType::Time => "Time",
|
FieldType::Time => "Time",
|
||||||
|
FieldType::Media => "Media",
|
||||||
};
|
};
|
||||||
s.to_string()
|
s.to_string()
|
||||||
}
|
}
|
||||||
@ -553,6 +555,10 @@ impl FieldType {
|
|||||||
matches!(self, FieldType::Time)
|
matches!(self, FieldType::Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_media(&self) -> bool {
|
||||||
|
matches!(self, FieldType::Media)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn can_be_group(&self) -> bool {
|
pub fn can_be_group(&self) -> bool {
|
||||||
self.is_select_option() || self.is_checkbox() || self.is_url()
|
self.is_select_option() || self.is_checkbox() || self.is_url()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
|
use flowy_error::ErrorCode;
|
||||||
|
|
||||||
|
use crate::services::filter::ParseFilterData;
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||||
|
pub struct MediaFilterPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub condition: MediaFilterConditionPB,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum MediaFilterConditionPB {
|
||||||
|
#[default]
|
||||||
|
MediaIsEmpty = 0,
|
||||||
|
MediaIsNotEmpty = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<MediaFilterConditionPB> for u32 {
|
||||||
|
fn from(value: MediaFilterConditionPB) -> Self {
|
||||||
|
value as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::TryFrom<u8> for MediaFilterConditionPB {
|
||||||
|
type Error = ErrorCode;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(MediaFilterConditionPB::MediaIsEmpty),
|
||||||
|
1 => Ok(MediaFilterConditionPB::MediaIsNotEmpty),
|
||||||
|
_ => Err(ErrorCode::InvalidParams),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseFilterData for MediaFilterPB {
|
||||||
|
fn parse(condition: u8, content: String) -> Self {
|
||||||
|
Self {
|
||||||
|
condition: MediaFilterConditionPB::try_from(condition)
|
||||||
|
.unwrap_or(MediaFilterConditionPB::MediaIsEmpty),
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ mod checkbox_filter;
|
|||||||
mod checklist_filter;
|
mod checklist_filter;
|
||||||
mod date_filter;
|
mod date_filter;
|
||||||
mod filter_changeset;
|
mod filter_changeset;
|
||||||
|
mod media_filter;
|
||||||
mod number_filter;
|
mod number_filter;
|
||||||
mod relation_filter;
|
mod relation_filter;
|
||||||
mod select_option_filter;
|
mod select_option_filter;
|
||||||
@ -13,6 +14,7 @@ pub use checkbox_filter::*;
|
|||||||
pub use checklist_filter::*;
|
pub use checklist_filter::*;
|
||||||
pub use date_filter::*;
|
pub use date_filter::*;
|
||||||
pub use filter_changeset::*;
|
pub use filter_changeset::*;
|
||||||
|
pub use media_filter::*;
|
||||||
pub use number_filter::*;
|
pub use number_filter::*;
|
||||||
pub use relation_filter::*;
|
pub use relation_filter::*;
|
||||||
pub use select_option_filter::*;
|
pub use select_option_filter::*;
|
||||||
|
@ -14,6 +14,8 @@ use crate::entities::{
|
|||||||
};
|
};
|
||||||
use crate::services::filter::{Filter, FilterChangeset, FilterInner};
|
use crate::services::filter::{Filter, FilterChangeset, FilterInner};
|
||||||
|
|
||||||
|
use super::MediaFilterPB;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, ProtoBuf_Enum, Eq, PartialEq, Copy)]
|
#[derive(Debug, Default, Clone, ProtoBuf_Enum, Eq, PartialEq, Copy)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum FilterType {
|
pub enum FilterType {
|
||||||
@ -117,6 +119,10 @@ impl From<&Filter> for FilterPB {
|
|||||||
.cloned::<TextFilterPB>()
|
.cloned::<TextFilterPB>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.try_into(),
|
.try_into(),
|
||||||
|
FieldType::Media => condition_and_content
|
||||||
|
.cloned::<MediaFilterPB>()
|
||||||
|
.unwrap()
|
||||||
|
.try_into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@ -170,6 +176,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
|
|||||||
FieldType::Translate => {
|
FieldType::Translate => {
|
||||||
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||||
},
|
},
|
||||||
|
FieldType::Media => {
|
||||||
|
BoxAny::new(MediaFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self::Data {
|
Ok(Self::Data {
|
||||||
|
@ -18,6 +18,7 @@ macro_rules! impl_into_field_type {
|
|||||||
11 => FieldType::Summary,
|
11 => FieldType::Summary,
|
||||||
12 => FieldType::Translate,
|
12 => FieldType::Translate,
|
||||||
13 => FieldType::Time,
|
13 => FieldType::Time,
|
||||||
|
14 => FieldType::Media,
|
||||||
_ => {
|
_ => {
|
||||||
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
|
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
|
||||||
FieldType::RichText
|
FieldType::RichText
|
||||||
|
@ -0,0 +1,152 @@
|
|||||||
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entities::CellIdPB,
|
||||||
|
services::field::{MediaCellData, MediaFile, MediaFileType, MediaUploadType},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||||
|
pub struct MediaCellDataPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub files: Vec<MediaFilePB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaCellData> for MediaCellDataPB {
|
||||||
|
fn from(data: MediaCellData) -> Self {
|
||||||
|
Self {
|
||||||
|
files: data.files.into_iter().map(Into::into).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaCellDataPB> for MediaCellData {
|
||||||
|
fn from(data: MediaCellDataPB) -> Self {
|
||||||
|
Self {
|
||||||
|
files: data.files.into_iter().map(Into::into).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||||
|
pub struct MediaTypeOptionPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub files: Vec<MediaFilePB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||||
|
pub struct MediaFilePB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub url: String,
|
||||||
|
|
||||||
|
#[pb(index = 4)]
|
||||||
|
pub upload_type: MediaUploadTypePB,
|
||||||
|
|
||||||
|
#[pb(index = 5)]
|
||||||
|
pub file_type: MediaFileTypePB,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum MediaUploadTypePB {
|
||||||
|
#[default]
|
||||||
|
LocalMedia = 0,
|
||||||
|
NetworkMedia = 1,
|
||||||
|
CloudMedia = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaUploadType> for MediaUploadTypePB {
|
||||||
|
fn from(data: MediaUploadType) -> Self {
|
||||||
|
match data {
|
||||||
|
MediaUploadType::LocalMedia => MediaUploadTypePB::LocalMedia,
|
||||||
|
MediaUploadType::NetworkMedia => MediaUploadTypePB::NetworkMedia,
|
||||||
|
MediaUploadType::CloudMedia => MediaUploadTypePB::CloudMedia,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaUploadTypePB> for MediaUploadType {
|
||||||
|
fn from(data: MediaUploadTypePB) -> Self {
|
||||||
|
match data {
|
||||||
|
MediaUploadTypePB::LocalMedia => MediaUploadType::LocalMedia,
|
||||||
|
MediaUploadTypePB::NetworkMedia => MediaUploadType::NetworkMedia,
|
||||||
|
MediaUploadTypePB::CloudMedia => MediaUploadType::CloudMedia,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum MediaFileTypePB {
|
||||||
|
#[default]
|
||||||
|
Other = 0,
|
||||||
|
Image = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaFileType> for MediaFileTypePB {
|
||||||
|
fn from(data: MediaFileType) -> Self {
|
||||||
|
match data {
|
||||||
|
MediaFileType::Other => MediaFileTypePB::Other,
|
||||||
|
MediaFileType::Image => MediaFileTypePB::Image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaFileTypePB> for MediaFileType {
|
||||||
|
fn from(data: MediaFileTypePB) -> Self {
|
||||||
|
match data {
|
||||||
|
MediaFileTypePB::Other => MediaFileType::Other,
|
||||||
|
MediaFileTypePB::Image => MediaFileType::Image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaFile> for MediaFilePB {
|
||||||
|
fn from(data: MediaFile) -> Self {
|
||||||
|
Self {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
url: data.url,
|
||||||
|
upload_type: data.upload_type.into(),
|
||||||
|
file_type: data.file_type.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaFilePB> for MediaFile {
|
||||||
|
fn from(data: MediaFilePB) -> Self {
|
||||||
|
Self {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
url: data.url,
|
||||||
|
upload_type: data.upload_type.into(),
|
||||||
|
file_type: data.file_type.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||||
|
pub struct MediaCellChangesetPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub view_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub cell_id: CellIdPB,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub inserted_files: Vec<MediaFilePB>,
|
||||||
|
|
||||||
|
#[pb(index = 4)]
|
||||||
|
pub removed_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct MediaCellChangeset {
|
||||||
|
pub inserted_files: Vec<MediaFile>,
|
||||||
|
pub removed_ids: Vec<String>,
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
mod checkbox_entities;
|
mod checkbox_entities;
|
||||||
mod checklist_entities;
|
mod checklist_entities;
|
||||||
mod date_entities;
|
mod date_entities;
|
||||||
|
mod media_entities;
|
||||||
mod number_entities;
|
mod number_entities;
|
||||||
mod relation_entities;
|
mod relation_entities;
|
||||||
mod select_option_entities;
|
mod select_option_entities;
|
||||||
@ -14,6 +15,7 @@ mod url_entities;
|
|||||||
pub use checkbox_entities::*;
|
pub use checkbox_entities::*;
|
||||||
pub use checklist_entities::*;
|
pub use checklist_entities::*;
|
||||||
pub use date_entities::*;
|
pub use date_entities::*;
|
||||||
|
pub use media_entities::*;
|
||||||
pub use number_entities::*;
|
pub use number_entities::*;
|
||||||
pub use relation_entities::*;
|
pub use relation_entities::*;
|
||||||
pub use select_option_entities::*;
|
pub use select_option_entities::*;
|
||||||
|
@ -54,9 +54,8 @@ pub struct RepeatedSelectOptionPayload {
|
|||||||
pub items: Vec<SelectOptionPB>,
|
pub items: Vec<SelectOptionPB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum, PartialEq, Eq, Debug, Clone)]
|
#[derive(ProtoBuf_Enum, PartialEq, Eq, Debug, Clone, Default)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Default)]
|
|
||||||
pub enum SelectOptionColorPB {
|
pub enum SelectOptionColorPB {
|
||||||
#[default]
|
#[default]
|
||||||
Purple = 0,
|
Purple = 0,
|
||||||
|
@ -1261,3 +1261,31 @@ pub(crate) async fn translate_row_handler(
|
|||||||
rx.await??;
|
rx.await??;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||||
|
pub(crate) async fn update_media_cell_handler(
|
||||||
|
data: AFPluginData<MediaCellChangesetPB>,
|
||||||
|
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let manager = upgrade_manager(manager)?;
|
||||||
|
let params: MediaCellChangesetPB = data.into_inner();
|
||||||
|
let cell_id: CellIdParams = params.cell_id.try_into()?;
|
||||||
|
let cell_changeset = MediaCellChangeset {
|
||||||
|
inserted_files: params.inserted_files.into_iter().map(Into::into).collect(),
|
||||||
|
removed_ids: params.removed_ids,
|
||||||
|
};
|
||||||
|
|
||||||
|
let database_editor = manager
|
||||||
|
.get_database_editor_with_view_id(&cell_id.view_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
database_editor
|
||||||
|
.update_cell_with_changeset(
|
||||||
|
&cell_id.view_id,
|
||||||
|
&cell_id.row_id,
|
||||||
|
&cell_id.field_id,
|
||||||
|
BoxAny::new(cell_changeset),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -94,6 +94,8 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
|
|||||||
// AI
|
// AI
|
||||||
.event(DatabaseEvent::SummarizeRow, summarize_row_handler)
|
.event(DatabaseEvent::SummarizeRow, summarize_row_handler)
|
||||||
.event(DatabaseEvent::TranslateRow, translate_row_handler)
|
.event(DatabaseEvent::TranslateRow, translate_row_handler)
|
||||||
|
// Media
|
||||||
|
.event(DatabaseEvent::UpdateMediaCell, update_media_cell_handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
|
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
|
||||||
@ -385,4 +387,7 @@ pub enum DatabaseEvent {
|
|||||||
|
|
||||||
#[event(input = "DatabaseViewIdPB", output = "RepeatedRowMetaPB")]
|
#[event(input = "DatabaseViewIdPB", output = "RepeatedRowMetaPB")]
|
||||||
GetAllRows = 177,
|
GetAllRows = 177,
|
||||||
|
|
||||||
|
#[event(input = "MediaCellChangesetPB")]
|
||||||
|
UpdateMediaCell = 178,
|
||||||
}
|
}
|
||||||
|
@ -219,7 +219,7 @@ impl<'a> CellBuilder<'a> {
|
|||||||
if let Some(field) = field_maps.get(&field_id) {
|
if let Some(field) = field_maps.get(&field_id) {
|
||||||
let field_type = FieldType::from(field.field_type);
|
let field_type = FieldType::from(field.field_type);
|
||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => {
|
FieldType::RichText | FieldType::Translate | FieldType::Summary => {
|
||||||
cells.insert(field_id, insert_text_cell(cell_str, field));
|
cells.insert(field_id, insert_text_cell(cell_str, field));
|
||||||
},
|
},
|
||||||
FieldType::Number | FieldType::Time => {
|
FieldType::Number | FieldType::Time => {
|
||||||
@ -259,11 +259,8 @@ impl<'a> CellBuilder<'a> {
|
|||||||
FieldType::Relation => {
|
FieldType::Relation => {
|
||||||
cells.insert(field_id, (&RelationCellData::from(cell_str)).into());
|
cells.insert(field_id, (&RelationCellData::from(cell_str)).into());
|
||||||
},
|
},
|
||||||
FieldType::Summary => {
|
FieldType::Media => {
|
||||||
cells.insert(field_id, insert_text_cell(cell_str, field));
|
cells.insert(field_id, (&MediaCellData::from(cell_str)).into());
|
||||||
},
|
|
||||||
FieldType::Translate => {
|
|
||||||
cells.insert(field_id, insert_text_cell(cell_str, field));
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ use crate::entities::FieldType;
|
|||||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||||
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
|
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
|
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MediaTypeOption, MultiSelectTypeOption,
|
||||||
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption,
|
NumberTypeOption, RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption,
|
||||||
TimestampTypeOption, TypeOptionTransform, URLTypeOption,
|
TimestampTypeOption, TypeOptionTransform, URLTypeOption,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@ -127,5 +127,8 @@ fn get_type_option_transform_handler(
|
|||||||
FieldType::Translate => {
|
FieldType::Translate => {
|
||||||
Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||||
},
|
},
|
||||||
|
FieldType::Media => {
|
||||||
|
Box::new(MediaTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct MediaFile {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub upload_type: MediaUploadType,
|
||||||
|
pub file_type: MediaFileType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MediaFile {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"MediaFile(id: {}, name: {}, url: {}, upload_type: {:?}, file_type: {:?})",
|
||||||
|
self.id, self.name, self.url, self.upload_type, self.file_type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Default, Clone)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum MediaUploadType {
|
||||||
|
#[default]
|
||||||
|
LocalMedia = 0,
|
||||||
|
NetworkMedia = 1,
|
||||||
|
CloudMedia = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Default, Clone)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum MediaFileType {
|
||||||
|
#[default]
|
||||||
|
Other = 0,
|
||||||
|
Image = 1,
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
use collab_database::{fields::Field, rows::Cell};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entities::{MediaFilterConditionPB, MediaFilterPB},
|
||||||
|
services::{cell::insert_text_cell, filter::PreFillCellsWithFilter},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl MediaFilterPB {
|
||||||
|
pub fn is_visible<T: AsRef<str>>(&self, cell_data: T) -> bool {
|
||||||
|
let cell_data = cell_data.as_ref().to_lowercase();
|
||||||
|
match self.condition {
|
||||||
|
MediaFilterConditionPB::MediaIsEmpty => cell_data.is_empty(),
|
||||||
|
MediaFilterConditionPB::MediaIsNotEmpty => !cell_data.is_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreFillCellsWithFilter for MediaFilterPB {
|
||||||
|
fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) {
|
||||||
|
let text = match self.condition {
|
||||||
|
MediaFilterConditionPB::MediaIsNotEmpty if !self.content.is_empty() => {
|
||||||
|
Some(self.content.clone())
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let open_after_create = matches!(self.condition, MediaFilterConditionPB::MediaIsNotEmpty);
|
||||||
|
|
||||||
|
(text.map(|s| insert_text_cell(s, field)), open_after_create)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,255 @@
|
|||||||
|
use std::{cmp::Ordering, sync::Arc};
|
||||||
|
|
||||||
|
use collab::{preclude::Any, util::AnyMapExt};
|
||||||
|
use collab_database::{
|
||||||
|
fields::{Field, TypeOptionData, TypeOptionDataBuilder},
|
||||||
|
rows::{new_cell_builder, Cell},
|
||||||
|
};
|
||||||
|
use flowy_error::FlowyResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entities::{FieldType, MediaCellChangeset, MediaCellDataPB, MediaFilterPB, MediaTypeOptionPB},
|
||||||
|
services::{
|
||||||
|
cell::{CellDataChangeset, CellDataDecoder},
|
||||||
|
field::{
|
||||||
|
default_order, StringCellData, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
|
||||||
|
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, CELL_DATA,
|
||||||
|
},
|
||||||
|
sort::SortCondition,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::MediaFile;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
pub struct MediaCellData {
|
||||||
|
pub files: Vec<MediaFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Cell> for MediaCellData {
|
||||||
|
fn from(cell: &Cell) -> Self {
|
||||||
|
let files = match cell.get(CELL_DATA) {
|
||||||
|
Some(Any::Array(array)) => array
|
||||||
|
.iter()
|
||||||
|
.flat_map(|item| {
|
||||||
|
if let Any::String(string) = item {
|
||||||
|
Some(serde_json::from_str::<MediaFile>(string).unwrap_or_default())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
_ => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
Self { files }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&MediaCellData> for Cell {
|
||||||
|
fn from(value: &MediaCellData) -> Self {
|
||||||
|
let data = Any::Array(Arc::from(
|
||||||
|
value
|
||||||
|
.files
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|file| Any::String(Arc::from(serde_json::to_string(&file).unwrap_or_default())))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut cell = new_cell_builder(FieldType::Media);
|
||||||
|
cell.insert(CELL_DATA.into(), data);
|
||||||
|
cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for MediaCellData {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
if s.is_empty() {
|
||||||
|
return MediaCellData { files: vec![] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = s
|
||||||
|
.split(", ")
|
||||||
|
.map(|file: &str| serde_json::from_str::<MediaFile>(file).unwrap_or_default())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
MediaCellData { files }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionCellData for MediaCellData {
|
||||||
|
fn is_cell_empty(&self) -> bool {
|
||||||
|
self.files.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for MediaCellData {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self
|
||||||
|
.files
|
||||||
|
.iter()
|
||||||
|
.map(|file| file.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct MediaTypeOption {
|
||||||
|
#[serde(default)]
|
||||||
|
pub files: Vec<MediaFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOption for MediaTypeOption {
|
||||||
|
type CellData = MediaCellData;
|
||||||
|
type CellChangeset = MediaCellChangeset;
|
||||||
|
type CellProtobufType = MediaCellDataPB;
|
||||||
|
type CellFilter = MediaFilterPB;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TypeOptionData> for MediaTypeOption {
|
||||||
|
fn from(data: TypeOptionData) -> Self {
|
||||||
|
data
|
||||||
|
.get_as::<String>("content")
|
||||||
|
.map(|s| serde_json::from_str::<MediaTypeOption>(&s).unwrap_or_default())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaTypeOption> for TypeOptionData {
|
||||||
|
fn from(data: MediaTypeOption) -> Self {
|
||||||
|
let content = serde_json::to_string(&data).unwrap_or_default();
|
||||||
|
TypeOptionDataBuilder::from([("content".into(), content.into())])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaTypeOption> for MediaTypeOptionPB {
|
||||||
|
fn from(value: MediaTypeOption) -> Self {
|
||||||
|
Self {
|
||||||
|
files: value.files.into_iter().map(Into::into).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaTypeOptionPB> for MediaTypeOption {
|
||||||
|
fn from(value: MediaTypeOptionPB) -> Self {
|
||||||
|
Self {
|
||||||
|
files: value.files.into_iter().map(Into::into).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionTransform for MediaTypeOption {}
|
||||||
|
|
||||||
|
impl TypeOptionCellDataSerde for MediaTypeOption {
|
||||||
|
fn protobuf_encode(
|
||||||
|
&self,
|
||||||
|
cell_data: <Self as TypeOption>::CellData,
|
||||||
|
) -> <Self as TypeOption>::CellProtobufType {
|
||||||
|
cell_data.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||||
|
Ok(cell.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CellDataDecoder for MediaTypeOption {
|
||||||
|
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||||
|
self.parse_cell(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_cell_with_transform(
|
||||||
|
&self,
|
||||||
|
_cell: &Cell,
|
||||||
|
from_field_type: FieldType,
|
||||||
|
_field: &Field,
|
||||||
|
) -> Option<<Self as TypeOption>::CellData> {
|
||||||
|
match from_field_type {
|
||||||
|
FieldType::RichText
|
||||||
|
| FieldType::Number
|
||||||
|
| FieldType::DateTime
|
||||||
|
| FieldType::SingleSelect
|
||||||
|
| FieldType::MultiSelect
|
||||||
|
| FieldType::Checkbox
|
||||||
|
| FieldType::URL
|
||||||
|
| FieldType::Summary
|
||||||
|
| FieldType::Translate
|
||||||
|
| FieldType::Time
|
||||||
|
| FieldType::Checklist
|
||||||
|
| FieldType::LastEditedTime
|
||||||
|
| FieldType::CreatedTime
|
||||||
|
| FieldType::Relation
|
||||||
|
| FieldType::Media => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
|
||||||
|
cell_data.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
|
||||||
|
StringCellData::from(cell).0.parse::<f64>().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CellDataChangeset for MediaTypeOption {
|
||||||
|
fn apply_changeset(
|
||||||
|
&self,
|
||||||
|
changeset: <Self as TypeOption>::CellChangeset,
|
||||||
|
cell: Option<Cell>,
|
||||||
|
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
||||||
|
if cell.is_none() {
|
||||||
|
let cell_data = MediaCellData {
|
||||||
|
files: changeset.inserted_files,
|
||||||
|
};
|
||||||
|
return Ok(((&cell_data).into(), cell_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell_data: MediaCellData = MediaCellData::from(&cell.unwrap());
|
||||||
|
let mut files = cell_data.files.clone();
|
||||||
|
for removed_id in changeset.removed_ids.iter() {
|
||||||
|
if let Some(index) = files.iter().position(|file| file.id == removed_id.clone()) {
|
||||||
|
files.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for inserted in changeset.inserted_files.iter() {
|
||||||
|
if !files.iter().any(|file| file.id == inserted.id) {
|
||||||
|
files.push(inserted.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell_data = MediaCellData { files };
|
||||||
|
|
||||||
|
Ok((Cell::from(&cell_data), cell_data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionCellDataFilter for MediaTypeOption {
|
||||||
|
fn apply_filter(
|
||||||
|
&self,
|
||||||
|
_filter: &<Self as TypeOption>::CellFilter,
|
||||||
|
_cell_data: &<Self as TypeOption>::CellData,
|
||||||
|
) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionCellDataCompare for MediaTypeOption {
|
||||||
|
fn apply_cmp(
|
||||||
|
&self,
|
||||||
|
cell_data: &<Self as TypeOption>::CellData,
|
||||||
|
other_cell_data: &<Self as TypeOption>::CellData,
|
||||||
|
_sort_condition: SortCondition,
|
||||||
|
) -> Ordering {
|
||||||
|
match (cell_data.files.is_empty(), other_cell_data.is_cell_empty()) {
|
||||||
|
(true, true) => Ordering::Equal,
|
||||||
|
(true, false) => Ordering::Greater,
|
||||||
|
(false, true) => Ordering::Less,
|
||||||
|
(false, false) => default_order(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
#![allow(clippy::module_inception)]
|
||||||
|
mod media_file;
|
||||||
|
mod media_filter;
|
||||||
|
mod media_type_option;
|
||||||
|
|
||||||
|
pub use media_file::*;
|
||||||
|
pub use media_type_option::*;
|
@ -1,6 +1,7 @@
|
|||||||
pub mod checkbox_type_option;
|
pub mod checkbox_type_option;
|
||||||
pub mod checklist_type_option;
|
pub mod checklist_type_option;
|
||||||
pub mod date_type_option;
|
pub mod date_type_option;
|
||||||
|
pub mod media_type_option;
|
||||||
pub mod number_type_option;
|
pub mod number_type_option;
|
||||||
pub mod relation_type_option;
|
pub mod relation_type_option;
|
||||||
pub mod selection_type_option;
|
pub mod selection_type_option;
|
||||||
@ -17,6 +18,7 @@ mod util;
|
|||||||
pub use checkbox_type_option::*;
|
pub use checkbox_type_option::*;
|
||||||
pub use checklist_type_option::*;
|
pub use checklist_type_option::*;
|
||||||
pub use date_type_option::*;
|
pub use date_type_option::*;
|
||||||
|
pub use media_type_option::*;
|
||||||
pub use number_type_option::*;
|
pub use number_type_option::*;
|
||||||
pub use relation_type_option::*;
|
pub use relation_type_option::*;
|
||||||
pub use selection_type_option::*;
|
pub use selection_type_option::*;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::entities::SelectOptionCellDataPB;
|
use crate::entities::SelectOptionCellDataPB;
|
||||||
use crate::services::field::SelectOptionIds;
|
use crate::services::field::SelectOptionIds;
|
||||||
use collab_database::entity::SelectOption;
|
use collab_database::entity::SelectOption;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SelectOptionCellData {
|
pub struct SelectOptionCellData {
|
||||||
pub select_options: Vec<SelectOption>,
|
pub select_options: Vec<SelectOption>,
|
||||||
|
@ -81,6 +81,7 @@ impl CellDataDecoder for RichTextTypeOption {
|
|||||||
| FieldType::URL
|
| FieldType::URL
|
||||||
| FieldType::Summary
|
| FieldType::Summary
|
||||||
| FieldType::Translate
|
| FieldType::Translate
|
||||||
|
| FieldType::Media
|
||||||
| FieldType::Time => Some(StringCellData::from(stringify_cell(cell, field))),
|
| FieldType::Time => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||||
FieldType::Checklist
|
FieldType::Checklist
|
||||||
| FieldType::LastEditedTime
|
| FieldType::LastEditedTime
|
||||||
|
@ -10,7 +10,7 @@ use std::fmt::Debug;
|
|||||||
use flowy_error::FlowyResult;
|
use flowy_error::FlowyResult;
|
||||||
|
|
||||||
use crate::entities::{
|
use crate::entities::{
|
||||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
|
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType, MediaTypeOptionPB,
|
||||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
|
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
|
||||||
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimeTypeOptionPB, TimestampTypeOptionPB,
|
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimeTypeOptionPB, TimestampTypeOptionPB,
|
||||||
TranslateTypeOptionPB, URLTypeOptionPB,
|
TranslateTypeOptionPB, URLTypeOptionPB,
|
||||||
@ -20,8 +20,9 @@ use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
|||||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||||
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
|
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
|
CheckboxTypeOption, DateTypeOption, MediaTypeOption, MultiSelectTypeOption, NumberTypeOption,
|
||||||
RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption, TimestampTypeOption, URLTypeOption,
|
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption,
|
||||||
|
TimestampTypeOption, URLTypeOption,
|
||||||
};
|
};
|
||||||
use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter};
|
use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter};
|
||||||
use crate::services::sort::SortCondition;
|
use crate::services::sort::SortCondition;
|
||||||
@ -197,6 +198,9 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
|
|||||||
FieldType::Translate => {
|
FieldType::Translate => {
|
||||||
TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into())
|
TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into())
|
||||||
},
|
},
|
||||||
|
FieldType::Media => {
|
||||||
|
MediaTypeOptionPB::try_from(bytes).map(|pb| MediaTypeOption::from(pb).into())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +278,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
|
|||||||
.try_into()
|
.try_into()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
},
|
},
|
||||||
|
FieldType::Media => {
|
||||||
|
let media_type_option: MediaTypeOption = type_option.into();
|
||||||
|
MediaTypeOptionPB::from(media_type_option)
|
||||||
|
.try_into()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,5 +306,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
|
|||||||
FieldType::Summary => SummarizationTypeOption::default().into(),
|
FieldType::Summary => SummarizationTypeOption::default().into(),
|
||||||
FieldType::Translate => TranslateTypeOption::default().into(),
|
FieldType::Translate => TranslateTypeOption::default().into(),
|
||||||
FieldType::Time => TimeTypeOption.into(),
|
FieldType::Time => TimeTypeOption.into(),
|
||||||
|
FieldType::Media => MediaTypeOption::default().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellP
|
|||||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||||
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
|
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
|
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MediaTypeOption, MultiSelectTypeOption,
|
||||||
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption,
|
NumberTypeOption, RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption,
|
||||||
TimestampTypeOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
|
TimestampTypeOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
|
||||||
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
|
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
|
||||||
};
|
};
|
||||||
@ -496,6 +496,16 @@ impl<'a> TypeOptionCellExt<'a> {
|
|||||||
self.cell_data_cache.clone(),
|
self.cell_data_cache.clone(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
FieldType::Media => self
|
||||||
|
.field
|
||||||
|
.get_type_option::<MediaTypeOption>(field_type)
|
||||||
|
.map(|type_option| {
|
||||||
|
TypeOptionCellDataHandlerImpl::new_with_boxed(
|
||||||
|
type_option,
|
||||||
|
field_type,
|
||||||
|
self.cell_data_cache.clone(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ use tracing::error;
|
|||||||
|
|
||||||
use crate::entities::{
|
use crate::entities::{
|
||||||
CheckboxFilterPB, ChecklistFilterPB, DateFilterContent, DateFilterPB, FieldType, FilterType,
|
CheckboxFilterPB, ChecklistFilterPB, DateFilterContent, DateFilterPB, FieldType, FilterType,
|
||||||
InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB,
|
InsertedRowPB, MediaFilterPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB,
|
||||||
TimeFilterPB,
|
TextFilterPB, TimeFilterPB,
|
||||||
};
|
};
|
||||||
use crate::services::field::SelectOptionIds;
|
use crate::services::field::SelectOptionIds;
|
||||||
|
|
||||||
@ -287,6 +287,7 @@ impl FilterInner {
|
|||||||
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
|
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
|
||||||
FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
|
FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
|
||||||
FieldType::Time => BoxAny::new(TimeFilterPB::parse(condition as u8, content)),
|
FieldType::Time => BoxAny::new(TimeFilterPB::parse(condition as u8, content)),
|
||||||
|
FieldType::Media => BoxAny::new(MediaFilterPB::parse(condition as u8, content)),
|
||||||
};
|
};
|
||||||
|
|
||||||
FilterInner::Data {
|
FilterInner::Data {
|
||||||
@ -388,6 +389,10 @@ impl<'a> From<&'a Filter> for FilterMap {
|
|||||||
let filter = condition_and_content.cloned::<TextFilterPB>()?;
|
let filter = condition_and_content.cloned::<TextFilterPB>()?;
|
||||||
(filter.condition as u8, filter.content)
|
(filter.condition as u8, filter.content)
|
||||||
},
|
},
|
||||||
|
FieldType::Media => {
|
||||||
|
let filter = condition_and_content.cloned::<MediaFilterPB>()?;
|
||||||
|
(filter.condition as u8, filter.content)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Some((condition, content))
|
Some((condition, content))
|
||||||
};
|
};
|
||||||
|
@ -141,7 +141,7 @@ pub fn make_test_board() -> DatabaseData {
|
|||||||
.build();
|
.build();
|
||||||
fields.push(time_field);
|
fields.push(time_field);
|
||||||
},
|
},
|
||||||
FieldType::Translate => {},
|
FieldType::Translate | FieldType::Media => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ use flowy_database2::entities::FieldType;
|
|||||||
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
|
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||||
use flowy_database2::services::field::translate_type_option::translate::TranslateTypeOption;
|
use flowy_database2::services::field::translate_type_option::translate::TranslateTypeOption;
|
||||||
use flowy_database2::services::field::{
|
use flowy_database2::services::field::{
|
||||||
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
|
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MediaTypeOption,
|
||||||
NumberFormat, NumberTypeOption, RelationTypeOption, SingleSelectTypeOption, TimeFormat,
|
MultiSelectTypeOption, NumberFormat, NumberTypeOption, RelationTypeOption,
|
||||||
TimeTypeOption, TimestampTypeOption,
|
SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption,
|
||||||
};
|
};
|
||||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||||
|
|
||||||
@ -151,6 +151,14 @@ pub fn make_test_grid() -> DatabaseData {
|
|||||||
.build();
|
.build();
|
||||||
fields.push(translate_field);
|
fields.push(translate_field);
|
||||||
},
|
},
|
||||||
|
FieldType::Media => {
|
||||||
|
let type_option = MediaTypeOption { files: vec![] };
|
||||||
|
|
||||||
|
let media_field = FieldBuilder::new(field_type, type_option)
|
||||||
|
.name("Media")
|
||||||
|
.build();
|
||||||
|
fields.push(media_field);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,15 +76,16 @@ async fn export_and_then_import_meta_csv_test() {
|
|||||||
assert_eq!(s, "Google,Facebook");
|
assert_eq!(s, "Google,Facebook");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
FieldType::Checkbox => {},
|
FieldType::Checkbox
|
||||||
FieldType::URL => {},
|
| FieldType::URL
|
||||||
FieldType::Checklist => {},
|
| FieldType::Checklist
|
||||||
FieldType::LastEditedTime => {},
|
| FieldType::LastEditedTime
|
||||||
FieldType::CreatedTime => {},
|
| FieldType::CreatedTime
|
||||||
FieldType::Relation => {},
|
| FieldType::Relation
|
||||||
FieldType::Summary => {},
|
| FieldType::Summary
|
||||||
FieldType::Time => {},
|
| FieldType::Time
|
||||||
FieldType::Translate => {},
|
| FieldType::Translate
|
||||||
|
| FieldType::Media => {},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
panic!(
|
panic!(
|
||||||
@ -163,13 +164,14 @@ async fn history_database_import_test() {
|
|||||||
assert_eq!(s, "AppFlowy website - https://www.appflowy.io");
|
assert_eq!(s, "AppFlowy website - https://www.appflowy.io");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
FieldType::Checklist => {},
|
FieldType::Checklist
|
||||||
FieldType::LastEditedTime => {},
|
| FieldType::LastEditedTime
|
||||||
FieldType::CreatedTime => {},
|
| FieldType::CreatedTime
|
||||||
FieldType::Relation => {},
|
| FieldType::Relation
|
||||||
FieldType::Summary => {},
|
| FieldType::Summary
|
||||||
FieldType::Time => {},
|
| FieldType::Time
|
||||||
FieldType::Translate => {},
|
| FieldType::Translate
|
||||||
|
| FieldType::Media => {},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
panic!(
|
panic!(
|
||||||
|
Loading…
Reference in New Issue
Block a user