parent
1c2ba2971b
commit
0ad85f24d5
15 changed files with 285 additions and 24 deletions
34
lib/helpers/extensions/save_attachment.dart
Normal file
34
lib/helpers/extensions/save_attachment.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/helpers/extensions/show_error_snack_bar.dart";
|
||||||
|
import "package:nexus/helpers/extensions/with_progress_snackbar.dart";
|
||||||
|
import "package:nexus/helpers/file_dialog_helper.dart";
|
||||||
|
|
||||||
|
extension SaveAttachment on BuildContext {
|
||||||
|
/// Downloads the media at [mxc], displaying a snackbar whilst it's donwnloading,
|
||||||
|
/// then lets the user pick where to save it, suggesting [filename].
|
||||||
|
Future<void> saveAttachment(
|
||||||
|
WidgetRef ref,
|
||||||
|
Uri mxc, {
|
||||||
|
required bool encrypted,
|
||||||
|
String? filename,
|
||||||
|
}) async {
|
||||||
|
final file = await withProgressSnackbar(
|
||||||
|
"Downloading file...",
|
||||||
|
() => ref
|
||||||
|
.read(ClientController.provider.notifier)
|
||||||
|
.downloadMedia(.new(mxc: mxc, encrypted: encrypted)),
|
||||||
|
);
|
||||||
|
if (file == null || !mounted) return;
|
||||||
|
|
||||||
|
final messenger = ScaffoldMessenger.of(this);
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.read(FileDialogHelper.provider)
|
||||||
|
.saveFile(file, filename ?? mxc.pathSegments.last);
|
||||||
|
} catch (error) {
|
||||||
|
messenger.showErrorSnackBar(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
lib/helpers/file_dialog_helper.dart
Normal file
93
lib/helpers/file_dialog_helper.dart
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:file_selector/file_selector.dart";
|
||||||
|
import "package:flutter_file_dialog/flutter_file_dialog.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:mime/mime.dart";
|
||||||
|
import "package:nexus/controllers/image_picker.dart";
|
||||||
|
import "package:nexus/controllers/portal.dart";
|
||||||
|
import "package:xdg_desktop_portal/xdg_desktop_portal.dart";
|
||||||
|
|
||||||
|
class FileDialogHelper(final Ref ref) {
|
||||||
|
Future<bool> saveFile(File file, String filename) async {
|
||||||
|
final mimeType = lookupMimeType(
|
||||||
|
file.path,
|
||||||
|
headerBytes: await file
|
||||||
|
.openRead(0, defaultMagicNumbersMaxLength)
|
||||||
|
.expand((bytes) => bytes)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final filenameWithExtension = mimeType == null
|
||||||
|
? filename
|
||||||
|
: "$filename.${extensionFromMime(mimeType)}";
|
||||||
|
|
||||||
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
|
return await FlutterFileDialog.saveFile(
|
||||||
|
params: .new(
|
||||||
|
sourceFilePath: file.path,
|
||||||
|
fileName: filenameWithExtension,
|
||||||
|
),
|
||||||
|
) !=
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final destination = Platform.isLinux
|
||||||
|
? await getLinuxSaveLocation(filenameWithExtension)
|
||||||
|
: (await getSaveLocation(suggestedName: filenameWithExtension))?.path;
|
||||||
|
if (destination == null) return false;
|
||||||
|
|
||||||
|
await file.copy(destination);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLinuxSaveLocation(String filename) async {
|
||||||
|
final portal = await ref.read(PortalController.provider.future);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await portal.fileChooser
|
||||||
|
.saveFile(title: "Save file", currentName: filename)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
return switch (result.uris.firstOrNull) {
|
||||||
|
final uri? => Uri.parse(uri).toFilePath(),
|
||||||
|
null => null,
|
||||||
|
};
|
||||||
|
} on XdgPortalRequestCancelledException catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lets the user pick a file, or only images if [images] is true. Returns
|
||||||
|
/// null if the user cancelled.
|
||||||
|
Future<XFile?> pickFile({bool images = false}) async {
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
return images
|
||||||
|
? ref.read(ImagePickerController.provider).pickImage(source: .gallery)
|
||||||
|
: openFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
final portal = await ref.read(PortalController.provider.future);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await portal.fileChooser
|
||||||
|
.openFile(
|
||||||
|
title: images ? "Pick image" : "Pick file",
|
||||||
|
filters: [
|
||||||
|
if (images)
|
||||||
|
.new("Images", [XdgFileChooserMimeTypePattern("image/*")]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
return switch (result.uris.firstOrNull) {
|
||||||
|
final uri? => XFile(Uri.parse(uri).toFilePath()),
|
||||||
|
null => null,
|
||||||
|
};
|
||||||
|
} on XdgPortalRequestCancelledException catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = Provider<FileDialogHelper>(FileDialogHelper.new);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:file_selector/file_selector.dart";
|
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/widgets/emoji_picker.dart";
|
import "package:nexus/widgets/emoji_picker.dart";
|
||||||
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
||||||
|
|
@ -13,6 +12,7 @@ import "package:fluttertagger/fluttertagger.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/controllers/attachment.dart";
|
import "package:nexus/controllers/attachment.dart";
|
||||||
import "package:nexus/controllers/image_picker.dart";
|
import "package:nexus/controllers/image_picker.dart";
|
||||||
|
import "package:nexus/helpers/file_dialog_helper.dart";
|
||||||
import "package:image_picker/image_picker.dart";
|
import "package:image_picker/image_picker.dart";
|
||||||
import "package:nexus/controllers/power_level.dart";
|
import "package:nexus/controllers/power_level.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
|
|
@ -245,14 +245,24 @@ class const Composer(
|
||||||
leading: Icon(icon),
|
leading: Icon(icon),
|
||||||
),
|
),
|
||||||
onTap: () => attach(
|
onTap: () => attach(
|
||||||
ref
|
source == .gallery
|
||||||
.read(ImagePickerController.provider)
|
? ref
|
||||||
.pickImage(source: source),
|
.read(FileDialogHelper.provider)
|
||||||
|
.pickFile(images: true)
|
||||||
|
: ref
|
||||||
|
.read(
|
||||||
|
ImagePickerController
|
||||||
|
.provider,
|
||||||
|
)
|
||||||
|
.pickImage(source: source),
|
||||||
).onError(showError),
|
).onError(showError),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () =>
|
onTap: () => attach(
|
||||||
attach(openFile()).onError(showError),
|
ref
|
||||||
|
.read(FileDialogHelper.provider)
|
||||||
|
.pickFile(),
|
||||||
|
).onError(showError),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text("Files"),
|
title: Text("Files"),
|
||||||
leading: Icon(Icons.attachment),
|
leading: Icon(Icons.attachment),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:m3e_buttons/m3e_buttons.dart";
|
import "package:m3e_buttons/m3e_buttons.dart";
|
||||||
|
import "package:nexus/helpers/extensions/save_attachment.dart";
|
||||||
import "package:nexus/helpers/mxc_image.dart";
|
import "package:nexus/helpers/mxc_image.dart";
|
||||||
import "package:nexus/models/requests/download_media.dart";
|
import "package:nexus/models/requests/download_media.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
|
@ -8,6 +9,9 @@ import "package:nexus/widgets/error_dialog.dart";
|
||||||
final class const ExpandableImage(
|
final class const ExpandableImage(
|
||||||
final DownloadMediaRequest? request, {
|
final DownloadMediaRequest? request, {
|
||||||
required final Widget child,
|
required final Widget child,
|
||||||
|
|
||||||
|
/// If set, a download button is shown, suggesting this filename
|
||||||
|
final String? downloadFilename,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends ConsumerWidget {
|
}) extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -38,9 +42,25 @@ final class const ExpandableImage(
|
||||||
alignment: .topRight,
|
alignment: .topRight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: .all(32),
|
padding: .all(32),
|
||||||
child: M3EButton(
|
child: Row(
|
||||||
onPressed: Navigator.of(context).pop,
|
mainAxisSize: .min,
|
||||||
child: Icon(Icons.close),
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
if (downloadFilename != null)
|
||||||
|
M3EButton(
|
||||||
|
onPressed: () => context.saveAttachment(
|
||||||
|
ref,
|
||||||
|
request!.mxc,
|
||||||
|
encrypted: request!.encrypted,
|
||||||
|
filename: downloadFilename,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.download),
|
||||||
|
),
|
||||||
|
M3EButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
child: Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/helpers/extensions/save_attachment.dart";
|
||||||
import "package:nexus/helpers/extensions/size_to_string.dart";
|
import "package:nexus/helpers/extensions/size_to_string.dart";
|
||||||
import "package:nexus/models/info/file.dart";
|
import "package:nexus/models/info/file.dart";
|
||||||
|
|
||||||
|
|
@ -6,10 +8,11 @@ final class const FileCard(
|
||||||
final Uri uri,
|
final Uri uri,
|
||||||
final FileInfo? info, {
|
final FileInfo? info, {
|
||||||
final String? filename,
|
final String? filename,
|
||||||
|
final bool encrypted = false,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends StatelessWidget {
|
}) extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => SizedBox(
|
Widget build(BuildContext context, WidgetRef ref) => SizedBox(
|
||||||
width: 320,
|
width: 320,
|
||||||
child: Card(
|
child: Card(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
|
@ -17,8 +20,15 @@ final class const FileCard(
|
||||||
leading: Icon(Icons.file_copy),
|
leading: Icon(Icons.file_copy),
|
||||||
title: Text(filename ?? "file", maxLines: 1, overflow: .ellipsis),
|
title: Text(filename ?? "file", maxLines: 1, overflow: .ellipsis),
|
||||||
subtitle: info?.size == null ? null : Text(info!.size!.sizeAsString),
|
subtitle: info?.size == null ? null : Text(info!.size!.sizeAsString),
|
||||||
// TODO: Downloading files
|
trailing: IconButton(
|
||||||
trailing: IconButton(onPressed: null, icon: Icon(Icons.download)),
|
onPressed: () => context.saveAttachment(
|
||||||
|
ref,
|
||||||
|
uri,
|
||||||
|
encrypted: encrypted,
|
||||||
|
filename: filename,
|
||||||
|
),
|
||||||
|
icon: Icon(Icons.download),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ class const Html(
|
||||||
alignment: PlaceholderAlignment.middle,
|
alignment: PlaceholderAlignment.middle,
|
||||||
child: ExpandableImage(
|
child: ExpandableImage(
|
||||||
.new(mxc: src),
|
.new(mxc: src),
|
||||||
|
downloadFilename: src.pathSegments.last,
|
||||||
child: Image(
|
child: Image(
|
||||||
image: MxcImage(ref, .new(mxc: src)),
|
image: MxcImage(ref, .new(mxc: src)),
|
||||||
errorBuilder: (_, error, _) => Text(
|
errorBuilder: (_, error, _) => Text(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ final class const MessageImage(
|
||||||
final Uri url, {
|
final Uri url, {
|
||||||
final i.ImageInfo? info,
|
final i.ImageInfo? info,
|
||||||
required final bool encrypted,
|
required final bool encrypted,
|
||||||
|
final String? filename,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends ConsumerWidget {
|
}) extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -18,6 +19,7 @@ final class const MessageImage(
|
||||||
final request = DownloadMediaRequest(mxc: url, encrypted: encrypted);
|
final request = DownloadMediaRequest(mxc: url, encrypted: encrypted);
|
||||||
return ExpandableImage(
|
return ExpandableImage(
|
||||||
request,
|
request,
|
||||||
|
downloadFilename: filename,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: .all(.circular(8)),
|
borderRadius: .all(.circular(8)),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:media_kit/media_kit.dart";
|
import "package:media_kit/media_kit.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/helpers/extensions/save_attachment.dart";
|
||||||
import "package:nexus/models/info/audio.dart";
|
import "package:nexus/models/info/audio.dart";
|
||||||
import "package:nexus/widgets/players/player_controls.dart";
|
import "package:nexus/widgets/players/player_controls.dart";
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ class const AudioPlayer(
|
||||||
final Uri uri,
|
final Uri uri,
|
||||||
final AudioInfo? info, {
|
final AudioInfo? info, {
|
||||||
final bool encrypted = false,
|
final bool encrypted = false,
|
||||||
|
final String? filename,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends HookConsumerWidget {
|
}) extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -38,8 +40,24 @@ class const AudioPlayer(
|
||||||
child: Card(
|
child: Card(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: .only(left: 8, right: 16),
|
padding: .symmetric(horizontal: 8),
|
||||||
child: PlayerControls(player, loading: !loaded.value),
|
child: PlayerControls(
|
||||||
|
player,
|
||||||
|
loading: !loaded.value,
|
||||||
|
actions: .new([
|
||||||
|
SizedBox(width: 4),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.saveAttachment(
|
||||||
|
ref,
|
||||||
|
uri,
|
||||||
|
encrypted: encrypted,
|
||||||
|
filename: filename,
|
||||||
|
),
|
||||||
|
tooltip: "Download",
|
||||||
|
icon: Icon(Icons.download),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import "dart:async";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/helpers/extensions/save_attachment.dart";
|
||||||
import "package:nexus/models/info/video.dart";
|
import "package:nexus/models/info/video.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:media_kit/media_kit.dart";
|
import "package:media_kit/media_kit.dart";
|
||||||
|
|
@ -13,6 +14,7 @@ class const VideoPlayer(
|
||||||
final Uri uri,
|
final Uri uri,
|
||||||
final VideoInfo? info, {
|
final VideoInfo? info, {
|
||||||
final bool encrypted = false,
|
final bool encrypted = false,
|
||||||
|
final String? filename,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends HookConsumerWidget {
|
}) extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -61,6 +63,24 @@ class const VideoPlayer(
|
||||||
onTap: loaded.value ? player.playOrPause : null,
|
onTap: loaded.value ? player.playOrPause : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => context.saveAttachment(
|
||||||
|
ref,
|
||||||
|
uri,
|
||||||
|
encrypted: encrypted,
|
||||||
|
filename: filename,
|
||||||
|
),
|
||||||
|
color: Colors.white,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black38,
|
||||||
|
),
|
||||||
|
tooltip: "Download",
|
||||||
|
icon: Icon(Icons.download),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
|
|
|
||||||
|
|
@ -110,14 +110,53 @@ class const MessageRenderer(
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: .loose(.square(500)),
|
constraints: .loose(.square(500)),
|
||||||
child: switch (event.content) {
|
child: switch (event.content) {
|
||||||
VideoMessageContent(:final info, :final file) =>
|
VideoMessageContent(
|
||||||
VideoPlayer(url, info, encrypted: file != null),
|
:final info,
|
||||||
AudioMessageContent(:final info, :final file) =>
|
:final file,
|
||||||
AudioPlayer(url, info, encrypted: file != null),
|
:final filename,
|
||||||
FileMessageContent(:final info, :final filename) =>
|
:final body,
|
||||||
FileCard(url, info, filename: filename),
|
) =>
|
||||||
ImageMessageContent(:final info, :final file) =>
|
VideoPlayer(
|
||||||
MessageImage(url, info: info, encrypted: file != null),
|
url,
|
||||||
|
info,
|
||||||
|
encrypted: file != null,
|
||||||
|
filename: filename ?? body,
|
||||||
|
),
|
||||||
|
AudioMessageContent(
|
||||||
|
:final info,
|
||||||
|
:final file,
|
||||||
|
:final filename,
|
||||||
|
:final body,
|
||||||
|
) =>
|
||||||
|
AudioPlayer(
|
||||||
|
url,
|
||||||
|
info,
|
||||||
|
encrypted: file != null,
|
||||||
|
filename: filename ?? body,
|
||||||
|
),
|
||||||
|
FileMessageContent(
|
||||||
|
:final info,
|
||||||
|
:final filename,
|
||||||
|
:final file,
|
||||||
|
) =>
|
||||||
|
FileCard(
|
||||||
|
url,
|
||||||
|
info,
|
||||||
|
filename: filename,
|
||||||
|
encrypted: file != null,
|
||||||
|
),
|
||||||
|
ImageMessageContent(
|
||||||
|
:final info,
|
||||||
|
:final file,
|
||||||
|
:final filename,
|
||||||
|
:final body,
|
||||||
|
) =>
|
||||||
|
MessageImage(
|
||||||
|
url,
|
||||||
|
info: info,
|
||||||
|
encrypted: file != null,
|
||||||
|
filename: filename ?? body,
|
||||||
|
),
|
||||||
_ => SizedBox.shrink(),
|
_ => SizedBox.shrink(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ class const RoomSummarySheet(
|
||||||
if (summary.avatarUrl != null)
|
if (summary.avatarUrl != null)
|
||||||
ExpandableImage(
|
ExpandableImage(
|
||||||
summary.avatarUrl == null ? null : .new(mxc: summary.avatarUrl!),
|
summary.avatarUrl == null ? null : .new(mxc: summary.avatarUrl!),
|
||||||
|
downloadFilename: name,
|
||||||
child: AvatarOrHash(
|
child: AvatarOrHash(
|
||||||
summary.avatarUrl,
|
summary.avatarUrl,
|
||||||
"",
|
"",
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,8 @@ class const SpaceHierarchySheet(final String spaceId, {super.key})
|
||||||
if (space?.avatarUrl case final avatarUrl?)
|
if (space?.avatarUrl case final avatarUrl?)
|
||||||
ExpandableImage(
|
ExpandableImage(
|
||||||
.new(mxc: avatarUrl),
|
.new(mxc: avatarUrl),
|
||||||
|
downloadFilename:
|
||||||
|
space?.name ?? space?.canonicalAlias ?? spaceId,
|
||||||
child: AvatarOrHash(avatarUrl, "", dimension: 32),
|
child: AvatarOrHash(avatarUrl, "", dimension: 32),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ final class const UserBottomSheet(
|
||||||
|
|
||||||
ExpandableImage(
|
ExpandableImage(
|
||||||
member.avatarUrl == null ? null : .new(mxc: member.avatarUrl!),
|
member.avatarUrl == null ? null : .new(mxc: member.avatarUrl!),
|
||||||
|
downloadFilename: member.displayName ?? userId.localpart,
|
||||||
child: AvatarOrHash(
|
child: AvatarOrHash(
|
||||||
member.avatarUrl,
|
member.avatarUrl,
|
||||||
member.displayName ?? userId.localpart,
|
member.displayName ?? userId.localpart,
|
||||||
|
|
|
||||||
10
pubspec.lock
10
pubspec.lock
|
|
@ -455,6 +455,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.1"
|
version: "0.9.1"
|
||||||
|
flutter_file_dialog:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_file_dialog
|
||||||
|
sha256: "5d270d69b6a5fdf45bdb88ebcf590e073dd89dfb45526ac83a64524ba57303b5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.3"
|
||||||
flutter_hooks:
|
flutter_hooks:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -985,7 +993,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.0"
|
version: "1.19.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: mime
|
name: mime
|
||||||
sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6
|
sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@ dependencies:
|
||||||
url: https://github.com/Henry-Hiles/xdg_desktop_portal.dart
|
url: https://github.com/Henry-Hiles/xdg_desktop_portal.dart
|
||||||
ref: quad/nexus
|
ref: quad/nexus
|
||||||
pasteboard: 0.5.0
|
pasteboard: 0.5.0
|
||||||
|
flutter_file_dialog: 3.3.3
|
||||||
|
mime: 2.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: 2.16.2
|
build_runner: 2.16.2
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue