diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c2083ad9e..1e01e7208 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -108,5 +108,9 @@ + + + + diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt index 5c630841e..98964c4eb 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt @@ -1,6 +1,7 @@ package nl.jknaapen.fladder import BatteryOptimizationPigeon +import FlutterError import NativeVideoActivity import PlayerSettingsPigeon import StartResult @@ -17,6 +18,7 @@ import android.provider.Settings import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.ui.platform.LocalContext +import androidx.core.content.FileProvider import com.ryanheise.audioservice.AudioServiceFragmentActivity import io.flutter.embedding.engine.FlutterEngine import nl.jknaapen.fladder.objects.PlayerSettingsObject @@ -24,6 +26,12 @@ import nl.jknaapen.fladder.objects.TranslationsMessenger import nl.jknaapen.fladder.objects.VideoPlayerObject import nl.jknaapen.fladder.utility.leanBackEnabled import androidx.core.net.toUri +import nl.jknaapen.fladder.wallpaper.WallpaperApi +import nl.jknaapen.fladder.wallpaper.WallpaperApiUtility +import java.io.File +import java.util.Objects + +class WallpaperFileProvider : FileProvider() class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity { private lateinit var videoPlayerLauncher: ActivityResultLauncher @@ -37,6 +45,10 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity { flutterEngine.dartExecutor.binaryMessenger, this ) + WallpaperApi.setUp( + flutterEngine.dartExecutor.binaryMessenger, + WallpaperApiUtility(this, wallpaperLauncher) + ) VideoPlayerApi.setUp( flutterEngine.dartExecutor.binaryMessenger, videoPlayerHost.implementation @@ -93,6 +105,12 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity { setIntent(intent) } + private val wallpaperLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + // Handle the result of the wallpaper intent if needed + } + override fun launchActivity(callback: (Result) -> Unit) { try { videoPlayerCallback = callback diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/wallpaper/WallpaperApi.g.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/wallpaper/WallpaperApi.g.kt new file mode 100644 index 000000000..5118e94db --- /dev/null +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/wallpaper/WallpaperApi.g.kt @@ -0,0 +1,97 @@ +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package nl.jknaapen.fladder.wallpaper + +import FlutterError +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 WallpaperApiPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + 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) + ) + } + } +} + +private open class WallpaperApiPigeonCodec : 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 WallpaperApi { + fun openWallpaperPopup(filePath: String, callback: (Result) -> Unit) + + companion object { + /** The codec used by WallpaperApi. */ + val codec: MessageCodec by lazy { + WallpaperApiPigeonCodec() + } + + /** Sets up an instance of `WallpaperApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp( + binaryMessenger: BinaryMessenger, + api: WallpaperApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.nl_jknaapen_fladder.wallpaper.WallpaperApi.openWallpaperPopup$separatedMessageChannelSuffix", + codec + ) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val filePathArg = args[0] as String + api.openWallpaperPopup(filePathArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(WallpaperApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(WallpaperApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/wallpaper/WallpaperApiUtility.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/wallpaper/WallpaperApiUtility.kt new file mode 100644 index 000000000..fffcfe95c --- /dev/null +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/wallpaper/WallpaperApiUtility.kt @@ -0,0 +1,45 @@ +package nl.jknaapen.fladder.wallpaper + +import FlutterError +import android.content.Intent +import androidx.core.content.FileProvider +import java.io.File + +class WallpaperApiUtility( + val applicationContext: android.content.Context, + private val launcher: androidx.activity.result.ActivityResultLauncher +) : WallpaperApi { + private var pendingCallback: ((Result) -> Unit)? = null + + override fun openWallpaperPopup(filePath: String, callback: (Result) -> Unit) { + this.pendingCallback = callback + + try { + val file = File(filePath) + + val uri = + FileProvider.getUriForFile( + applicationContext, + "${applicationContext.packageName}.wallpaper_provider", + file + ) + + val intent = Intent(Intent.ACTION_ATTACH_DATA).apply { + addCategory(Intent.CATEGORY_DEFAULT) + setDataAndType(uri, "image/*") + putExtra("mimeType", "image/*") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + launcher.launch(android.content.Intent.createChooser(intent, "Set wallpaper as:")) + } catch (e: Exception) { + // Pigeon handles exceptions gracefully and passes them back to Dart + throw FlutterError("INTENT_ERROR", "Failed to launch intent: ${e.message}", null) + } + } + + fun onResult(success: Boolean) { + pendingCallback?.invoke(Result.success(success)) + pendingCallback = null + } +} \ No newline at end of file diff --git a/android/app/src/main/res/xml/wallpaper_file_paths.xml b/android/app/src/main/res/xml/wallpaper_file_paths.xml new file mode 100644 index 000000000..9ab682c64 --- /dev/null +++ b/android/app/src/main/res/xml/wallpaper_file_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d2fa5c6d3..db330a143 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2764,5 +2764,6 @@ "musicDashboard": "Music Dashboard", "quiet": "Quiet", "loud": "Loud", - "share": "Share" + "share": "Share", + "setAs": "Set as" } \ No newline at end of file diff --git a/lib/screens/photo_viewer/photo_viewer_screen.dart b/lib/screens/photo_viewer/photo_viewer_screen.dart index 876737d0a..8f8c24d07 100644 --- a/lib/screens/photo_viewer/photo_viewer_screen.dart +++ b/lib/screens/photo_viewer/photo_viewer_screen.dart @@ -9,7 +9,6 @@ import 'package:extended_image/extended_image.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:share_plus/share_plus.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; @@ -484,11 +483,6 @@ class _PhotoViewerScreenState extends ConsumerState with Widg ? IconsaxPlusLinear.filter_remove : IconsaxPlusLinear.filter, ), - ElevatedIconButtonLabel( - label: context.localized.share, - onPressed: () => sharePhoto(currentPhoto), - icon: IconsaxPlusLinear.share, - ), ].addInBetween(const SizedBox(width: 16)), ); }), @@ -512,16 +506,6 @@ class _PhotoViewerScreenState extends ConsumerState with Widg }, ); - Future sharePhoto(PhotoModel photo) async { - final file = await CustomCacheManager.instance.getSingleFile(photo.downloadPath(ref)); - await SharePlus.instance.share(ShareParams(files: [ - XFile( - file.path, - ), - ])); - await file.delete(); - } - Future markAsFavourite(PhotoModel photo, {bool? value}) async { await ref.read(userProvider.notifier).setAsFavorite(value ?? !photo.userData.isFavourite, photo.id); diff --git a/lib/src/wallpaper_api.g.dart b/lib/src/wallpaper_api.g.dart new file mode 100644 index 000000000..cad40ae76 --- /dev/null +++ b/lib/src/wallpaper_api.g.dart @@ -0,0 +1,80 @@ +// Autogenerated from Pigeon (v26.1.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'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + + +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 WallpaperApi { + /// Constructor for [WallpaperApi]. 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. + WallpaperApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future openWallpaperPopup(String filePath) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.nl_jknaapen_fladder.wallpaper.WallpaperApi.openWallpaperPopup$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([filePath]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } +} diff --git a/lib/util/item_base_model/item_base_model_extensions.dart b/lib/util/item_base_model/item_base_model_extensions.dart index c1ac26007..ba15a09ef 100644 --- a/lib/util/item_base_model/item_base_model_extensions.dart +++ b/lib/util/item_base_model/item_base_model_extensions.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -13,6 +14,7 @@ import 'package:fladder/models/items/audio_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/movie_model.dart'; +import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/user_provider.dart'; @@ -27,7 +29,9 @@ import 'package:fladder/screens/shared/fladder_notification_overlay.dart'; import 'package:fladder/screens/syncing/sync_button.dart'; import 'package:fladder/screens/syncing/sync_item_details.dart'; import 'package:fladder/seerr/seerr_models.dart'; +import 'package:fladder/src/wallpaper_api.g.dart'; import 'package:fladder/util/clipboard_helper.dart'; +import 'package:fladder/util/custom_cache_manager.dart'; import 'package:fladder/util/file_downloader.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -101,6 +105,8 @@ enum ItemActions { mediaInfo, identify, download, + setAsWallpaper, + share, } extension ItemBaseModelExtensions on ItemBaseModel { @@ -215,6 +221,20 @@ extension ItemBaseModelExtensions on ItemBaseModel { action: () => parentBaseModel.navigateTo(context), label: Text(context.localized.showAlbum), ), + if (this case PhotoModel photo) ...[ + if (!kIsWeb && !exclude.contains(ItemActions.setAsWallpaper) && defaultTargetPlatform == TargetPlatform.android) + ItemActionButton( + action: () => setAsWallpaper(photo, ref), + icon: const Icon(IconsaxPlusLinear.document_upload), + label: Text(context.localized.setAs), + ), + if (!exclude.contains(ItemActions.share)) + ItemActionButton( + action: () => sharePhoto(photo, ref), + icon: const Icon(IconsaxPlusLinear.share), + label: Text(context.localized.share), + ), + ], if (!exclude.contains(ItemActions.playFromStart)) if ((userData.progress) > 0) ItemActionButton( @@ -416,6 +436,22 @@ extension ItemBaseModelExtensions on ItemBaseModel { ]; } + Future setAsWallpaper(PhotoModel photo, WidgetRef ref) async { + final file = await CustomCacheManager.instance.getSingleFile(photo.downloadPath(ref)); + await WallpaperApi().openWallpaperPopup(file.path); + await file.delete(); + } + + Future sharePhoto(PhotoModel photo, WidgetRef ref) async { + final file = await CustomCacheManager.instance.getSingleFile(photo.downloadPath(ref)); + await SharePlus.instance.share(ShareParams(files: [ + XFile( + file.path, + ), + ])); + await file.delete(); + } + int? get tmdbId { final providerIds = this is MovieModel ? (this as MovieModel).providerIds diff --git a/pigeons/wallpaper_api.dart b/pigeons/wallpaper_api.dart new file mode 100644 index 000000000..671b28639 --- /dev/null +++ b/pigeons/wallpaper_api.dart @@ -0,0 +1,18 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/wallpaper_api.g.dart', + kotlinOut: 'android/app/src/main/kotlin/nl/jknaapen/fladder/wallpaper/WallpaperApi.g.kt', + kotlinOptions: KotlinOptions( + package: 'nl.jknaapen.fladder.wallpaper', + includeErrorClass: false, + ), + dartPackageName: 'nl_jknaapen_fladder.wallpaper', + ), +) +@HostApi() +abstract class WallpaperApi { + @async + bool openWallpaperPopup(String filePath); +}