✨ Chat input full featured upload
This commit is contained in:
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'70acac63c720987d8b1688500e3735f1c2d16fdc';
|
String _$messagesNotifierHash() => r'e4b760068f7349cc2991d0788055dbd855184f82';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
@@ -31,6 +31,7 @@ import "package:super_sliver_list/super_sliver_list.dart";
|
|||||||
import "package:material_symbols_icons/symbols.dart";
|
import "package:material_symbols_icons/symbols.dart";
|
||||||
import "package:island/widgets/chat/call_button.dart";
|
import "package:island/widgets/chat/call_button.dart";
|
||||||
import "package:island/widgets/chat/chat_input.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";
|
import "package:island/widgets/chat/public_room_preview.dart";
|
||||||
|
|
||||||
class ChatRoomScreen extends HookConsumerWidget {
|
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() {
|
void sendMessage() {
|
||||||
if (messageController.text.trim().isNotEmpty ||
|
if (messageController.text.trim().isNotEmpty ||
|
||||||
attachments.value.isNotEmpty) {
|
attachments.value.isNotEmpty) {
|
||||||
@@ -680,11 +734,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
pickVideoMedia();
|
pickVideoMedia();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPickAudio: pickAudioMedia,
|
||||||
|
onPickGeneralFile: pickGeneralFile,
|
||||||
|
onLinkAttachment: linkAttachment,
|
||||||
attachments: attachments.value,
|
attachments: attachments.value,
|
||||||
onUploadAttachment: uploadAttachment,
|
onUploadAttachment: uploadAttachment,
|
||||||
onDeleteAttachment: (index) async {
|
onDeleteAttachment: (index) async {
|
||||||
final attachment = attachments.value[index];
|
final attachment = attachments.value[index];
|
||||||
if (attachment.isOnCloud) {
|
if (attachment.isOnCloud && !attachment.isLink) {
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
await client.delete(
|
await client.delete(
|
||||||
'/drive/files/${attachment.data.id}',
|
'/drive/files/${attachment.data.id}',
|
||||||
|
@@ -11,6 +11,7 @@ import "package:island/models/file.dart";
|
|||||||
import "package:island/pods/config.dart";
|
import "package:island/pods/config.dart";
|
||||||
import "package:island/services/responsive.dart";
|
import "package:island/services/responsive.dart";
|
||||||
import "package:island/widgets/content/attachment_preview.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:material_symbols_icons/material_symbols_icons.dart";
|
||||||
import "package:pasteboard/pasteboard.dart";
|
import "package:pasteboard/pasteboard.dart";
|
||||||
import "package:styled_widget/styled_widget.dart";
|
import "package:styled_widget/styled_widget.dart";
|
||||||
@@ -24,6 +25,9 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
final VoidCallback onSend;
|
final VoidCallback onSend;
|
||||||
final VoidCallback onClear;
|
final VoidCallback onClear;
|
||||||
final Function(bool isPhoto) onPickFile;
|
final Function(bool isPhoto) onPickFile;
|
||||||
|
final VoidCallback onPickAudio;
|
||||||
|
final VoidCallback onPickGeneralFile;
|
||||||
|
final VoidCallback? onLinkAttachment;
|
||||||
final SnChatMessage? messageReplyingTo;
|
final SnChatMessage? messageReplyingTo;
|
||||||
final SnChatMessage? messageForwardingTo;
|
final SnChatMessage? messageForwardingTo;
|
||||||
final SnChatMessage? messageEditingTo;
|
final SnChatMessage? messageEditingTo;
|
||||||
@@ -41,6 +45,9 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
required this.onSend,
|
required this.onSend,
|
||||||
required this.onClear,
|
required this.onClear,
|
||||||
required this.onPickFile,
|
required this.onPickFile,
|
||||||
|
required this.onPickAudio,
|
||||||
|
required this.onPickGeneralFile,
|
||||||
|
this.onLinkAttachment,
|
||||||
required this.messageReplyingTo,
|
required this.messageReplyingTo,
|
||||||
required this.messageForwardingTo,
|
required this.messageForwardingTo,
|
||||||
required this.messageEditingTo,
|
required this.messageEditingTo,
|
||||||
@@ -336,31 +343,32 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
UploadMenu(
|
||||||
icon: const Icon(Symbols.photo_library),
|
items: [
|
||||||
itemBuilder:
|
MenuItemData(
|
||||||
(context) => [
|
Symbols.add_a_photo,
|
||||||
PopupMenuItem(
|
'addPhoto',
|
||||||
onTap: () => onPickFile(true),
|
() => onPickFile(true),
|
||||||
child: Row(
|
),
|
||||||
spacing: 12,
|
MenuItemData(
|
||||||
children: [
|
Symbols.videocam,
|
||||||
const Icon(Symbols.photo),
|
'addVideo',
|
||||||
Text('addPhoto').tr(),
|
() => onPickFile(false),
|
||||||
],
|
),
|
||||||
),
|
MenuItemData(Symbols.mic, 'addAudio', onPickAudio),
|
||||||
),
|
MenuItemData(
|
||||||
PopupMenuItem(
|
Symbols.file_upload,
|
||||||
onTap: () => onPickFile(false),
|
'uploadFile',
|
||||||
child: Row(
|
onPickGeneralFile,
|
||||||
spacing: 12,
|
),
|
||||||
children: [
|
if (onLinkAttachment != null)
|
||||||
const Icon(Symbols.video_call),
|
MenuItemData(
|
||||||
Text('addVideo').tr(),
|
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_embed_sheet.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
import 'package:island/widgets/post/draft_manager.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:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.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;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
if (isCompact) {
|
if (isCompact) {
|
||||||
@@ -96,86 +104,9 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
MenuAnchor(
|
UploadMenu(
|
||||||
builder:
|
items: uploadMenuItems,
|
||||||
(context, controller, child) => IconButton(
|
isCompact: isCompact,
|
||||||
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()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: linkAttachment,
|
onPressed: linkAttachment,
|
||||||
@@ -290,63 +221,7 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
MenuAnchor(
|
UploadMenu(items: uploadMenuItems, isCompact: isCompact),
|
||||||
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()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: linkAttachment,
|
onPressed: linkAttachment,
|
||||||
icon: const Icon(Symbols.attach_file),
|
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