fix: save unsupported media formats to folder instead of gallery (#2766)

This commit is contained in:
Shlomo
2025-10-27 03:33:43 +02:00
committed by GitHub
parent 96a37c71b9
commit a9e01b078a
2 changed files with 92 additions and 51 deletions
@@ -495,30 +495,19 @@ class ReceiveController {
),
);
final fileType = receivingFile.file.fileType;
final saveToGallery = receiveState.saveToGallery && (fileType == FileType.image || fileType == FileType.video);
final shouldSaveToGallery = receiveState.saveToGallery && (fileType == FileType.image || fileType == FileType.video);
String? outerDestinationPath;
String? filePath;
bool savedToGallery = false;
try {
final (destinationPath, documentUri, finalName) = await digestFilePathAndPrepareDirectory(
parentDirectory: saveToGallery ? receiveState.cacheDirectory : receiveState.destinationDirectory,
_logger.info('Saving ${receivingFile.file.fileName}');
(savedToGallery, filePath) = await saveFile(
destinationDirectory: receiveState.destinationDirectory,
fileName: receivingFile.desiredName!,
createdDirectories: receiveState.createdDirectories,
);
outerDestinationPath = destinationPath;
_logger.info('Saving ${receivingFile.file.fileName} to $destinationPath');
await saveFile(
destinationPath: destinationPath,
documentUri: documentUri,
name: finalName,
saveToGallery: saveToGallery,
saveToGallery: shouldSaveToGallery,
isImage: fileType == FileType.image,
stream: request,
androidSdkInt: server.ref.read(deviceInfoProvider).androidSdkInt,
lastModified: receivingFile.file.metadata?.lastModified,
lastAccessed: receivingFile.file.metadata?.lastAccessed,
onProgress: (savedBytes) {
if (receivingFile.file.size != 0) {
server.ref
@@ -530,6 +519,10 @@ class ReceiveController {
);
}
},
lastModified: receivingFile.file.metadata?.lastModified,
lastAccessed: receivingFile.file.metadata?.lastAccessed,
androidSdkInt: server.ref.read(deviceInfoProvider).androidSdkInt,
createdDirectories: receiveState.createdDirectories,
);
if (server.getState().session == null || !allowedStates.contains(server.getState().session!.status)) {
return await request.respondJson(500, message: 'Server is in invalid state');
@@ -539,8 +532,8 @@ class ReceiveController {
session: oldState.session?.fileFinished(
fileId: fileId,
status: FileStatus.finished,
path: saveToGallery ? null : destinationPath,
savedToGallery: saveToGallery,
path: filePath,
savedToGallery: savedToGallery,
errorMessage: null,
),
),
@@ -554,8 +547,8 @@ class ReceiveController {
entryId: fileId,
fileName: receivingFile.desiredName!,
fileType: receivingFile.file.fileType,
path: saveToGallery ? null : destinationPath,
savedToGallery: saveToGallery,
path: filePath,
savedToGallery: savedToGallery,
isMessage: false,
fileSize: receivingFile.file.size,
senderAlias: receiveState.senderAlias,
@@ -622,13 +615,13 @@ class ReceiveController {
Routerino.context.pushRootImmediately(() => const HomePage(initialTab: HomeTab.receive, appStart: false));
// open the dialog to open file instantly
if (outerDestinationPath != null && outerDestinationPath.isNotEmpty) {
if (filePath != null && filePath.isNotEmpty) {
// ignore: discarded_futures
OpenFileDialog.open(
Routerino.context, // ignore: use_build_context_synchronously
filePath: outerDestinationPath,
filePath: filePath,
fileType: fileType,
openGallery: saveToGallery,
openGallery: savedToGallery,
);
}
});
+73 -25
View File
@@ -6,6 +6,7 @@ import 'package:legalize/legalize.dart';
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:localsend_app/util/native/channel/android_channel.dart' as android_channel;
import 'package:localsend_app/util/native/content_uri_helper.dart';
import 'package:localsend_app/util/native/directories.dart';
import 'package:logging/logging.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
@@ -16,51 +17,72 @@ final _logger = Logger('FileSaver');
final _saf = SafStream();
/// Saves the data [stream] to the [destinationPath].
/// [onProgress] will be called on every 100 ms.
Future<void> saveFile({
required String destinationPath,
required String? documentUri,
required String name,
/// Saves file from [stream] to [destinationDirectory] with [fileName].
///
/// When [saveToGallery] is true:
/// - Saves to cache directory first, then transfers to OS gallery (Photos/Videos)
/// - If format is unsupported (see https://github.com/natsuk4ze/gal/wiki/Formats),
/// moves the cached file to [destinationDirectory] as fallback
///
/// [onProgress] is called every 100ms with the number of bytes saved.
///
/// Returns (savedToGallery, filePath):
/// - savedToGallery: true if saved to gallery, false if saved to directory
/// - filePath: absolute path to file (null when saved to gallery)
///
/// Throws [GalException] for permission or I/O errors (file will be deleted on error)
Future<(bool, String?)> saveFile({
required String destinationDirectory,
required String fileName,
required bool saveToGallery,
required bool isImage,
required Stream<Uint8List> stream,
required int? androidSdkInt,
required DateTime? lastModified,
required DateTime? lastAccessed,
required void Function(int savedBytes) onProgress,
required void Function(int) onProgress,
required Set<String> createdDirectories,
int? androidSdkInt,
DateTime? lastModified,
DateTime? lastAccessed,
}) async {
final parentDirectory = saveToGallery ? await getCacheDirectory() : destinationDirectory;
final (destinationPath, documentUri, finalName) = await digestFilePathAndPrepareDirectory(
parentDirectory: parentDirectory,
fileName: fileName,
createdDirectories: createdDirectories,
);
// When saveToGallery is enabled, cache directory is used so SAF is not needed
if (!saveToGallery && androidSdkInt != null) {
// Use SAF to save the file
// When saveToGallery is enabled, the destination is always the app's cache directory so we don't need to use SAF
SafWriteStreamInfo? safInfo;
if (documentUri != null || destinationPath.startsWith('content://')) {
_logger.info('Using SAF to save file to ${documentUri ?? destinationPath} as $name');
_logger.info('Using SAF to save file to ${documentUri ?? destinationPath} as $finalName');
safInfo = await _saf.startWriteStream(
documentUri ?? destinationPath,
name,
lookupMimeType(name) ?? (isImage ? 'image/*' : '*/*'),
finalName,
lookupMimeType(finalName) ?? (isImage ? 'image/*' : '*/*'),
);
} else {
final sdCardPath = getSdCardPath(destinationPath);
if (sdCardPath != null) {
// Use Android SAF to save the file to the SD card
final uriString = ContentUriHelper.encodeTreeUri(sdCardPath.path.parentPath());
_logger.info('Using SAF to save file to $uriString');
safInfo = await _saf.startWriteStream(
'content://com.android.externalstorage.documents/tree/${sdCardPath.sdCardId}:$uriString',
name,
lookupMimeType(name) ?? (isImage ? 'image/*' : '*/*'),
finalName,
lookupMimeType(finalName) ?? (isImage ? 'image/*' : '*/*'),
);
}
}
if (safInfo != null) {
final sessionID = safInfo.session;
await _saveFile(
return await _saveFile(
destinationPath: destinationPath,
saveToGallery: saveToGallery,
destinationDirectory: destinationDirectory,
fileName: fileName,
createdDirectories: createdDirectories,
isImage: isImage,
stream: stream,
onProgress: onProgress,
@@ -73,15 +95,17 @@ Future<void> saveFile({
await _saf.endWriteStream(sessionID);
},
);
return;
}
}
final file = File(destinationPath);
final sink = file.openWrite();
await _saveFile(
return await _saveFile(
destinationPath: destinationPath,
saveToGallery: saveToGallery,
destinationDirectory: destinationDirectory,
fileName: fileName,
createdDirectories: createdDirectories,
isImage: isImage,
stream: stream,
onProgress: onProgress,
@@ -104,12 +128,15 @@ Future<void> saveFile({
);
}
Future<void> _saveFile({
Future<(bool, String?)> _saveFile({
required String destinationPath,
required bool saveToGallery,
required String destinationDirectory,
required String fileName,
required Set<String> createdDirectories,
required bool isImage,
required Stream<Uint8List> stream,
required void Function(int savedBytes) onProgress,
required void Function(int) onProgress,
required void Function(Uint8List data)? write,
required Future<void> Function(Uint8List data)? writeAsync,
required Future<void> Function()? flush,
@@ -143,11 +170,32 @@ Future<void> _saveFile({
await close();
if (saveToGallery) {
isImage ? await Gal.putImage(destinationPath) : await Gal.putVideo(destinationPath);
await File(destinationPath).delete();
try {
isImage ? await Gal.putImage(destinationPath) : await Gal.putVideo(destinationPath);
await File(destinationPath).delete();
onProgress(savedBytes);
return (true, null);
} on GalException catch (e) {
if (e.type == GalExceptionType.notSupportedFormat) {
_logger.info('File format not supported by gallery, moving to destination directory');
final (fallbackPath, _, _) = await digestFilePathAndPrepareDirectory(
parentDirectory: destinationDirectory,
fileName: fileName,
createdDirectories: createdDirectories,
);
_logger.info('Moving file from $destinationPath to $fallbackPath');
await File(destinationPath).rename(fallbackPath);
onProgress(savedBytes);
return (false, fallbackPath);
}
rethrow;
}
}
onProgress(savedBytes); // always emit final event
return (false, destinationPath); // Saved to destination (not gallery)
} catch (_) {
try {
await close();