mirror of
https://github.com/immich-app/immich.git
synced 2026-07-10 06:03:14 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1989bf6b6f | |||
| 352c8086f9 |
+2
-2
@@ -431,8 +431,8 @@
|
|||||||
"transcoding_realtime_description": "Permet au transcodage d'être réalisé en temps réel durant la diffusion du flux de la vidéo. Active la bascule automatique entre résolutions, mais peut entraîner une latence importante de lecture et des microcoupures en fonction des capacités du serveur.",
|
"transcoding_realtime_description": "Permet au transcodage d'être réalisé en temps réel durant la diffusion du flux de la vidéo. Active la bascule automatique entre résolutions, mais peut entraîner une latence importante de lecture et des microcoupures en fonction des capacités du serveur.",
|
||||||
"transcoding_realtime_enabled": "Activer la transcodification en temps réel",
|
"transcoding_realtime_enabled": "Activer la transcodification en temps réel",
|
||||||
"transcoding_realtime_enabled_description": "Si désactivé, le serveur refusera de démarrer de nouvelles sessions de transcodifications en temps réel.",
|
"transcoding_realtime_enabled_description": "Si désactivé, le serveur refusera de démarrer de nouvelles sessions de transcodifications en temps réel.",
|
||||||
"transcoding_realtime_resolutions": "Définitions",
|
"transcoding_realtime_resolutions": "Résolutions",
|
||||||
"transcoding_realtime_resolutions_description": "Les définitions proposées pour le transcodage en temps réel. Des définitions supérieures peuvent causer des soucis pendant la lecture si le serveur ne peut pas les transcoder suffisamment rapidement.",
|
"transcoding_realtime_resolutions_description": "Les résolutions proposées pour le transcodage en temps réel. Des résolutions supérieures peuvent causer des soucis pendant la lecture si le serveur ne peut pas les transcoder suffisamment rapidement.",
|
||||||
"transcoding_realtime_video_codecs": "Codecs vidéo",
|
"transcoding_realtime_video_codecs": "Codecs vidéo",
|
||||||
"transcoding_realtime_video_codecs_description": "Les codecs vidéo proposés pour le transcodage en temps réel. Les clients choisissent la meilleure option qu'ils supportent durant la lecture. AV1 est plus efficace que HEVC, qui est plus efficace que H.264. En utilisant l'accélération matérielle, sélectionnez seulement les codecs que l'accélérateur peut encoder. En utilisant le transcodage logiciel, notez que H.264 est plus rapide que AV1, qui est plus rapide que HEVC.",
|
"transcoding_realtime_video_codecs_description": "Les codecs vidéo proposés pour le transcodage en temps réel. Les clients choisissent la meilleure option qu'ils supportent durant la lecture. AV1 est plus efficace que HEVC, qui est plus efficace que H.264. En utilisant l'accélération matérielle, sélectionnez seulement les codecs que l'accélérateur peut encoder. En utilisant le transcodage logiciel, notez que H.264 est plus rapide que AV1, qui est plus rapide que HEVC.",
|
||||||
"transcoding_reference_frames": "Trames de référence",
|
"transcoding_reference_frames": "Trames de référence",
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import app.alextran.immich.images.LocalImageApi
|
|||||||
import app.alextran.immich.images.LocalImagesImpl
|
import app.alextran.immich.images.LocalImagesImpl
|
||||||
import app.alextran.immich.images.RemoteImageApi
|
import app.alextran.immich.images.RemoteImageApi
|
||||||
import app.alextran.immich.images.RemoteImagesImpl
|
import app.alextran.immich.images.RemoteImagesImpl
|
||||||
import app.alextran.immich.mediasave.MediaSavePlugin
|
|
||||||
import app.alextran.immich.permission.PermissionApi
|
import app.alextran.immich.permission.PermissionApi
|
||||||
import app.alextran.immich.permission.PermissionApiImpl
|
import app.alextran.immich.permission.PermissionApiImpl
|
||||||
import app.alextran.immich.sync.NativeSyncApi
|
import app.alextran.immich.sync.NativeSyncApi
|
||||||
@@ -64,7 +63,6 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||||
|
|
||||||
flutterEngine.plugins.add(ViewIntentPlugin())
|
flutterEngine.plugins.add(ViewIntentPlugin())
|
||||||
flutterEngine.plugins.add(MediaSavePlugin())
|
|
||||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||||
flutterEngine.plugins.add(permissionApiImpl)
|
flutterEngine.plugins.add(permissionApiImpl)
|
||||||
|
|||||||
-97
@@ -1,97 +0,0 @@
|
|||||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
|
||||||
// See also: https://pub.dev/packages/pigeon
|
|
||||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
|
||||||
|
|
||||||
package app.alextran.immich.mediasave
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import io.flutter.plugin.common.BasicMessageChannel
|
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
|
||||||
import io.flutter.plugin.common.EventChannel
|
|
||||||
import io.flutter.plugin.common.MessageCodec
|
|
||||||
import io.flutter.plugin.common.StandardMethodCodec
|
|
||||||
import io.flutter.plugin.common.StandardMessageCodec
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
private object MediaSavePigeonUtils {
|
|
||||||
|
|
||||||
fun wrapResult(result: Any?): List<Any?> {
|
|
||||||
return listOf(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun wrapError(exception: Throwable): List<Any?> {
|
|
||||||
return if (exception is FlutterError) {
|
|
||||||
listOf(
|
|
||||||
exception.code,
|
|
||||||
exception.message,
|
|
||||||
exception.details
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
exception.javaClass.simpleName,
|
|
||||||
exception.toString(),
|
|
||||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
|
||||||
* @property code The error code.
|
|
||||||
* @property message The error message.
|
|
||||||
* @property details The error details. Must be a datatype supported by the api codec.
|
|
||||||
*/
|
|
||||||
class FlutterError (
|
|
||||||
val code: String,
|
|
||||||
override val message: String? = null,
|
|
||||||
val details: Any? = null
|
|
||||||
) : RuntimeException()
|
|
||||||
private open class MediaSavePigeonCodec : StandardMessageCodec() {
|
|
||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
|
||||||
return super.readValueOfType(type, buffer)
|
|
||||||
}
|
|
||||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
|
||||||
super.writeValue(stream, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
|
||||||
interface MediaSaveApi {
|
|
||||||
fun saveToDownloads(filePath: String, title: String, relativePath: String?, callback: (Result<String?>) -> Unit)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** The codec used by MediaSaveApi. */
|
|
||||||
val codec: MessageCodec<Any?> by lazy {
|
|
||||||
MediaSavePigeonCodec()
|
|
||||||
}
|
|
||||||
/** Sets up an instance of `MediaSaveApi` to handle messages through the `binaryMessenger`. */
|
|
||||||
@JvmOverloads
|
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: MediaSaveApi?, messageChannelSuffix: String = "") {
|
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.MediaSaveApi.saveToDownloads$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { message, reply ->
|
|
||||||
val args = message as List<Any?>
|
|
||||||
val filePathArg = args[0] as String
|
|
||||||
val titleArg = args[1] as String
|
|
||||||
val relativePathArg = args[2] as String?
|
|
||||||
api.saveToDownloads(filePathArg, titleArg, relativePathArg) { result: Result<String?> ->
|
|
||||||
val error = result.exceptionOrNull()
|
|
||||||
if (error != null) {
|
|
||||||
reply.reply(MediaSavePigeonUtils.wrapError(error))
|
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MediaSavePigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package app.alextran.immich.mediasave
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MediaSavePlugin : FlutterPlugin, MediaSaveApi {
|
|
||||||
private var context: Context? = null
|
|
||||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
context = binding.applicationContext
|
|
||||||
MediaSaveApi.setUp(binding.binaryMessenger, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
MediaSaveApi.setUp(binding.binaryMessenger, null)
|
|
||||||
ioScope.cancel()
|
|
||||||
context = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveToDownloads(
|
|
||||||
filePath: String,
|
|
||||||
title: String,
|
|
||||||
relativePath: String?,
|
|
||||||
callback: (Result<String?>) -> Unit,
|
|
||||||
) {
|
|
||||||
val context = context ?: run {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ioScope.launch {
|
|
||||||
try {
|
|
||||||
callback(Result.success(insertIntoFiles(context, filePath, title, relativePath)))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
callback(Result.failure(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uses the Files collection, not Images: Images only accepts MIME types the
|
|
||||||
// platform knows and rejects raw formats like CR3, while Files accepts any
|
|
||||||
// type. The file lands under [relativePath] (Download/Immich), not the gallery.
|
|
||||||
private fun insertIntoFiles(
|
|
||||||
context: Context,
|
|
||||||
filePath: String,
|
|
||||||
title: String,
|
|
||||||
relativePath: String?,
|
|
||||||
): String? {
|
|
||||||
val resolver = context.contentResolver
|
|
||||||
val collection = MediaStore.Files.getContentUri("external")
|
|
||||||
val source = File(filePath)
|
|
||||||
// Anything reaching this fallback is a format the platform can't type, so
|
|
||||||
// store it as a generic binary. The file saves and stays openable.
|
|
||||||
val mimeType = "application/octet-stream"
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val values = ContentValues().apply {
|
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, title)
|
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
|
||||||
relativePath?.let { put(MediaStore.MediaColumns.RELATIVE_PATH, it) }
|
|
||||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
|
||||||
}
|
|
||||||
val uri = resolver.insert(collection, values) ?: return null
|
|
||||||
|
|
||||||
try {
|
|
||||||
val out = resolver.openOutputStream(uri)
|
|
||||||
if (out == null) {
|
|
||||||
resolver.delete(uri, null, null)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
out.use { source.inputStream().use { input -> input.copyTo(it) } }
|
|
||||||
resolver.update(uri, ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }, null, null)
|
|
||||||
return uri.lastPathSegment
|
|
||||||
} catch (e: Exception) {
|
|
||||||
resolver.delete(uri, null, null)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dir = File(Environment.getExternalStorageDirectory(), relativePath ?: Environment.DIRECTORY_DCIM).apply { mkdirs() }
|
|
||||||
val target = File(dir, title)
|
|
||||||
source.inputStream().use { input -> target.outputStream().use { input.copyTo(it) } }
|
|
||||||
val values = ContentValues().apply {
|
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, title)
|
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
put(MediaStore.MediaColumns.DATA, target.absolutePath)
|
|
||||||
}
|
|
||||||
return resolver.insert(collection, values)?.lastPathSegment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
|
||||||
// See also: https://pub.dev/packages/pigeon
|
|
||||||
// ignore_for_file: unused_import, unused_shown_name
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
|
||||||
|
|
||||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
|
||||||
if (replyList == null) {
|
|
||||||
throw PlatformException(
|
|
||||||
code: 'channel-error',
|
|
||||||
message: 'Unable to establish connection on channel: "$channelName".',
|
|
||||||
);
|
|
||||||
} else if (replyList.length > 1) {
|
|
||||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
|
||||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
|
||||||
throw PlatformException(
|
|
||||||
code: 'null-error',
|
|
||||||
message: 'Host platform returned null value for non-null return value.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return replyList.firstOrNull;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PigeonCodec extends StandardMessageCodec {
|
|
||||||
const _PigeonCodec();
|
|
||||||
@override
|
|
||||||
void writeValue(WriteBuffer buffer, Object? value) {
|
|
||||||
if (value is int) {
|
|
||||||
buffer.putUint8(4);
|
|
||||||
buffer.putInt64(value);
|
|
||||||
} else {
|
|
||||||
super.writeValue(buffer, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
|
||||||
switch (type) {
|
|
||||||
default:
|
|
||||||
return super.readValueOfType(type, buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MediaSaveApi {
|
|
||||||
/// Constructor for [MediaSaveApi]. The [binaryMessenger] named argument is
|
|
||||||
/// available for dependency injection. If it is left null, the default
|
|
||||||
/// BinaryMessenger will be used which routes to the host platform.
|
|
||||||
MediaSaveApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
|
||||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
|
||||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
|
||||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
|
||||||
|
|
||||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
|
||||||
|
|
||||||
final String pigeonVar_messageChannelSuffix;
|
|
||||||
|
|
||||||
Future<String?> saveToDownloads(String filePath, String title, String? relativePath) async {
|
|
||||||
final pigeonVar_channelName =
|
|
||||||
'dev.flutter.pigeon.immich_mobile.MediaSaveApi.saveToDownloads$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
|
||||||
pigeonChannelCodec,
|
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
|
||||||
);
|
|
||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[filePath, title, relativePath]);
|
|
||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
|
||||||
pigeonVar_replyList,
|
|
||||||
pigeonVar_channelName,
|
|
||||||
isNullValid: true,
|
|
||||||
);
|
|
||||||
return pigeonVar_replyValue as String?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
|
||||||
import 'package:immich_mobile/platform/media_save_api.g.dart';
|
|
||||||
import 'package:immich_mobile/utils/mime.utils.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||||
|
|
||||||
final _mediaSaveApi = MediaSaveApi();
|
|
||||||
|
|
||||||
final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository());
|
final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository());
|
||||||
|
|
||||||
class FileMediaRepository {
|
class FileMediaRepository {
|
||||||
@@ -31,19 +25,8 @@ class FileMediaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<AssetEntity?> saveImageWithFile(String filePath, {String? title, String? relativePath}) async {
|
Future<AssetEntity?> saveImageWithFile(String filePath, {String? title, String? relativePath}) async {
|
||||||
try {
|
final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath);
|
||||||
return await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath);
|
return entity;
|
||||||
} on PlatformException catch (e) {
|
|
||||||
// Some formats (e.g. raw like CR3) have no MIME the platform recognises, so
|
|
||||||
// photo_manager falls back to `image/*` and MediaStore rejects the save.
|
|
||||||
// Save those to Downloads ourselves, where the Files collection takes any
|
|
||||||
// type. Anything else isn't ours to handle.
|
|
||||||
if (!CurrentPlatform.isAndroid || !isUnsupportedMimeError(e)) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
final id = await _mediaSaveApi.saveToDownloads(filePath, title ?? p.basename(filePath), 'Download/Immich');
|
|
||||||
return id == null ? null : AssetEntity(id: id, typeInt: 1, width: 0, height: 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AssetEntity?> saveLivePhoto({required File image, required File video, required String title}) async {
|
Future<AssetEntity?> saveLivePhoto({required File image, required File video, required String title}) async {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
// True when a gallery save failed because MediaStore rejected the file's MIME.
|
|
||||||
// Android raises `Unsupported MIME type` for formats it can't type (e.g. raw
|
|
||||||
// like CR3), where photo_manager falls back to `image/*`. Matched on the detail
|
|
||||||
// string (case-insensitive) because photo_manager surfaces no distinct error
|
|
||||||
// code for it. Keeps `mime type` in the match so it doesn't catch unrelated
|
|
||||||
// `Unsupported*` errors (e.g. UnsupportedOperationException).
|
|
||||||
bool isUnsupportedMimeError(PlatformException e) {
|
|
||||||
final details = e.details;
|
|
||||||
return details is String && details.toLowerCase().contains('unsupported mime type');
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import 'package:pigeon/pigeon.dart';
|
|
||||||
|
|
||||||
@ConfigurePigeon(
|
|
||||||
PigeonOptions(
|
|
||||||
dartOut: 'lib/platform/media_save_api.g.dart',
|
|
||||||
kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/mediasave/MediaSave.g.kt',
|
|
||||||
kotlinOptions: KotlinOptions(package: 'app.alextran.immich.mediasave'),
|
|
||||||
dartOptions: DartOptions(),
|
|
||||||
dartPackageName: 'immich_mobile',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
@HostApi()
|
|
||||||
abstract class MediaSaveApi {
|
|
||||||
// Saves a file to a MediaStore Files-collection entry under [relativePath]
|
|
||||||
// (e.g. Download/Immich). Fallback for when photo_manager can't save a file
|
|
||||||
// because the platform has no MIME for it (e.g. raw like CR3) and MediaStore
|
|
||||||
// rejects the `image/*` it falls back to; the Files collection accepts any
|
|
||||||
// type. Returns the new media id, or null on failure.
|
|
||||||
@async
|
|
||||||
String? saveToDownloads(String filePath, String title, String? relativePath);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:immich_mobile/utils/mime.utils.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('isUnsupportedMimeError', () {
|
|
||||||
PlatformException err(Object? details) => PlatformException(code: 'saveImageWithPath', details: details);
|
|
||||||
|
|
||||||
test('detects the MediaStore unsupported-mime failure', () {
|
|
||||||
expect(isUnsupportedMimeError(err('java.lang.IllegalArgumentException: Unsupported MIME type image/*')), isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('is case-insensitive', () {
|
|
||||||
expect(isUnsupportedMimeError(err('unsupported mime type foo')), isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ignores other Unsupported* errors', () {
|
|
||||||
expect(isUnsupportedMimeError(err('java.lang.UnsupportedOperationException')), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ignores unrelated failures and non-string details', () {
|
|
||||||
expect(isUnsupportedMimeError(err('Permission denied')), isFalse);
|
|
||||||
expect(isUnsupportedMimeError(err(null)), isFalse);
|
|
||||||
expect(isUnsupportedMimeError(err(42)), isFalse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
const { trigger, selectedKey, onClose }: Props = $props();
|
const { trigger, selectedKey, onClose }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BasicModal title={$t('add_step')} {onClose} size="medium">
|
<BasicModal title={$t('add_step')} onClose={() => onClose()} size="medium">
|
||||||
{#await searchPluginMethods({ trigger })}
|
{#await searchPluginMethods({ trigger })}
|
||||||
<div class="flex w-full place-content-center place-items-center">
|
<div class="flex w-full place-content-center place-items-center">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
|||||||
Reference in New Issue
Block a user