✨ Chat input full featured upload
This commit is contained in:
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'70acac63c720987d8b1688500e3735f1c2d16fdc';
|
||||
String _$messagesNotifierHash() => r'e4b760068f7349cc2991d0788055dbd855184f82';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@@ -31,6 +31,7 @@ import "package:super_sliver_list/super_sliver_list.dart";
|
||||
import "package:material_symbols_icons/symbols.dart";
|
||||
import "package:island/widgets/chat/call_button.dart";
|
||||
import "package:island/widgets/chat/chat_input.dart";
|
||||
import "package:island/widgets/chat/chat_link_attachments.dart";
|
||||
import "package:island/widgets/chat/public_room_preview.dart";
|
||||
|
||||
class ChatRoomScreen extends HookConsumerWidget {
|
||||
@@ -192,6 +193,59 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> pickAudioMedia() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.audio,
|
||||
allowMultiple: true,
|
||||
allowCompression: false,
|
||||
);
|
||||
if (result == null || result.count == 0) return;
|
||||
attachments.value = [
|
||||
...attachments.value,
|
||||
...result.files.map(
|
||||
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.audio),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> pickGeneralFile() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: true,
|
||||
allowCompression: false,
|
||||
);
|
||||
if (result == null || result.count == 0) return;
|
||||
attachments.value = [
|
||||
...attachments.value,
|
||||
...result.files.map(
|
||||
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.file),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void linkAttachment() async {
|
||||
final cloudFile = await showModalBottomSheet<SnCloudFile?>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const ChatLinkAttachment(),
|
||||
);
|
||||
if (cloudFile == null) return;
|
||||
|
||||
attachments.value = [
|
||||
...attachments.value,
|
||||
UniversalFile(
|
||||
data: cloudFile,
|
||||
type: switch (cloudFile.mimeType?.split('/').firstOrNull) {
|
||||
'image' => UniversalFileType.image,
|
||||
'video' => UniversalFileType.video,
|
||||
'audio' => UniversalFileType.audio,
|
||||
_ => UniversalFileType.file,
|
||||
},
|
||||
isLink: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void sendMessage() {
|
||||
if (messageController.text.trim().isNotEmpty ||
|
||||
attachments.value.isNotEmpty) {
|
||||
@@ -680,11 +734,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
pickVideoMedia();
|
||||
}
|
||||
},
|
||||
onPickAudio: pickAudioMedia,
|
||||
onPickGeneralFile: pickGeneralFile,
|
||||
onLinkAttachment: linkAttachment,
|
||||
attachments: attachments.value,
|
||||
onUploadAttachment: uploadAttachment,
|
||||
onDeleteAttachment: (index) async {
|
||||
final attachment = attachments.value[index];
|
||||
if (attachment.isOnCloud) {
|
||||
if (attachment.isOnCloud && !attachment.isLink) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.delete(
|
||||
'/drive/files/${attachment.data.id}',
|
||||
|
@@ -11,6 +11,7 @@ import "package:island/models/file.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/widgets/content/attachment_preview.dart";
|
||||
import "package:island/widgets/shared/upload_menu.dart";
|
||||
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||
import "package:pasteboard/pasteboard.dart";
|
||||
import "package:styled_widget/styled_widget.dart";
|
||||
@@ -24,6 +25,9 @@ class ChatInput extends HookConsumerWidget {
|
||||
final VoidCallback onSend;
|
||||
final VoidCallback onClear;
|
||||
final Function(bool isPhoto) onPickFile;
|
||||
final VoidCallback onPickAudio;
|
||||
final VoidCallback onPickGeneralFile;
|
||||
final VoidCallback? onLinkAttachment;
|
||||
final SnChatMessage? messageReplyingTo;
|
||||
final SnChatMessage? messageForwardingTo;
|
||||
final SnChatMessage? messageEditingTo;
|
||||
@@ -41,6 +45,9 @@ class ChatInput extends HookConsumerWidget {
|
||||
required this.onSend,
|
||||
required this.onClear,
|
||||
required this.onPickFile,
|
||||
required this.onPickAudio,
|
||||
required this.onPickGeneralFile,
|
||||
this.onLinkAttachment,
|
||||
required this.messageReplyingTo,
|
||||
required this.messageForwardingTo,
|
||||
required this.messageEditingTo,
|
||||
@@ -336,31 +343,32 @@ class ChatInput extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Symbols.photo_library),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
onTap: () => onPickFile(true),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(Symbols.photo),
|
||||
Text('addPhoto').tr(),
|
||||
],
|
||||
UploadMenu(
|
||||
items: [
|
||||
MenuItemData(
|
||||
Symbols.add_a_photo,
|
||||
'addPhoto',
|
||||
() => onPickFile(true),
|
||||
),
|
||||
MenuItemData(
|
||||
Symbols.videocam,
|
||||
'addVideo',
|
||||
() => onPickFile(false),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => onPickFile(false),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(Symbols.video_call),
|
||||
Text('addVideo').tr(),
|
||||
],
|
||||
MenuItemData(Symbols.mic, 'addAudio', onPickAudio),
|
||||
MenuItemData(
|
||||
Symbols.file_upload,
|
||||
'uploadFile',
|
||||
onPickGeneralFile,
|
||||
),
|
||||
if (onLinkAttachment != null)
|
||||
MenuItemData(
|
||||
Symbols.attach_file,
|
||||
'linkAttachment',
|
||||
onLinkAttachment!,
|
||||
),
|
||||
],
|
||||
iconColor: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
205
lib/widgets/chat/chat_link_attachments.dart
Normal file
205
lib/widgets/chat/chat_link_attachments.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
part 'chat_link_attachments.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ChatCloudFileListNotifier extends _$ChatCloudFileListNotifier
|
||||
with CursorPagingNotifierMixin<SnCloudFile> {
|
||||
@override
|
||||
Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null);
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||
final take = 20;
|
||||
|
||||
final queryParameters = {'offset': offset, 'take': take};
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/files/me',
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<SnCloudFile> items =
|
||||
(response.data as List)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
|
||||
final hasMore = offset + items.length < total;
|
||||
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
||||
|
||||
return CursorPagingData(
|
||||
items: items,
|
||||
hasMore: hasMore,
|
||||
nextCursor: nextCursor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatLinkAttachment extends HookConsumerWidget {
|
||||
const ChatLinkAttachment({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final idController = useTextEditingController();
|
||||
final errorMessage = useState<String?>(null);
|
||||
|
||||
return SheetScaffold(
|
||||
heightFactor: 0.6,
|
||||
titleText: 'linkAttachment'.tr(),
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'attachmentsRecentUploads'.tr()),
|
||||
Tab(text: 'attachmentsManualInput'.tr()),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
PagingHelperView(
|
||||
provider: chatCloudFileListNotifierProvider,
|
||||
futureRefreshable: chatCloudFileListNotifierProvider.future,
|
||||
notifierRefreshable:
|
||||
chatCloudFileListNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => ListView.builder(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final item = data.items[index];
|
||||
final itemType =
|
||||
item.mimeType?.split('/').firstOrNull;
|
||||
return ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: switch (itemType) {
|
||||
'image' => CloudImageWidget(file: item),
|
||||
'audio' =>
|
||||
const Icon(
|
||||
Symbols.audio_file,
|
||||
fill: 1,
|
||||
).center(),
|
||||
'video' =>
|
||||
const Icon(
|
||||
Symbols.video_file,
|
||||
fill: 1,
|
||||
).center(),
|
||||
_ =>
|
||||
const Icon(
|
||||
Symbols.body_system,
|
||||
fill: 1,
|
||||
).center(),
|
||||
},
|
||||
),
|
||||
),
|
||||
title:
|
||||
item.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(item.name),
|
||||
onTap: () {
|
||||
Navigator.pop(context, item);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: idController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fileId'.tr(),
|
||||
helperText: 'fileIdHint'.tr(),
|
||||
helperMaxLines: 3,
|
||||
errorText: errorMessage.value,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(16),
|
||||
InkWell(
|
||||
child: Text(
|
||||
'fileIdLinkHint',
|
||||
).tr().fontSize(13).opacity(0.85),
|
||||
onTap: () {
|
||||
launchUrlString('https://fs.solian.app');
|
||||
},
|
||||
).padding(horizontal: 14),
|
||||
const Gap(16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('add'.tr()),
|
||||
onPressed: () async {
|
||||
final fileId = idController.text.trim();
|
||||
if (fileId.isEmpty) {
|
||||
errorMessage.value = 'fileIdCannotBeEmpty'.tr();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get(
|
||||
'/drive/files/$fileId/info',
|
||||
);
|
||||
final SnCloudFile cloudFile =
|
||||
SnCloudFile.fromJson(response.data);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop(cloudFile);
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'failedToFetchFile'.tr(
|
||||
args: [e.toString()],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
31
lib/widgets/chat/chat_link_attachments.g.dart
Normal file
31
lib/widgets/chat/chat_link_attachments.g.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'chat_link_attachments.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatCloudFileListNotifierHash() =>
|
||||
r'5da3929229fe00212530f63bd19ae4cd829176f5';
|
||||
|
||||
/// See also [ChatCloudFileListNotifier].
|
||||
@ProviderFor(ChatCloudFileListNotifier)
|
||||
final chatCloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||
ChatCloudFileListNotifier,
|
||||
CursorPagingData<SnCloudFile>
|
||||
>.internal(
|
||||
ChatCloudFileListNotifier.new,
|
||||
name: r'chatCloudFileListNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatCloudFileListNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ChatCloudFileListNotifier =
|
||||
AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@@ -6,6 +6,7 @@ import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:island/widgets/post/compose_embed_sheet.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
import 'package:island/widgets/post/draft_manager.dart';
|
||||
import 'package:island/widgets/shared/upload_menu.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -80,6 +81,13 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final uploadMenuItems = [
|
||||
MenuItemData(Symbols.add_a_photo, 'addPhoto', pickPhotoMedia),
|
||||
MenuItemData(Symbols.videocam, 'addVideo', pickVideoMedia),
|
||||
MenuItemData(Symbols.mic, 'addAudio', addAudio),
|
||||
MenuItemData(Symbols.file_upload, 'uploadFile', pickGeneralFile),
|
||||
];
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
if (isCompact) {
|
||||
@@ -96,86 +104,9 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
MenuAnchor(
|
||||
builder:
|
||||
(context, controller, child) => IconButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
tooltip: 'uploadFile'.tr(),
|
||||
icon: const Icon(Symbols.file_upload),
|
||||
color: colorScheme.primary,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
pickPhotoMedia();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
leadingIcon: const Icon(Symbols.add_a_photo),
|
||||
child: Text('addPhoto'.tr()),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
pickVideoMedia();
|
||||
},
|
||||
leadingIcon: const Icon(Symbols.videocam),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text('addVideo'.tr()),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
addAudio();
|
||||
},
|
||||
leadingIcon: const Icon(Symbols.mic),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text('addAudio'.tr()),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
pickGeneralFile();
|
||||
},
|
||||
leadingIcon: const Icon(Symbols.file_upload),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text('uploadFile'.tr()),
|
||||
),
|
||||
],
|
||||
UploadMenu(
|
||||
items: uploadMenuItems,
|
||||
isCompact: isCompact,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: linkAttachment,
|
||||
@@ -290,63 +221,7 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
MenuAnchor(
|
||||
builder:
|
||||
(context, controller, child) => IconButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
tooltip: 'uploadFile'.tr(),
|
||||
icon: const Icon(Symbols.file_upload),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
pickPhotoMedia();
|
||||
},
|
||||
leadingIcon: const Icon(Symbols.add_a_photo),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('addPhoto'.tr()),
|
||||
),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
pickVideoMedia();
|
||||
},
|
||||
leadingIcon: const Icon(Symbols.videocam),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('addVideo'.tr()),
|
||||
),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
addAudio();
|
||||
},
|
||||
leadingIcon: const Icon(Symbols.mic),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('addAudio'.tr()),
|
||||
),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
pickGeneralFile();
|
||||
},
|
||||
leadingIcon: const Icon(Symbols.file_upload),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('uploadFile'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
UploadMenu(items: uploadMenuItems, isCompact: isCompact),
|
||||
IconButton(
|
||||
onPressed: linkAttachment,
|
||||
icon: const Icon(Symbols.attach_file),
|
||||
|
64
lib/widgets/shared/upload_menu.dart
Normal file
64
lib/widgets/shared/upload_menu.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class MenuItemData {
|
||||
final IconData icon;
|
||||
final String textKey;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const MenuItemData(this.icon, this.textKey, this.onPressed);
|
||||
}
|
||||
|
||||
class UploadMenu extends StatelessWidget {
|
||||
final List<MenuItemData> items;
|
||||
final bool isCompact;
|
||||
final Color? iconColor;
|
||||
|
||||
const UploadMenu({
|
||||
super.key,
|
||||
required this.items,
|
||||
this.isCompact = false,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return MenuAnchor(
|
||||
builder:
|
||||
(context, controller, child) => IconButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
tooltip: 'uploadFile'.tr(),
|
||||
icon: const Icon(Symbols.file_upload),
|
||||
color: iconColor ?? colorScheme.primary,
|
||||
visualDensity:
|
||||
isCompact
|
||||
? const VisualDensity(horizontal: -4, vertical: -2)
|
||||
: null,
|
||||
),
|
||||
menuChildren:
|
||||
items
|
||||
.map(
|
||||
(item) => MenuItemButton(
|
||||
onPressed: item.onPressed,
|
||||
leadingIcon: Icon(item.icon),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
),
|
||||
),
|
||||
child: Text(item.textKey.tr()),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user