mirror of https://github.com/immich-app/immich.git
Merge 162682ef8b into 6d499c782a
commit
0c005cef69
@ -0,0 +1,208 @@
|
||||
package app.alextran.immich.picker
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import app.alextran.immich.MainActivity
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.FlutterEngineCache
|
||||
import java.io.File
|
||||
|
||||
|
||||
/**
|
||||
* Activity that handles ACTION_GET_CONTENT and ACTION_PICK intents
|
||||
* Communicates with Flutter to get the selected image URI
|
||||
*/
|
||||
class ImagePickerActivity : FlutterActivity() {
|
||||
private var imagePickerApi: ImagePickerProviderApi? = null
|
||||
private var hasRequestedImage = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Log.d(TAG, "onCreate() called")
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val action = intent.action
|
||||
val type = intent.type
|
||||
|
||||
Log.d(TAG, "ImagePickerActivity started with action: $action, type: $type")
|
||||
|
||||
if ((action == Intent.ACTION_GET_CONTENT || action == Intent.ACTION_PICK)) {
|
||||
Log.d(TAG, "Valid intent detected")
|
||||
} else {
|
||||
// Invalid intent, finish immediately
|
||||
Log.w(TAG, "Invalid intent action or type, finishing activity")
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
Log.d(TAG, "onCreate() finished")
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
Log.d(TAG, "configureFlutterEngine() called, hasRequestedImage = $hasRequestedImage")
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
// Register all plugins
|
||||
Log.d(TAG, "Registering plugins...")
|
||||
MainActivity.registerPlugins(this, flutterEngine)
|
||||
Log.d(TAG, "Plugins registered")
|
||||
|
||||
// Set up the image picker API
|
||||
Log.d(TAG, "Setting up ImagePickerProviderApi...")
|
||||
imagePickerApi = ImagePickerProviderApi(flutterEngine.dartExecutor.binaryMessenger)
|
||||
Log.d(TAG, "ImagePickerProviderApi set up: ${true}")
|
||||
|
||||
// Check if this is a valid image picker intent and we haven't requested yet
|
||||
val action = intent.action
|
||||
if (!hasRequestedImage && (action == Intent.ACTION_GET_CONTENT || action == Intent.ACTION_PICK)) {
|
||||
Log.d(TAG, "Valid intent and haven't requested yet, calling requestImageFromFlutter()")
|
||||
hasRequestedImage = true
|
||||
requestImageFromFlutter()
|
||||
} else {
|
||||
Log.w(
|
||||
TAG,
|
||||
"NOT calling requestImageFromFlutter() - hasRequestedImage: $hasRequestedImage, action: $action"
|
||||
)
|
||||
}
|
||||
Log.d(TAG, "configureFlutterEngine() finished")
|
||||
}
|
||||
|
||||
private fun requestImageFromFlutter() {
|
||||
Log.d(TAG, "=== requestImageFromFlutter() CALLED ===")
|
||||
Log.d(TAG, "imagePickerApi is null: ${imagePickerApi == null}")
|
||||
|
||||
// Check if the calling app allows multiple selection
|
||||
val allowMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
||||
Log.d(TAG, "Intent allows multiple selection: $allowMultiple")
|
||||
|
||||
imagePickerApi?.pickImagesForIntent { result ->
|
||||
Log.d(TAG, "pickImagesForIntent callback received")
|
||||
result.fold(onSuccess = { imageUriList ->
|
||||
Log.d(TAG, "SUCCESS: Received ${imageUriList?.size ?: 0} image URI(s) from Flutter")
|
||||
|
||||
if (imageUriList.isNullOrEmpty()) {
|
||||
// User cancelled or no images selected
|
||||
Log.d(TAG, "No images selected, returning RESULT_CANCELED")
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
return@fold
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert all URIs to content URIs
|
||||
val contentUris = imageUriList.filterNotNull().map { uriString ->
|
||||
try {
|
||||
convertToContentUri(uriString)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error converting URI: $uriString", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (contentUris.isEmpty()) {
|
||||
Log.e(TAG, "No valid content URIs after conversion")
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
return@fold
|
||||
}
|
||||
|
||||
val resultIntent = Intent()
|
||||
|
||||
if (contentUris.size == 1 || !allowMultiple) {
|
||||
// Single image or app doesn't support multiple
|
||||
Log.d(TAG, "Returning single image URI: ${contentUris.first()}")
|
||||
resultIntent.data = contentUris.first()
|
||||
} else {
|
||||
// Multiple images - use ClipData
|
||||
Log.d(TAG, "Returning ${contentUris.size} images using ClipData")
|
||||
val clipData = ClipData.newUri(contentResolver, "Images", contentUris.first())
|
||||
|
||||
// Add the rest of the URIs to ClipData
|
||||
for (i in 1 until contentUris.size) {
|
||||
clipData.addItem(ClipData.Item(contentUris[i]))
|
||||
}
|
||||
|
||||
resultIntent.clipData = clipData
|
||||
resultIntent.data = contentUris.first() // Also set primary URI for compatibility
|
||||
}
|
||||
|
||||
// Grant temporary read permission to all URIs
|
||||
resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing URIs", e)
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}, onFailure = { error ->
|
||||
Log.e(TAG, "Error getting images from Flutter", error)
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a file:// URI to a content:// URI using FileProvider
|
||||
* This is required for API 24+ to share files with other apps
|
||||
*/
|
||||
private fun convertToContentUri(uriString: String): Uri {
|
||||
val uri = uriString.toUri()
|
||||
|
||||
return if (uri.scheme == "file") {
|
||||
val file = File(uri.path!!)
|
||||
FileProvider.getUriForFile(
|
||||
this, "${applicationContext.packageName}.fileprovider", file
|
||||
)
|
||||
} else {
|
||||
// Already a content URI or other type
|
||||
uri
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCachedEngineId(): String? {
|
||||
// Try to use the cached engine if available
|
||||
val hasCachedEngine = FlutterEngineCache.getInstance().contains(ENGINE_CACHE_KEY)
|
||||
Log.d(TAG, "getCachedEngineId() called, has cached engine: $hasCachedEngine")
|
||||
return if (hasCachedEngine) {
|
||||
Log.d(TAG, "Using cached engine 'immich_engine'")
|
||||
"immich_engine"
|
||||
} else {
|
||||
Log.d(TAG, "No cached engine found, will create new engine")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Log.d(TAG, "onStart() called")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.d(TAG, "onResume() called")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.d(TAG, "onPause() called")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "onDestroy() called")
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ImagePickerActivity"
|
||||
const val ENGINE_CACHE_KEY = "immich::image_picker::engine"
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
package app.alextran.immich.picker
|
||||
|
||||
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 ImagePickerProviderPigeonUtils {
|
||||
|
||||
fun createConnectionError(channelName: String): FlutterError {
|
||||
return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
) : Throwable()
|
||||
private open class ImagePickerProviderPigeonCodec : 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API for Android native to request an image from Flutter
|
||||
*
|
||||
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
|
||||
*/
|
||||
class ImagePickerProviderApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||
companion object {
|
||||
/** The codec used by ImagePickerProviderApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
ImagePickerProviderPigeonCodec()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called when Android needs images for ACTION_GET_CONTENT/ACTION_PICK
|
||||
* Returns a list of URIs of the selected images (content:// or file:// URIs)
|
||||
* Returns null or empty list if user cancels
|
||||
*/
|
||||
fun pickImagesForIntent(callback: (Result<List<String?>?>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.immich_mobile.ImagePickerProviderApi.pickImagesForIntent$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(null) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
val output = it[0] as List<String?>?
|
||||
callback(Result.success(output))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(ImagePickerProviderPigeonUtils.createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Temporary directory for sharing images -->
|
||||
<cache-path name="shared_images" path="." />
|
||||
<!-- External cache directory -->
|
||||
<external-cache-path name="external_cache" path="." />
|
||||
</paths>
|
||||
@ -0,0 +1,89 @@
|
||||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
import Flutter
|
||||
#elseif os(macOS)
|
||||
import FlutterMacOS
|
||||
#else
|
||||
#error("Unsupported platform.")
|
||||
#endif
|
||||
|
||||
private func createConnectionError(withChannelName channelName: String) -> PigeonError {
|
||||
return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "")
|
||||
}
|
||||
|
||||
private func isNullish(_ value: Any?) -> Bool {
|
||||
return value is NSNull || value == nil
|
||||
}
|
||||
|
||||
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||
if value is NSNull { return nil }
|
||||
return value as! T?
|
||||
}
|
||||
|
||||
|
||||
private class ImagePickerProviderPigeonCodecReader: FlutterStandardReader {
|
||||
}
|
||||
|
||||
private class ImagePickerProviderPigeonCodecWriter: FlutterStandardWriter {
|
||||
}
|
||||
|
||||
private class ImagePickerProviderPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||
override func reader(with data: Data) -> FlutterStandardReader {
|
||||
return ImagePickerProviderPigeonCodecReader(data: data)
|
||||
}
|
||||
|
||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||
return ImagePickerProviderPigeonCodecWriter(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
class ImagePickerProviderPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||
static let shared = ImagePickerProviderPigeonCodec(readerWriter: ImagePickerProviderPigeonCodecReaderWriter())
|
||||
}
|
||||
|
||||
/// API for Android native to request an image from Flutter
|
||||
///
|
||||
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
||||
protocol ImagePickerProviderApiProtocol {
|
||||
/// Called when Android needs images for ACTION_GET_CONTENT/ACTION_PICK
|
||||
/// Returns a list of URIs of the selected images (content:// or file:// URIs)
|
||||
/// Returns null or empty list if user cancels
|
||||
func pickImagesForIntent(completion: @escaping (Result<[String?]?, PigeonError>) -> Void)
|
||||
}
|
||||
class ImagePickerProviderApi: ImagePickerProviderApiProtocol {
|
||||
private let binaryMessenger: FlutterBinaryMessenger
|
||||
private let messageChannelSuffix: String
|
||||
init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") {
|
||||
self.binaryMessenger = binaryMessenger
|
||||
self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||
}
|
||||
var codec: ImagePickerProviderPigeonCodec {
|
||||
return ImagePickerProviderPigeonCodec.shared
|
||||
}
|
||||
/// Called when Android needs images for ACTION_GET_CONTENT/ACTION_PICK
|
||||
/// Returns a list of URIs of the selected images (content:// or file:// URIs)
|
||||
/// Returns null or empty list if user cancels
|
||||
func pickImagesForIntent(completion: @escaping (Result<[String?]?, PigeonError>) -> Void) {
|
||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.ImagePickerProviderApi.pickImagesForIntent\(messageChannelSuffix)"
|
||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||
channel.sendMessage(nil) { response in
|
||||
guard let listResponse = response as? [Any?] else {
|
||||
completion(.failure(createConnectionError(withChannelName: channelName)))
|
||||
return
|
||||
}
|
||||
if listResponse.count > 1 {
|
||||
let code: String = listResponse[0] as! String
|
||||
let message: String? = nilOrValue(listResponse[1])
|
||||
let details: String? = nilOrValue(listResponse[2])
|
||||
completion(.failure(PigeonError(code: code, message: message, details: details)))
|
||||
} else {
|
||||
let result: [String?]? = nilOrValue(listResponse[0])
|
||||
completion(.success(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
|
||||
if (empty) {
|
||||
return <Object?>[];
|
||||
}
|
||||
if (error == null) {
|
||||
return <Object?>[result];
|
||||
}
|
||||
return <Object?>[error.code, error.message, error.details];
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// API for Android native to request an image from Flutter
|
||||
abstract class ImagePickerProviderApi {
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
/// Called when Android needs images for ACTION_GET_CONTENT/ACTION_PICK
|
||||
/// Returns a list of URIs of the selected images (content:// or file:// URIs)
|
||||
/// Returns null or empty list if user cancels
|
||||
Future<List<String?>?> pickImagesForIntent();
|
||||
|
||||
static void setUp(ImagePickerProviderApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.ImagePickerProviderApi.pickImagesForIntent$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
try {
|
||||
final List<String?>? output = await api.pickImagesForIntent();
|
||||
return wrapResponse(result: output);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/platform/image_picker_provider_api.g.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
final imagePickerProviderServiceProvider = Provider(
|
||||
(ref) => ImagePickerProviderService(
|
||||
const StorageRepository(),
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(appRouterProvider),
|
||||
),
|
||||
);
|
||||
|
||||
/// Service that handles image picker requests from Android native code
|
||||
/// When other apps (like Twitter) request an image via ACTION_GET_CONTENT,
|
||||
/// this service provides the image URI
|
||||
class ImagePickerProviderService implements ImagePickerProviderApi {
|
||||
final StorageRepository _storageRepository;
|
||||
final ApiService _apiService;
|
||||
final AppRouter _appRouter;
|
||||
|
||||
ImagePickerProviderService(
|
||||
StorageRepository storageRepository,
|
||||
ApiService apiService,
|
||||
AppRouter appRouter,
|
||||
) : _storageRepository = storageRepository,
|
||||
_apiService = apiService,
|
||||
_appRouter = appRouter {
|
||||
// Register this service with the platform channel
|
||||
ImagePickerProviderApi.setUp(this);
|
||||
dPrint(() => "ImagePickerProviderService registered");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String?>?> pickImagesForIntent() async {
|
||||
dPrint(() => "pickImagesForIntent called from native");
|
||||
|
||||
try {
|
||||
// Show the asset selection timeline page to let the user choose images
|
||||
final selectedAssets = await _appRouter.push<Set<BaseAsset>>(
|
||||
DriftAssetSelectionTimelineRoute(),
|
||||
);
|
||||
|
||||
if (selectedAssets == null || selectedAssets.isEmpty) {
|
||||
dPrint(() => "No assets selected by user");
|
||||
return null;
|
||||
}
|
||||
|
||||
dPrint(() => "User selected ${selectedAssets.length} asset(s)");
|
||||
|
||||
// Process all selected assets
|
||||
final List<String> imageUris = [];
|
||||
|
||||
for (final asset in selectedAssets) {
|
||||
dPrint(() => "Processing asset: ${asset.runtimeType}");
|
||||
|
||||
String? uri = await _getAssetUri(asset);
|
||||
if (uri != null) {
|
||||
imageUris.add(uri);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUris.isEmpty) {
|
||||
dPrint(() => "No valid URIs obtained, returning null");
|
||||
return null;
|
||||
}
|
||||
|
||||
dPrint(() => "Returning ${imageUris.length} image URI(s)");
|
||||
return imageUris;
|
||||
} catch (e, stackTrace) {
|
||||
dPrint(() => "Error in pickImagesForIntent: $e\n$stackTrace");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the URI for a single asset (local, merged, or remote)
|
||||
Future<String?> _getAssetUri(BaseAsset asset) async {
|
||||
try {
|
||||
// Try to get the file from a local asset
|
||||
if (asset is LocalAsset) {
|
||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||
if (file != null) {
|
||||
dPrint(() => "Got local asset URI: file://${file.path}");
|
||||
return 'file://${file.path}';
|
||||
}
|
||||
} else if (asset is RemoteAsset) {
|
||||
final remoteAsset = asset;
|
||||
|
||||
// Check if remote asset also exists locally
|
||||
if (remoteAsset.localId != null) {
|
||||
final file = await _storageRepository.getFileForAsset(remoteAsset.localId!);
|
||||
if (file != null) {
|
||||
dPrint(() => "Got merged asset local URI: file://${file.path}");
|
||||
return 'file://${file.path}';
|
||||
}
|
||||
}
|
||||
|
||||
// Remote-only asset - download it
|
||||
dPrint(() => "Downloading remote asset ${remoteAsset.id}");
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final fileName = remoteAsset.name;
|
||||
final tempFile = await File('${tempDir.path}/$fileName').create();
|
||||
|
||||
try {
|
||||
final res = await _apiService.assetsApi.downloadAssetWithHttpInfo(remoteAsset.id);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
dPrint(() => "Asset download failed with status ${res.statusCode}");
|
||||
return null;
|
||||
}
|
||||
|
||||
await tempFile.writeAsBytes(res.bodyBytes);
|
||||
dPrint(() => "Downloaded remote asset to: file://${tempFile.path}");
|
||||
return 'file://${tempFile.path}';
|
||||
} catch (e) {
|
||||
dPrint(() => "Error downloading remote asset: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
dPrint(() => "No file available for asset");
|
||||
return null;
|
||||
} catch (e) {
|
||||
dPrint(() => "Error getting asset URI: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import 'package:pigeon/pigeon.dart';
|
||||
|
||||
@ConfigurePigeon(
|
||||
PigeonOptions(
|
||||
dartOut: 'lib/platform/image_picker_provider_api.g.dart',
|
||||
swiftOut: 'ios/Runner/ImagePickerProvider.g.swift',
|
||||
swiftOptions: SwiftOptions(includeErrorClass: false),
|
||||
kotlinOut:
|
||||
'android/app/src/main/kotlin/app/alextran/immich/picker/ImagePickerProvider.g.kt',
|
||||
kotlinOptions: KotlinOptions(package: 'app.alextran.immich.picker'),
|
||||
dartOptions: DartOptions(),
|
||||
dartPackageName: 'immich_mobile',
|
||||
),
|
||||
)
|
||||
|
||||
/// API for Android native to request an image from Flutter
|
||||
@FlutterApi()
|
||||
abstract class ImagePickerProviderApi {
|
||||
/// Called when Android needs images for ACTION_GET_CONTENT/ACTION_PICK
|
||||
/// Returns a list of URIs of the selected images (content:// or file:// URIs)
|
||||
/// Returns null or empty list if user cancels
|
||||
@async
|
||||
List<String?>? pickImagesForIntent();
|
||||
}
|
||||
Loading…
Reference in New Issue