diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/media_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/media_cell_bloc.dart index 7a3075e0f23a3..e572228c32ea1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/media_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/media_cell_bloc.dart @@ -4,6 +4,8 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/file_storage_task.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -12,7 +14,9 @@ import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.d import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:collection/collection.dart'; import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -29,6 +33,8 @@ class MediaCellBloc extends Bloc { late final RowBackendService _rowService = RowBackendService(viewId: cellController.viewId); final MediaCellController cellController; + final FileStorageService _fileStorageService = getIt(); + final Map> _progressNotifiers = {}; void Function()? _onCellChangedFn; @@ -53,6 +59,8 @@ class MediaCellBloc extends Bloc { (event, emit) async { await event.when( initial: () async { + _checkFileStatus(); + // Fetch user profile final userProfileResult = await UserBackendService.getCurrentUserProfile(); @@ -96,9 +104,14 @@ class MediaCellBloc extends Bloc { ); final result = await DatabaseEventUpdateMediaCell(payload).send(); - result.fold((l) => null, (err) => Log.error(err)); + result.fold( + (_) => _registerProgressNotifier(newFile), + (err) => Log.error(err), + ); }, removeFile: (id) async { + _removeNotifier(id); + final payload = MediaCellChangesetPB( viewId: cellController.viewId, cellId: CellIdPB( @@ -158,6 +171,49 @@ class MediaCellBloc extends Bloc { rowId: cellController.rowId, cover: cover, ), + onProgressUpdate: (id) { + final FileProgress? progress = _progressNotifiers[id]?.value; + if (progress != null) { + MediaUploadProgress? mediaUploadProgress = + state.uploadProgress.firstWhereOrNull((u) => u.fileId == id); + + if (progress.error != null) { + // Remove file from cell + add(MediaCellEvent.removeFile(fileId: id)); + _removeNotifier(id); + + // Remove progress + final uploadProgress = [...state.uploadProgress]; + uploadProgress.removeWhere((u) => u.fileId == id); + emit(state.copyWith(uploadProgress: uploadProgress)); + return; + } + + if (mediaUploadProgress == null) { + mediaUploadProgress ??= MediaUploadProgress( + fileId: id, + uploadState: progress.progress >= 1 + ? MediaUploadState.completed + : MediaUploadState.uploading, + fileProgress: progress, + ); + } else { + mediaUploadProgress = mediaUploadProgress.copyWith( + uploadState: progress.progress >= 1 + ? MediaUploadState.completed + : MediaUploadState.uploading, + fileProgress: progress, + ); + } + + final uploadProgress = [...state.uploadProgress]; + uploadProgress + ..removeWhere((u) => u.fileId == id) + ..add(mediaUploadProgress); + + emit(state.copyWith(uploadProgress: uploadProgress)); + } + }, ); }, ); @@ -174,6 +230,53 @@ class MediaCellBloc extends Bloc { ); } + /// We check the file state of all the files that are in Cloud (hosted by us) in the cell. + /// + /// If any file has failed, we should notify the user about it, + /// and also remove it from the cell. + /// + /// This method registers the progress notifiers for each file. + /// + void _checkFileStatus() { + for (final file in state.files) { + _registerProgressNotifier(file); + } + } + + void _registerProgressNotifier(MediaFilePB file) { + if (file.uploadType != FileUploadTypePB.CloudFile) { + return; + } + + final notifier = _fileStorageService.onFileProgress(fileUrl: file.url); + _progressNotifiers[file.id] = notifier; + notifier.addListener(() => _onProgressChanged(file.id)); + + add(MediaCellEvent.onProgressUpdate(file.id)); + } + + void _onProgressChanged(String id) { + // Ignore events if the file is already uploaded + final progress = + state.uploadProgress.firstWhereOrNull((u) => u.fileId == id); + if (progress == null || + progress.uploadState == MediaUploadState.completed) { + return; + } + + add(MediaCellEvent.onProgressUpdate(id)); + } + + /// Removes and disposes of a progress notifier if found + /// + void _removeNotifier(String id) { + SchedulerBinding.instance.addPostFrameCallback((_) { + final notifier = _progressNotifiers.remove(id); + notifier?.removeListener(() => _onProgressChanged(id)); + notifier?.dispose(); + }); + } + void _onFieldChangedListener(FieldInfo fieldInfo) { if (!isClosed) { add(MediaCellEvent.didUpdateField(fieldInfo.name)); @@ -221,6 +324,9 @@ class MediaCellEvent with _$MediaCellEvent { const factory MediaCellEvent.toggleShowAllFiles() = _ToggleShowAllFiles; const factory MediaCellEvent.setCover(RowCoverPB cover) = _SetCover; + + const factory MediaCellEvent.onProgressUpdate(String fileId) = + _OnProgressUpdate; } @freezed @@ -231,6 +337,7 @@ class MediaCellState with _$MediaCellState { @Default([]) List files, @Default(false) showAllFiles, @Default(true) hideFileNames, + @Default([]) List uploadProgress, }) = _MediaCellState; factory MediaCellState.initial(MediaCellController cellController) { @@ -245,3 +352,32 @@ class MediaCellState with _$MediaCellState { ); } } + +enum MediaUploadState { uploading, completed } + +class MediaUploadProgress { + const MediaUploadProgress({ + required this.fileId, + required this.uploadState, + required this.fileProgress, + }); + + final String fileId; + final MediaUploadState uploadState; + final FileProgress fileProgress; + + @override + String toString() => + 'MediaUploadProgress(fileId: $fileId, uploadState: $uploadState, fileProgress: $fileProgress)'; + + MediaUploadProgress copyWith({ + MediaUploadState? uploadState, + FileProgress? fileProgress, + }) { + return MediaUploadProgress( + fileId: fileId, + uploadState: uploadState ?? this.uploadState, + fileProgress: fileProgress ?? this.fileProgress, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart index bcf136bcf10d9..6be083452ce91 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart @@ -42,19 +42,30 @@ class GridMediaCellSkin extends IEditableMediaCellSkin { Widget child = BlocBuilder( builder: (context, state) { final wrapContent = context.read().wrapContent; - final List children = state.files - .map( - (file) => GestureDetector( - onTap: () => _openOrExpandFile(context, file, state.files), - child: Padding( - padding: wrapContent - ? const EdgeInsets.only(right: 4) - : EdgeInsets.zero, - child: _FilePreviewRender(file: file), + final List children = state.files.map( + (file) { + return GestureDetector( + onTap: () { + // TODO(Nathan): Add retry event if file upload failed + // if (uploadFailed) { + // return add_event_here; + // } + + _openOrExpandFile(context, file, state.files); + }, + child: Padding( + padding: wrapContent + ? const EdgeInsets.only(right: 4) + : EdgeInsets.zero, + child: _FilePreviewRender( + file: file, + // TODO(Nathan): Add error value for this file + didError: false, ), ), - ) - .toList(); + ); + }, + ).toList(); if (isMobileRowDetail && state.files.isEmpty) { children.add( @@ -221,29 +232,36 @@ class GridMediaCellSkin extends IEditableMediaCellSkin { } class _FilePreviewRender extends StatelessWidget { - const _FilePreviewRender({required this.file}); + const _FilePreviewRender({required this.file, this.didError = false}); final MediaFilePB file; + final bool didError; @override Widget build(BuildContext context) { - if (file.fileType != MediaFileTypePB.Image) { + if (file.fileType != MediaFileTypePB.Image || didError) { return FlowyTooltip( - message: file.name, + message: didError ? LocaleKeys.grid_media_uploadFailed.tr() : file.name, child: Container( height: 28, width: 28, clipBehavior: Clip.antiAlias, - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(didError ? 6 : 8), decoration: BoxDecoration( color: AFThemeExtension.of(context).greyHover, borderRadius: BorderRadius.circular(4), ), - child: FlowySvg( - file.fileType.icon, - size: const Size.square(12), - color: AFThemeExtension.of(context).textColor, - ), + child: didError + ? const FlowySvg( + FlowySvgs.notice_s, + size: Size.square(16), + blendMode: BlendMode.dstIn, + ) + : FlowySvg( + file.fileType.icon, + size: const Size.square(12), + color: AFThemeExtension.of(context).textColor, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/media_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/media_cell_editor.dart index eba42c1f97694..b72c0344637d9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/media_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/media_cell_editor.dart @@ -1,6 +1,7 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.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'; @@ -69,6 +70,7 @@ class _MediaCellEditorState extends State { index: index, enableReordering: state.files.length > 1, mutex: itemMutex, + // TODO(Nathan): Add progress and didError value here ), ), itemCount: state.files.length, @@ -231,6 +233,8 @@ class RenderMedia extends StatefulWidget { required this.images, required this.enableReordering, required this.mutex, + this.progress, + this.didError = false, }); final int index; @@ -239,18 +243,27 @@ class RenderMedia extends StatefulWidget { final bool enableReordering; final PopoverMutex mutex; + /// If the upload is in progress (anything other than null), we show the + /// progress indicator + /// + final double? progress; + + /// Signifies whether the upload failed for this File/Media + /// + final bool didError; + @override State createState() => _RenderMediaState(); } class _RenderMediaState extends State { + late final controller = PopoverController(); + bool isHovering = false; int? imageIndex; MediaFilePB get file => widget.file; - late final controller = PopoverController(); - @override void initState() { super.initState(); @@ -303,11 +316,65 @@ class _RenderMediaState extends State { context, files: widget.images, index: imageIndex!, - child: AFImage( - url: widget.file.url, - uploadType: widget.file.uploadType, - userProfile: - context.read().state.userProfile, + child: Stack( + children: [ + Container( + foregroundDecoration: widget.didError + ? BoxDecoration( + color: Colors.black.withOpacity(0.4), + ) + : null, + child: AFImage( + url: widget.file.url, + uploadType: widget.file.uploadType, + userProfile: context + .read() + .state + .userProfile, + ), + ), + if (!widget.didError && widget.progress != null) + Positioned( + right: 6, + bottom: 6, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + borderRadius: Corners.s4Border, + ), + child: Padding( + padding: const EdgeInsets.all(5), + child: CircularProgressIndicator( + value: widget.progress, + color: Colors.white, + strokeWidth: 1.5, + ), + ), + ), + ), + if (widget.didError) + Positioned.fill( + child: Center( + child: Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: FlowyIconButton( + icon: FlowySvg( + FlowySvgs.retry_s, + color: AFThemeExtension.of(context) + .strongText, + ), + ), + ), + ), + ), + ], ), ), ), @@ -341,6 +408,37 @@ class _RenderMediaState extends State { ), ), ), + if (widget.didError) ...[ + const HSpace(8), + FlowyTooltip( + message: LocaleKeys.grid_media_uploadFailed.tr(), + child: FlowyIconButton( + onPressed: () { + // TODO(Nathan): Add retry event + }, + hoverColor: Colors.transparent, + width: 24, + icon: const FlowySvg( + FlowySvgs.notice_s, + size: Size.square(16), + blendMode: BlendMode.dstIn, + ), + ), + ), + ], + if (!widget.didError && widget.progress != null) ...[ + const HSpace(4), + Container( + height: 16, + width: 16, + margin: const EdgeInsets.only(top: 4), + padding: const EdgeInsets.all(2.5), + child: CircularProgressIndicator( + value: widget.progress, + strokeWidth: 2, + ), + ), + ], ], const HSpace(4), AppFlowyPopover( @@ -358,6 +456,7 @@ class _RenderMediaState extends State { index: imageIndex ?? -1, closeContext: popoverContext, onAction: () => controller.close(), + didError: widget.didError, ), ), child: FlowyIconButton( @@ -407,6 +506,7 @@ class MediaItemMenu extends StatefulWidget { required this.index, this.closeContext, this.onAction, + this.didError = false, }); /// The [MediaFilePB] this menu concerns @@ -425,6 +525,11 @@ class MediaItemMenu extends StatefulWidget { /// Callback to be called when an action is performed final VoidCallback? onAction; + /// If the upload failed, we show very limited options, + /// and additionally add the retry option. + /// + final bool didError; + @override State createState() => _MediaItemMenuState(); } @@ -435,6 +540,8 @@ class _MediaItemMenuState extends State { BuildContext? renameContext; + bool get didError => widget.didError; + @override void dispose() { nameController.dispose(); @@ -448,60 +555,71 @@ class _MediaItemMenuState extends State { separatorBuilder: () => const VSpace(8), mainAxisSize: MainAxisSize.min, children: [ - if (widget.file.fileType == MediaFileTypePB.Image) ...[ + if (!didError) ...[ + if (widget.file.fileType == MediaFileTypePB.Image) ...[ + MediaMenuItem( + onTap: () { + widget.onAction?.call(); + _showInteractiveViewer(); + }, + icon: FlowySvgs.full_view_s, + label: LocaleKeys.grid_media_expand.tr(), + ), + MediaMenuItem( + onTap: () { + context.read().add( + MediaCellEvent.setCover( + RowCoverPB( + data: widget.file.url, + uploadType: widget.file.uploadType, + coverType: CoverTypePB.FileCover, + ), + ), + ); + widget.onAction?.call(); + }, + icon: FlowySvgs.cover_s, + label: LocaleKeys.grid_media_setAsCover.tr(), + ), + ], MediaMenuItem( onTap: () { widget.onAction?.call(); - _showInteractiveViewer(); + afLaunchUrlString(widget.file.url); }, - icon: FlowySvgs.full_view_s, - label: LocaleKeys.grid_media_expand.tr(), + icon: FlowySvgs.open_in_browser_s, + label: LocaleKeys.grid_media_openInBrowser.tr(), ), MediaMenuItem( - onTap: () { - context.read().add( - MediaCellEvent.setCover( - RowCoverPB( - data: widget.file.url, - uploadType: widget.file.uploadType, - coverType: CoverTypePB.FileCover, - ), - ), - ); + onTap: () async { + await _showRenameDialog(); widget.onAction?.call(); }, - icon: FlowySvgs.cover_s, - label: LocaleKeys.grid_media_setAsCover.tr(), + icon: FlowySvgs.rename_s, + label: LocaleKeys.grid_media_rename.tr(), ), - ], - MediaMenuItem( - onTap: () { - widget.onAction?.call(); - afLaunchUrlString(widget.file.url); - }, - icon: FlowySvgs.open_in_browser_s, - label: LocaleKeys.grid_media_openInBrowser.tr(), - ), - MediaMenuItem( - onTap: () async { - await _showRenameDialog(); - widget.onAction?.call(); - }, - icon: FlowySvgs.rename_s, - label: LocaleKeys.grid_media_rename.tr(), - ), - if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[ + if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[ + MediaMenuItem( + onTap: () async { + await downloadMediaFile( + context, + widget.file, + userProfile: context.read().state.userProfile, + ); + widget.onAction?.call(); + }, + icon: FlowySvgs.save_as_s, + label: LocaleKeys.button_download.tr(), + ), + ], + ] else ...[ MediaMenuItem( onTap: () async { - await downloadMediaFile( - context, - widget.file, - userProfile: context.read().state.userProfile, - ); + // TODO(Nathan): Add retry event widget.onAction?.call(); }, - icon: FlowySvgs.save_as_s, - label: LocaleKeys.button_download.tr(), + icon: FlowySvgs.retry_s, + label: LocaleKeys.button_retry.tr(), ), ], MediaMenuItem( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart index e4dd1d4e4a4c0..e149d2bde104f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart @@ -15,6 +15,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:cross_file/cross_file.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; @@ -356,17 +357,32 @@ class FileBlockComponentState extends State final name = node.attributes[FileBlockKeys.name] as String; return [ Expanded( - child: FlowyText( - name, - overflow: TextOverflow.ellipsis, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText( + name, + overflow: TextOverflow.ellipsis, + color: AFThemeExtension.of(context).strongText, + ), + // TODO(Nathan): Provide error message to show the error hint. + // Optionally you can make the string hardcoded in the FlowyText and just + // use a boolean, if we're only going to show this one message. + _buildUploadErrorText(), + ], ), ), + // TODO(Nathan): provide upload progress and boolean value for if it failed + _buildProgressOrRetry(), + const HSpace(8), if (UniversalPlatform.isDesktopOrWeb) ...[ ValueListenableBuilder( valueListenable: showActionsNotifier, builder: (_, value, __) { final url = node.attributes[FileBlockKeys.url]; + // TODO(Nathan): If upload failed, return an empty SizedBox if (!value || url == null || url.isEmpty) { return const SizedBox.shrink(); } @@ -417,6 +433,43 @@ class FileBlockComponentState extends State } } + Widget _buildUploadErrorText({String? error}) { + if (error == null) { + return const SizedBox.shrink(); + } + + return FlowyText.regular( + error, + fontSize: 11, + color: Theme.of(context).colorScheme.error, + ); + } + + Widget _buildProgressOrRetry({double? progress, bool didError = false}) { + if (progress == null && !didError) { + return const SizedBox.shrink(); + } + + if (didError) { + return GestureDetector( + onTap: () { + // TODO(Nathan): Retry event + }, + child: const FlowySvg(FlowySvgs.retry_s), + ); + } + + return SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 3, + color: const Color(0xFF8F959E), + ), + ); + } + // only used on mobile platform List _buildExtendActionWidgets(BuildContext context) { final String? url = widget.node.attributes[FileBlockKeys.url]; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart index 7a68ad99b98df..328913d108e4a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart @@ -143,12 +143,11 @@ class CustomImageBlockComponentState extends State @override Node get node => widget.node; - final imageKey = GlobalKey(); RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; - late final editorState = Provider.of(context, listen: false); - + final imageKey = GlobalKey(); final showActionsNotifier = ValueNotifier(false); + late final editorState = Provider.of(context, listen: false); bool alwaysShowMenu = false; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 607e1d4d06653..5cdf50003509d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -2,11 +2,14 @@ import 'dart:io'; import 'dart:math'; import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -110,7 +113,14 @@ class _ResizableImageState extends State { } return Stack( children: [ - child, + Container( + // TODO(Nathan): When image uplaod failed, show this foreground decoration + // foregroundDecoration: BoxDecoration( + // color: Colors.white.withOpacity(0.6), + // ), + child: child, + ), + // TODO(Nathan): Should we disable edge gestures if image is uploading/failed? if (widget.editable) ...[ _buildEdgeGesture( context, @@ -129,6 +139,10 @@ class _ResizableImageState extends State { onUpdate: (distance) => setState(() => moveDistance = -distance), ), ], + // TODO(Nathan): Provide error message and progress value + // If there is no error and no progress == complete/redundant, then it doesn't show. + // Optionally you can make `error` into a boolean value and just hardcode the "Upload failed" message + buildProgressOverlay(), ], ); } @@ -206,6 +220,78 @@ class _ResizableImageState extends State { ), ); } + + Widget buildProgressOverlay({double? progress, String? error}) { + Widget content; + if (error != null) { + content = Row( + children: [ + Stack( + children: [ + Container( + width: 12, + height: 12, + margin: const EdgeInsets.all(1), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: Corners.s8Border, + ), + ), + const FlowySvg( + FlowySvgs.notice_s, + size: Size.square(16), + blendMode: BlendMode.dstIn, + ), + ], + ), + const HSpace(4), + FlowyText.regular( + error, + fontSize: 11, + figmaLineHeight: 16, + color: Colors.white, + ), + const HSpace(8), + GestureDetector( + onTap: () { + // TODO(Nathan): Retry event + }, + child: FlowyText.regular( + LocaleKeys.button_retry.tr(), + fontSize: 11, + figmaLineHeight: 16, + color: const Color(0xFF49CFF4), + ), + ), + ], + ); + } else if (progress != null) { + content = CircularProgressIndicator( + value: progress, + color: Colors.white, + strokeWidth: 1.5, + ); + } else { + return const SizedBox.shrink(); + } + + return Positioned( + right: 6, + bottom: 6, + child: Container( + width: error != null ? null : 20, + height: error != null ? 26 : 20, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + borderRadius: Corners.s4Border, + ), + child: Padding( + padding: const EdgeInsets.all(5), + child: content, + ), + ), + ); + } } class _ImageLoadFailedWidget extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/startup/tasks/file_storage_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/file_storage_task.dart index 0695ceeab5bf5..19618eb094568 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/file_storage_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/file_storage_task.dart @@ -19,10 +19,7 @@ class FileStorageTask extends LaunchTask { @override Future initialize(LaunchContext context) async { - context.getIt.registerSingleton( - FileStorageService(), - dispose: (service) async => service.dispose(), - ); + context.getIt.registerSingleton(FileStorageService()); } @override @@ -131,6 +128,10 @@ class FileProgress { final double progress; final String fileUrl; final String? error; + + @override + String toString() => + 'FileProgress(progress: $progress, fileUrl: $fileUrl, error: $error)'; } class AutoRemoveNotifier extends ValueNotifier { diff --git a/frontend/resources/flowy_icons/16x/notice.svg b/frontend/resources/flowy_icons/16x/notice.svg new file mode 100644 index 0000000000000..5dd85076f74a4 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/notice.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/retry.svg b/frontend/resources/flowy_icons/16x/retry.svg new file mode 100644 index 0000000000000..d45f18aeaff3c --- /dev/null +++ b/frontend/resources/flowy_icons/16x/retry.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index dc93847dd6703..045208ad0f47b 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -414,7 +414,8 @@ "backToHome": "Back to Home", "viewing": "Viewing", "editing": "Editing", - "gotIt": "Got it" + "gotIt": "Got it", + "retry": "Retry" }, "label": { "welcome": "Welcome!", @@ -1600,7 +1601,8 @@ "downloadFailedToken": "Failed to download file, user token unavailable", "setAsCover": "Set as cover", "openInBrowser": "Open in browser", - "embedLink": "Embed file link" + "embedLink": "Embed file link", + "uploadFailed": "Upload failed, click to retry" } }, "document": {