diff --git a/lib/features/core/components/attachment_file_card.dart b/lib/features/core/components/attachment_file_card.dart new file mode 100644 index 0000000..0e5ba4e --- /dev/null +++ b/lib/features/core/components/attachment_file_card.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:popup_menu/popup_menu.dart'; + +import '../../../nocodb_sdk/models.dart'; + +class AttachmentFileCard extends HookConsumerWidget { + final NcAttachedFile _file; + final PopupMenu? popupMenu; + const AttachmentFileCard( + this._file, { + this.popupMenu, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final anchor = popupMenu != null ? GlobalKey() : null; + final child = Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + const SizedBox( + width: 64, + height: 64, + child: Icon(Icons.description_outlined, size: 48), + ), + Container( + padding: const EdgeInsets.all(8), + child: Text( + _file.title, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + + return Card( + key: anchor, + elevation: 4, + child: anchor == null + ? child + : InkWell( + onTap: () => popupMenu?.show(widgetKey: anchor), + child: child, + ), + ); + } +} diff --git a/lib/features/core/components/attachment_image_card.dart b/lib/features/core/components/attachment_image_card.dart new file mode 100644 index 0000000..0de40f0 --- /dev/null +++ b/lib/features/core/components/attachment_image_card.dart @@ -0,0 +1,45 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:popup_menu/popup_menu.dart'; + +import '../../../nocodb_sdk/client.dart'; +import '../../../nocodb_sdk/models.dart'; + +class AttachmentImageCard extends HookConsumerWidget { + final NcAttachedFile _file; + final PopupMenu? popupMenu; + const AttachmentImageCard( + this._file, { + this.popupMenu, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final anchor = popupMenu != null ? GlobalKey() : null; + final child = SizedBox( + width: 80, + height: 80, + child: CachedNetworkImage( + imageUrl: _file.signedUrl(api.uri), + placeholder: (context, url) => const Padding( + padding: EdgeInsets.all(24), + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + ); + + return Card( + key: anchor, + elevation: 4, + child: anchor == null + ? child + : InkWell( + onTap: () => popupMenu?.show(widgetKey: anchor), + child: child, + ), + ); + } +} diff --git a/lib/features/core/components/editors/attachment.dart b/lib/features/core/components/editors/attachment.dart index 19b7e52..d45adfb 100644 --- a/lib/features/core/components/editors/attachment.dart +++ b/lib/features/core/components/editors/attachment.dart @@ -1,4 +1,3 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -7,7 +6,8 @@ import '../../../../nocodb_sdk/models.dart'; import '../../../../nocodb_sdk/symbols.dart'; import '../../pages/attachment_editor.dart'; import '../../providers/providers.dart'; -import '/nocodb_sdk/client.dart'; +import '../attachment_file_card.dart'; +import '../attachment_image_card.dart'; import '/nocodb_sdk/models.dart' as model; class AttachmentEditor extends HookConsumerWidget { @@ -21,75 +21,41 @@ class AttachmentEditor extends HookConsumerWidget { required this.onUpdate, }); - Widget buildImageCard( - NcAttachedFile file, - GlobalKey key, - ) { - return Card( - elevation: 2, - child: InkWell( - key: key, - child: SizedBox( - width: 80, - height: 80, - child: CachedNetworkImage( - imageUrl: file.signedUrl(api.uri), - placeholder: (context, url) => const Padding( - padding: EdgeInsets.all(24), - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => const Icon(Icons.error), - ), - ), - ), - ); - } - - Widget buildFileCard( - NcAttachedFile file, - GlobalKey key, - ) { - return Card( - key: key, - elevation: 4, - child: InkWell( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - const SizedBox( - width: 72, - height: 72, - child: Icon(Icons.description_outlined, size: 48), - ), - Container( - padding: const EdgeInsets.all(8), - child: Text( - file.title, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), + Widget buildEmpty({text = '-'}) { + return Container( + height: 80, ); } - List buildChildren( - List files, - ) { - return files.map((file) { - final key = GlobalKey(); - return file.isImage - ? buildImageCard(file, key) - : buildFileCard(file, key); - }).toList(); - } - - // TODO: Show files on the list and open AttachmentEditorPage when further actions are required. @override Widget build(BuildContext context, WidgetRef ref) { + if (rowId == null) { + return InkWell( + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Record is not yet created.'), + content: const Text( + 'Once record is created, you can attach files.', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + }, + child: buildEmpty(), + ); + } + // TODO: Organize initialization. // NOTE: The initialization process needs to be the same for both AttachmentEditor and AttachmentEditorPage. // If they differ, file synchronization will be lost when navigating between the two components. @@ -116,16 +82,8 @@ class AttachmentEditor extends HookConsumerWidget { ) .toList() as List; - final children = buildChildren(files); - return GestureDetector( onTap: () { - if (rowId == null) { - // TODO: Show message to explain the reason why AttachmentEditorPage does not open. - // AttachmentEditorPage only supports updates and does not support saving new records, - // so it will not open if the line has not been saved yet. - return; - } Navigator.push( context, MaterialPageRoute( @@ -136,13 +94,19 @@ class AttachmentEditor extends HookConsumerWidget { ); }, child: ConstrainedBox( - constraints: - const BoxConstraints(maxHeight: 500), // TODO: Adjust maxHeight. + constraints: const BoxConstraints( + minHeight: 80, + maxHeight: 500, // TODO: Adjust maxHeight. + ), child: GridView.count( shrinkWrap: true, crossAxisCount: 3, padding: const EdgeInsets.all(8), - children: children, + children: files.map((file) { + return file.isImage + ? AttachmentImageCard(file) + : AttachmentFileCard(file); + }).toList(), ), ), ); diff --git a/lib/features/core/pages/attachment_editor.dart b/lib/features/core/pages/attachment_editor.dart index f7a5be8..eac860a 100644 --- a/lib/features/core/pages/attachment_editor.dart +++ b/lib/features/core/pages/attachment_editor.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -18,6 +17,8 @@ import '../../../common/logger.dart'; import '../../../nocodb_sdk/client.dart'; import '../../../nocodb_sdk/models.dart'; import '../../../nocodb_sdk/symbols.dart'; +import '../components/attachment_file_card.dart'; +import '../components/attachment_image_card.dart'; import '../components/dialog/file_rename_dialog.dart'; import '../providers/providers.dart'; @@ -49,9 +50,9 @@ enum FileUploadType { class AttachmentEditorPage extends HookConsumerWidget { final String rowId; final String columnTitle; - const AttachmentEditorPage(this.rowId, this.columnTitle, {super.key}); + AttachmentEditorPage(this.rowId, this.columnTitle, {super.key}); - // TODO: Fix the lifetime issue. + // TODO: Fix lifetime issue. // There is a possibility that the file upload will continue even after the screen is closed, // and there is a concern that the lifetime of onUpdate might expire when the file upload is complete. Future uploadFile( @@ -197,70 +198,7 @@ class AttachmentEditorPage extends HookConsumerWidget { ]; } - Widget buildImageCard( - NcAttachedFile file, - PopupMenu popupMenu, - GlobalKey key, - ) { - return Card( - elevation: 2, - child: InkWell( - key: key, - onTap: () { - popupMenu.show(widgetKey: key); - }, - child: SizedBox( - width: 80, - height: 80, - child: CachedNetworkImage( - imageUrl: file.signedUrl(api.uri), - placeholder: (context, url) => const Padding( - padding: EdgeInsets.all(24), - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => const Icon(Icons.error), - ), - ), - ), - ); - } - - Widget buildFileCard( - NcAttachedFile file, - PopupMenu popupMenu, - GlobalKey key, - ) { - return Card( - key: key, - elevation: 4, - child: InkWell( - onTap: () { - popupMenu.show(widgetKey: key); - }, - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - const SizedBox( - width: 64, - height: 64, - child: Icon(Icons.description_outlined, size: 48), - ), - Container( - padding: const EdgeInsets.all(8), - child: Text( - file.title, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ); - } - - static GlobalKey fabState = GlobalKey(); + GlobalKey fabState = GlobalKey(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -296,6 +234,7 @@ class AttachmentEditorPage extends HookConsumerWidget { ); } + // TODO: Adjust the design when files is empty. final files = (row[columnTitle] ?? []) .map( (e) => NcAttachedFile.fromJson(e as Map), @@ -328,6 +267,7 @@ class AttachmentEditorPage extends HookConsumerWidget { ), children: [ FloatingActionButton( + heroTag: 'upload_from_storage', onPressed: () { uploadFile( context, @@ -340,6 +280,7 @@ class AttachmentEditorPage extends HookConsumerWidget { child: const Icon(Icons.upload_file_rounded, size: 32), ), FloatingActionButton( + heroTag: 'upload_from_camera', onPressed: () { uploadFile( context, @@ -352,6 +293,7 @@ class AttachmentEditorPage extends HookConsumerWidget { child: const Icon(Icons.photo_camera_rounded, size: 32), ), FloatingActionButton( + heroTag: 'upload_from_gallery', onPressed: () { uploadFile( context, @@ -410,10 +352,15 @@ class AttachmentEditorPage extends HookConsumerWidget { context: context, ); - final key = GlobalKey(); return file.isImage - ? buildImageCard(file, popupMenu, key) - : buildFileCard(file, popupMenu, key); + ? AttachmentImageCard( + file, + popupMenu: popupMenu, + ) + : AttachmentFileCard( + file, + popupMenu: popupMenu, + ); }, ).toList(), ),