mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-30 00:38:39 +00:00
feat: media launch review (#6198)
* fix: unexpected thrown exceptions~ * fix: wrap toggle rebuild * fix: copywriting + checkbox group * fix: only show item menu on hover * feat: click to open image in viewer * feat: improve row detail ux * fix: add delete confirmation dialog --------- Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
This commit is contained in:
parent
006ea02bfe
commit
614f3ce81b
@ -147,6 +147,8 @@ class FieldOptionValues {
|
||||
timeFormat: timeFormat,
|
||||
includeTime: includeTime,
|
||||
).writeToBuffer();
|
||||
case FieldType.Media:
|
||||
return MediaTypeOptionPB().writeToBuffer();
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@ -140,6 +140,9 @@ class MediaCellBloc extends Bloc<MediaCellEvent, MediaCellState> {
|
||||
final result = await DatabaseEventRenameMediaFile(payload).send();
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
},
|
||||
toggleShowAllFiles: () {
|
||||
emit(state.copyWith(showAllFiles: !state.showAllFiles));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -199,6 +202,8 @@ class MediaCellEvent with _$MediaCellEvent {
|
||||
required String fileId,
|
||||
required String name,
|
||||
}) = _RenameFile;
|
||||
|
||||
const factory MediaCellEvent.toggleShowAllFiles() = _ToggleShowAllFiles;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -207,6 +212,7 @@ class MediaCellState with _$MediaCellState {
|
||||
UserProfilePB? userProfile,
|
||||
required String fieldName,
|
||||
@Default([]) List<MediaFilePB> files,
|
||||
@Default(false) showAllFiles,
|
||||
}) = _MediaCellState;
|
||||
|
||||
factory MediaCellState.initial(MediaCellController cellController) {
|
||||
|
||||
@ -200,11 +200,15 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
|
||||
Widget _buildHeaderIcon(GroupData customData) =>
|
||||
switch (customData.fieldType) {
|
||||
FieldType.Checkbox => FlowySvg(
|
||||
customData.asCheckboxGroup()!.isCheck
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
FieldType.Checkbox => Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: FlowySvg(
|
||||
customData.asCheckboxGroup()!.isCheck
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
size: const Size.square(18),
|
||||
),
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
|
||||
@ -10,15 +10,18 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.
|
||||
import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block_menu.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/util/theme_extension.dart';
|
||||
import 'package:appflowy/util/xfile_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.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_popover/appflowy_popover.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
@ -26,11 +29,15 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
const _defaultFilesToDisplay = 5;
|
||||
|
||||
class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
|
||||
final mutex = PopoverMutex();
|
||||
final addFileController = PopoverController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
addFileController.close();
|
||||
mutex.dispose();
|
||||
}
|
||||
|
||||
@ -46,38 +53,28 @@ class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
|
||||
child: Builder(
|
||||
builder: (context) => BlocBuilder<MediaCellBloc, MediaCellState>(
|
||||
builder: (context, state) {
|
||||
final filesToDisplay = state.files.take(4).toList();
|
||||
final extraCount = state.files.length - filesToDisplay.length;
|
||||
final filesToDisplay = state.showAllFiles
|
||||
? state.files
|
||||
: state.files.take(_defaultFilesToDisplay).toList();
|
||||
final extraCount = state.files.length - _defaultFilesToDisplay;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (state.files.isEmpty) {
|
||||
return GestureDetector(
|
||||
onTap: () => popoverController.show(),
|
||||
child: AppFlowyPopover(
|
||||
mutex: mutex,
|
||||
controller: popoverController,
|
||||
asBarrier: true,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 250,
|
||||
maxWidth: 250,
|
||||
maxHeight: 400,
|
||||
return _AddFileButton(
|
||||
controller: addFileController,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
mutex: mutex,
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor:
|
||||
AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
offset: const Offset(0, 10),
|
||||
margin: EdgeInsets.zero,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (_) => BlocProvider.value(
|
||||
value: context.read<MediaCellBloc>(),
|
||||
child: const MediaCellEditor(),
|
||||
),
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor:
|
||||
AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: addFileController.show,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
@ -99,52 +96,6 @@ class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
|
||||
runSpacing: 12,
|
||||
spacing: 12,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
AppFlowyPopover(
|
||||
mutex: mutex,
|
||||
controller: popoverController,
|
||||
asBarrier: true,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 250,
|
||||
maxWidth: 250,
|
||||
maxHeight: 400,
|
||||
),
|
||||
offset: const Offset(0, 10),
|
||||
margin: EdgeInsets.zero,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (_) => BlocProvider.value(
|
||||
value: context.read<MediaCellBloc>(),
|
||||
child: const MediaCellEditor(),
|
||||
),
|
||||
onClose: () =>
|
||||
cellContainerNotifier.isFocus = false,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
),
|
||||
child: FlowyHover(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const FlowySvg(FlowySvgs.edit_s),
|
||||
const HSpace(4),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.button_edit.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...filesToDisplay.map(
|
||||
(file) => _FilePreviewRender(
|
||||
key: ValueKey(file.id),
|
||||
@ -153,13 +104,39 @@ class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
|
||||
mutex: mutex,
|
||||
),
|
||||
),
|
||||
if (extraCount > 0)
|
||||
_ExtraInfo(
|
||||
extraCount: extraCount,
|
||||
controller: popoverController,
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size / 2,
|
||||
child: _AddFileButton(
|
||||
controller: addFileController,
|
||||
mutex: mutex,
|
||||
cellContainerNotifier: cellContainerNotifier,
|
||||
child: FlowyHover(
|
||||
resetHoverOnRebuild: false,
|
||||
child: GestureDetector(
|
||||
onTap: addFileController.show,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
size: Size.square(24),
|
||||
),
|
||||
const VSpace(4),
|
||||
FlowyText(
|
||||
LocaleKeys.grid_media_addFileOrImage.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (extraCount > 0)
|
||||
_ShowAllFilesButton(extraCount: extraCount),
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -172,6 +149,91 @@ class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
|
||||
}
|
||||
}
|
||||
|
||||
class _AddFileButton extends StatelessWidget {
|
||||
const _AddFileButton({
|
||||
this.mutex,
|
||||
required this.controller,
|
||||
this.direction = PopoverDirection.bottomWithCenterAligned,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final PopoverController controller;
|
||||
final PopoverMutex? mutex;
|
||||
final PopoverDirection direction;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
controller: controller,
|
||||
mutex: mutex,
|
||||
offset: const Offset(0, 10),
|
||||
direction: direction,
|
||||
popupBuilder: (_) => FileUploadMenu(
|
||||
onInsertLocalFile: (file) => insertLocalFile(
|
||||
context,
|
||||
file,
|
||||
userProfile: context.read<MediaCellBloc>().state.userProfile,
|
||||
documentId: context.read<MediaCellBloc>().rowId,
|
||||
onUploadSuccess: (path, isLocalMode) {
|
||||
final mediaCellBloc = context.read<MediaCellBloc>();
|
||||
if (mediaCellBloc.isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaCellBloc.add(
|
||||
MediaCellEvent.addFile(
|
||||
url: path,
|
||||
name: file.name,
|
||||
uploadType: isLocalMode
|
||||
? MediaUploadTypePB.LocalMedia
|
||||
: MediaUploadTypePB.CloudMedia,
|
||||
fileType: file.fileType.toMediaFileTypePB(),
|
||||
),
|
||||
);
|
||||
|
||||
controller.close();
|
||||
},
|
||||
),
|
||||
onInsertNetworkFile: (url) {
|
||||
if (url.isEmpty) return;
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fakeFile = XFile(uri.path);
|
||||
MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB();
|
||||
fileType = fileType == MediaFileTypePB.Other
|
||||
? MediaFileTypePB.Link
|
||||
: fileType;
|
||||
|
||||
String name =
|
||||
uri.pathSegments.isNotEmpty ? uri.pathSegments.last : "";
|
||||
if (name.isEmpty && uri.pathSegments.length > 1) {
|
||||
name = uri.pathSegments[uri.pathSegments.length - 2];
|
||||
} else if (name.isEmpty) {
|
||||
name = uri.host;
|
||||
}
|
||||
|
||||
context.read<MediaCellBloc>().add(
|
||||
MediaCellEvent.addFile(
|
||||
url: url,
|
||||
name: name,
|
||||
uploadType: MediaUploadTypePB.NetworkMedia,
|
||||
fileType: fileType,
|
||||
),
|
||||
);
|
||||
|
||||
controller.close();
|
||||
},
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilePreviewRender extends StatefulWidget {
|
||||
const _FilePreviewRender({
|
||||
super.key,
|
||||
@ -189,7 +251,11 @@ class _FilePreviewRender extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FilePreviewRenderState extends State<_FilePreviewRender> {
|
||||
final nameController = TextEditingController();
|
||||
final errorMessage = ValueNotifier<String?>(null);
|
||||
final controller = PopoverController();
|
||||
bool isHovering = false;
|
||||
bool isSelected = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@ -234,229 +300,233 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Corners.s6Radius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: widget.size,
|
||||
width: widget.size,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: widget.size < 150 ? 100 : 195,
|
||||
minHeight: widget.size < 150 ? 100 : 195,
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).greyHover,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Corners.s6Radius,
|
||||
topRight: Corners.s6Radius,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
Container(
|
||||
height: 28,
|
||||
width: widget.size,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).isLightMode
|
||||
? Theme.of(context).cardColor
|
||||
: AFThemeExtension.of(context).greyHover,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Corners.s6Radius,
|
||||
bottomRight: Corners.s6Radius,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
widget.file.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 12,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(top: 5, right: 5, child: FileItemMenu(file: widget.file)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FileItemMenu extends StatefulWidget {
|
||||
const FileItemMenu({super.key, required this.file});
|
||||
|
||||
final MediaFilePB file;
|
||||
|
||||
@override
|
||||
State<FileItemMenu> createState() => _FileItemMenuState();
|
||||
}
|
||||
|
||||
class _FileItemMenuState extends State<FileItemMenu> {
|
||||
final popoverController = PopoverController();
|
||||
final nameController = TextEditingController();
|
||||
final errorMessage = ValueNotifier<String?>(null);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
nameController.text = widget.file.name;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverController.close();
|
||||
errorMessage.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
controller: controller,
|
||||
constraints: const BoxConstraints(maxWidth: 150),
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
offset: const Offset(0, 5),
|
||||
popupBuilder: (_) {
|
||||
return SeparatedColumn(
|
||||
separatorBuilder: () => const VSpace(4),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.file.fileType == MediaFileTypePB.Image) ...[
|
||||
FlowyButton(
|
||||
onTap: () {
|
||||
popoverController.close();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => InteractiveImageViewer(
|
||||
userProfile:
|
||||
context.read<MediaCellBloc>().state.userProfile,
|
||||
imageProvider: AFBlockImageProvider(
|
||||
images: [
|
||||
ImageBlockData(
|
||||
url: widget.file.url,
|
||||
type: widget.file.uploadType.toCustomImageType(),
|
||||
),
|
||||
],
|
||||
onDeleteImage: (_) => context
|
||||
.read<MediaCellBloc>()
|
||||
.deleteFile(widget.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,
|
||||
),
|
||||
],
|
||||
onClose: () => setState(() => isSelected = false),
|
||||
popupBuilder: (_) => SeparatedColumn(
|
||||
separatorBuilder: () => const VSpace(4),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.file.fileType == MediaFileTypePB.Image) ...[
|
||||
FlowyButton(
|
||||
leftIcon: const FlowySvg(FlowySvgs.edit_s),
|
||||
text: FlowyText.regular(LocaleKeys.grid_media_rename.tr()),
|
||||
onTap: () {
|
||||
popoverController.close();
|
||||
|
||||
nameController.text = widget.file.name;
|
||||
nameController.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: nameController.text.length,
|
||||
);
|
||||
|
||||
showCustomConfirmDialog(
|
||||
controller.close();
|
||||
showDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.document_plugins_file_renameFile_title.tr(),
|
||||
description: LocaleKeys
|
||||
.document_plugins_file_renameFile_description
|
||||
.tr(),
|
||||
closeOnConfirm: false,
|
||||
builder: (dialogContext) => FileRenameTextField(
|
||||
nameController: nameController,
|
||||
errorMessage: errorMessage,
|
||||
onSubmitted: () => _saveName(context),
|
||||
disposeController: false,
|
||||
builder: (_) => InteractiveImageViewer(
|
||||
userProfile:
|
||||
context.read<MediaCellBloc>().state.userProfile,
|
||||
imageProvider: AFBlockImageProvider(
|
||||
images: [
|
||||
ImageBlockData(
|
||||
url: widget.file.url,
|
||||
type: widget.file.uploadType.toCustomImageType(),
|
||||
),
|
||||
],
|
||||
onDeleteImage: (_) => context
|
||||
.read<MediaCellBloc>()
|
||||
.deleteFile(widget.file.id),
|
||||
),
|
||||
),
|
||||
confirmLabel: LocaleKeys.button_save.tr(),
|
||||
onConfirm: () => _saveName(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
FlowyButton(
|
||||
onTap: () async => downloadMediaFile(
|
||||
context,
|
||||
widget.file,
|
||||
userProfile: context.read<MediaCellBloc>().state.userProfile,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.download_s,
|
||||
FlowySvgs.full_view_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(18),
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.button_download.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
FlowyButton(
|
||||
onTap: () => context.read<MediaCellBloc>().add(
|
||||
MediaCellEvent.removeFile(
|
||||
fileId: widget.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(),
|
||||
LocaleKeys.settings_files_open.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.all(Corners.s8Radius),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: FlowyIconButton(
|
||||
width: 20,
|
||||
radius: BorderRadius.circular(0),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.three_dots_s,
|
||||
FlowyButton(
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.edit_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.grid_media_rename.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
onTap: () {
|
||||
controller.close();
|
||||
|
||||
nameController.text = widget.file.name;
|
||||
nameController.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: nameController.text.length,
|
||||
);
|
||||
|
||||
showCustomConfirmDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.document_plugins_file_renameFile_title.tr(),
|
||||
description: LocaleKeys
|
||||
.document_plugins_file_renameFile_description
|
||||
.tr(),
|
||||
closeOnConfirm: false,
|
||||
builder: (dialogContext) => FileRenameTextField(
|
||||
nameController: nameController,
|
||||
errorMessage: errorMessage,
|
||||
onSubmitted: () => _saveName(context),
|
||||
disposeController: false,
|
||||
),
|
||||
confirmLabel: LocaleKeys.button_save.tr(),
|
||||
onConfirm: () => _saveName(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
FlowyButton(
|
||||
onTap: () async => downloadMediaFile(
|
||||
context,
|
||||
widget.file,
|
||||
userProfile: context.read<MediaCellBloc>().state.userProfile,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.download_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(18),
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.button_download.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
FlowyButton(
|
||||
onTap: () {
|
||||
controller.close();
|
||||
showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: widget.file.name,
|
||||
description: LocaleKeys.grid_media_deleteFileDescription.tr(),
|
||||
onConfirm: () => context
|
||||
.read<MediaCellBloc>()
|
||||
.add(MediaCellEvent.removeFile(fileId: widget.file.id)),
|
||||
);
|
||||
},
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.delete_s,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: const Size.square(18),
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.button_delete.tr(),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: widget.file.fileType != MediaFileTypePB.Image
|
||||
? null
|
||||
: () => openInteractiveViewerFromFile(
|
||||
context,
|
||||
widget.file,
|
||||
userProfile: context.read<MediaCellBloc>().state.userProfile,
|
||||
onDeleteImage: (_) =>
|
||||
context.read<MediaCellBloc>().deleteFile(widget.file.id),
|
||||
),
|
||||
child: FlowyHover(
|
||||
isSelected: () => isSelected,
|
||||
resetHoverOnRebuild: false,
|
||||
onHover: (hovering) => setState(() => isHovering = hovering),
|
||||
child: Stack(
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Corners.s6Radius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: widget.size,
|
||||
width: widget.size,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: widget.size < 150 ? 100 : 195,
|
||||
minHeight: widget.size < 150 ? 100 : 195,
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).greyHover,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Corners.s6Radius,
|
||||
topRight: Corners.s6Radius,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
Container(
|
||||
height: 28,
|
||||
width: widget.size,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).isLightMode
|
||||
? Theme.of(context).cardColor
|
||||
: AFThemeExtension.of(context).greyHover,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Corners.s6Radius,
|
||||
bottomRight: Corners.s6Radius,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
widget.file.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 12,
|
||||
color:
|
||||
AFThemeExtension.of(context).secondaryTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isHovering || isSelected)
|
||||
Positioned(
|
||||
top: 5,
|
||||
right: 5,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.all(Corners.s8Radius),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: FlowyIconButton(
|
||||
onPressed: () {
|
||||
setState(() => isSelected = true);
|
||||
controller.show();
|
||||
},
|
||||
width: 20,
|
||||
radius: BorderRadius.circular(0),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.three_dots_s,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -476,57 +546,45 @@ class _FileItemMenuState extends State<FileItemMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtraInfo extends StatelessWidget {
|
||||
const _ExtraInfo({
|
||||
required this.extraCount,
|
||||
required this.controller,
|
||||
required this.mutex,
|
||||
required this.cellContainerNotifier,
|
||||
});
|
||||
class _ShowAllFilesButton extends StatelessWidget {
|
||||
const _ShowAllFilesButton({required this.extraCount});
|
||||
|
||||
final int extraCount;
|
||||
final PopoverController controller;
|
||||
final PopoverMutex mutex;
|
||||
final CellContainerNotifier cellContainerNotifier;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
key: const Key('extra_info'),
|
||||
mutex: mutex,
|
||||
controller: controller,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 250,
|
||||
maxWidth: 250,
|
||||
maxHeight: 400,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (_) => BlocProvider.value(
|
||||
value: context.read<MediaCellBloc>(),
|
||||
child: const MediaCellEditor(),
|
||||
),
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: controller.show,
|
||||
child: FlowyHover(
|
||||
resetHoverOnRebuild: false,
|
||||
child: Container(
|
||||
height: 38,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).greyHover,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.grid_media_showMore.tr(args: ['$extraCount']),
|
||||
lineHeight: 1,
|
||||
),
|
||||
final show = context.read<MediaCellBloc>().state.showAllFiles;
|
||||
|
||||
final label = show
|
||||
? extraCount == 1
|
||||
? LocaleKeys.grid_media_hideFile.tr()
|
||||
: LocaleKeys.grid_media_hideFiles.tr(args: ['$extraCount'])
|
||||
: extraCount == 1
|
||||
? LocaleKeys.grid_media_showFile.tr()
|
||||
: LocaleKeys.grid_media_showFiles.tr(args: ['$extraCount']);
|
||||
|
||||
final quarterTurns = show ? 1 : 3;
|
||||
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
label,
|
||||
lineHeight: 1.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
leftIcon: RotatedBox(
|
||||
quarterTurns: quarterTurns,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.arrow_left_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||
onTap: () => context
|
||||
.read<MediaCellBloc>()
|
||||
.add(const MediaCellEvent.toggleShowAllFiles()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ class _MediaCellEditorState extends State<MediaCellEditor> {
|
||||
children: [
|
||||
if (state.files.isNotEmpty) ...[
|
||||
ReorderableListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (_, index) => BlocProvider.value(
|
||||
@ -465,19 +466,22 @@ class _MediaItemMenuState extends State<MediaItemMenu> {
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
FlowyButton(
|
||||
onTap: () => context.read<MediaCellBloc>().add(
|
||||
MediaCellEvent.removeFile(
|
||||
fileId: widget.file.id,
|
||||
),
|
||||
),
|
||||
onTap: () => showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: widget.file.name,
|
||||
description: LocaleKeys.grid_media_deleteFileDescription.tr(),
|
||||
onConfirm: () => context
|
||||
.read<MediaCellBloc>()
|
||||
.add(MediaCellEvent.removeFile(fileId: widget.file.id)),
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.delete_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: const Size.square(18),
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.button_delete.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
|
||||
@ -196,6 +196,7 @@ class FieldActionCell extends StatelessWidget {
|
||||
}
|
||||
|
||||
return FlowyButton(
|
||||
resetHoverOnRebuild: false,
|
||||
disable: !enable,
|
||||
text: FlowyText.medium(
|
||||
action.title(fieldInfo),
|
||||
|
||||
@ -88,6 +88,7 @@ extension FieldTypeExtension on FieldType {
|
||||
FieldType.Summary => const Color(0xFF6859A7),
|
||||
FieldType.Time => const Color(0xFFFDEDA7),
|
||||
FieldType.Translate => const Color(0xFF6859A7),
|
||||
FieldType.Media => const Color(0xFFBE9090),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FlowyIconTextButton extends StatelessWidget {
|
||||
final Widget Function(bool onHover) textBuilder;
|
||||
@ -160,6 +161,7 @@ class FlowyButton extends StatelessWidget {
|
||||
final bool expand;
|
||||
final Color? borderColor;
|
||||
final Color? backgroundColor;
|
||||
final bool resetHoverOnRebuild;
|
||||
|
||||
const FlowyButton({
|
||||
super.key,
|
||||
@ -185,6 +187,7 @@ class FlowyButton extends StatelessWidget {
|
||||
this.expand = false,
|
||||
this.borderColor,
|
||||
this.backgroundColor,
|
||||
this.resetHoverOnRebuild = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -207,6 +210,7 @@ class FlowyButton extends StatelessWidget {
|
||||
onTap: disable ? null : onTap,
|
||||
onSecondaryTap: disable ? null : onSecondaryTap,
|
||||
child: FlowyHover(
|
||||
resetHoverOnRebuild: resetHoverOnRebuild,
|
||||
cursor:
|
||||
disable ? SystemMouseCursors.forbidden : SystemMouseCursors.click,
|
||||
style: HoverStyle(
|
||||
|
||||
@ -1469,11 +1469,15 @@
|
||||
"download": "Download",
|
||||
"open": "Open",
|
||||
"delete": "Delete",
|
||||
"moreFilesHint": "+{} file(s)",
|
||||
"showMore": "There are {} more file(s), click to view",
|
||||
"moreFilesHint": "+{}",
|
||||
"addFileOrImage": "Add a file, image, or link",
|
||||
"attachmentsHint": "{} attachment(s)",
|
||||
"addFileMobile": "Add file"
|
||||
"attachmentsHint": "{}",
|
||||
"addFileMobile": "Add file",
|
||||
"showFile": "Show 1 file",
|
||||
"showFiles": "Show {} files",
|
||||
"hideFile": "Hide 1 file",
|
||||
"hideFiles": "Hide {} files",
|
||||
"deleteFileDescription": "Are you sure you want to delete this file? This action is irreversible."
|
||||
}
|
||||
},
|
||||
"document": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user