gallery: library-change Stream + native observer parity
Add UxGallery.libraryChanges — a Stream<void> that emits whenever the underlying photo library reports a change, so picker UIs can drop their cached asset lists and reload reactively. - iOS: GalleryPlugin conforms to PHPhotoLibraryChangeObserver + FlutterStreamHandler; on photoLibraryDidChange the fetchCache is cleared and a void event is pushed over ux/gallery/changes. The observer is registered lazily on the first granted/limited authorization so plugin init doesn't trigger iOS's permission evaluation at app launch. - macOS: near-verbatim port of the iOS shape (same Photos.framework, same fetchCache staleness, same fix). - Android: registers a MediaStore ContentObserver on Files.getContentUri(VOLUME_EXTERNAL) with notifyForDescendants = true; observer lifecycle tracks the Dart subscription (registered on onListen, unregistered on onCancel / plugin detach). No native cache to invalidate today, but the pipeline is wired for the upcoming READ_MEDIA_VISUAL_USER_SELECTED limited-access work. - iOS presentLimitedLibraryPicker switched to the iOS 15+ completion- handler variant so the Dart await resolves on dismissal, not on presentation. The actual reload is now driven by the change observer (which fires after iOS commits the new subset), side-stepping the completion-vs-commit race that produced an off-by-one in the picker on consecutive MANAGE taps. - FakeUxGalleryBackend exposes emitLibraryChange() so tests can drive the reactive-reload wiring without going through the real method channel.
This commit is contained in:
@@ -6,15 +6,19 @@ import android.content.ContentUris
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.database.ContentObserver
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.plugin.common.PluginRegistry
|
import io.flutter.plugin.common.PluginRegistry
|
||||||
@@ -28,8 +32,9 @@ import java.io.FileOutputStream
|
|||||||
/// resolution into the app cache so `dart:io` can read what the
|
/// resolution into the app cache so `dart:io` can read what the
|
||||||
/// system holds behind a `content://` URI.
|
/// system holds behind a `content://` URI.
|
||||||
class GalleryPlugin : NativePlugin, MethodChannel.MethodCallHandler,
|
class GalleryPlugin : NativePlugin, MethodChannel.MethodCallHandler,
|
||||||
PluginRegistry.RequestPermissionsResultListener {
|
PluginRegistry.RequestPermissionsResultListener, EventChannel.StreamHandler {
|
||||||
private var methodChannel: MethodChannel? = null
|
private var methodChannel: MethodChannel? = null
|
||||||
|
private var changesChannel: EventChannel? = null
|
||||||
private var context: Context? = null
|
private var context: Context? = null
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var activityBinding: ActivityPluginBinding? = null
|
private var activityBinding: ActivityPluginBinding? = null
|
||||||
@@ -39,19 +44,64 @@ class GalleryPlugin : NativePlugin, MethodChannel.MethodCallHandler,
|
|||||||
/// is rejected.
|
/// is rejected.
|
||||||
private var pendingPermissionResult: MethodChannel.Result? = null
|
private var pendingPermissionResult: MethodChannel.Result? = null
|
||||||
|
|
||||||
|
/// Dart subscriber on `ux/gallery/changes` — fed by [mediaObserver]
|
||||||
|
/// when `MediaStore` reports an insert/update/delete so picker UIs
|
||||||
|
/// can reload reactively (parity with iOS's `photoLibraryDidChange`).
|
||||||
|
private var libraryEventSink: EventChannel.EventSink? = null
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val mediaObserver = object : ContentObserver(mainHandler) {
|
||||||
|
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||||
|
libraryEventSink?.success(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var mediaObserverRegistered = false
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
context = binding.applicationContext
|
context = binding.applicationContext
|
||||||
methodChannel = MethodChannel(binding.binaryMessenger, "ux/gallery").also {
|
methodChannel = MethodChannel(binding.binaryMessenger, "ux/gallery").also {
|
||||||
it.setMethodCallHandler(this)
|
it.setMethodCallHandler(this)
|
||||||
}
|
}
|
||||||
|
changesChannel = EventChannel(
|
||||||
|
binding.binaryMessenger, "ux/gallery/changes",
|
||||||
|
).also { it.setStreamHandler(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
methodChannel?.setMethodCallHandler(null)
|
methodChannel?.setMethodCallHandler(null)
|
||||||
methodChannel = null
|
methodChannel = null
|
||||||
|
changesChannel?.setStreamHandler(null)
|
||||||
|
changesChannel = null
|
||||||
|
unregisterMediaObserver()
|
||||||
context = null
|
context = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||||
|
libraryEventSink = events
|
||||||
|
registerMediaObserver()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(arguments: Any?) {
|
||||||
|
libraryEventSink = null
|
||||||
|
unregisterMediaObserver()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerMediaObserver() {
|
||||||
|
if (mediaObserverRegistered) return
|
||||||
|
val ctx = context ?: return
|
||||||
|
ctx.contentResolver.registerContentObserver(
|
||||||
|
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
|
||||||
|
/* notifyForDescendants */ true,
|
||||||
|
mediaObserver,
|
||||||
|
)
|
||||||
|
mediaObserverRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterMediaObserver() {
|
||||||
|
if (!mediaObserverRegistered) return
|
||||||
|
context?.contentResolver?.unregisterContentObserver(mediaObserver)
|
||||||
|
mediaObserverRegistered = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
activity = binding.activity
|
activity = binding.activity
|
||||||
activityBinding = binding
|
activityBinding = binding
|
||||||
|
|||||||
@@ -6,9 +6,17 @@ import UIKit
|
|||||||
/// `Photos.framework` bridge for `UxGallery` — paginated asset queries,
|
/// `Photos.framework` bridge for `UxGallery` — paginated asset queries,
|
||||||
/// cell-sized thumbnails via `PHCachingImageManager`, and on-demand
|
/// cell-sized thumbnails via `PHCachingImageManager`, and on-demand
|
||||||
/// file resolution into the app cache.
|
/// file resolution into the app cache.
|
||||||
public class GalleryPlugin: NSObject, NativePlugin {
|
public class GalleryPlugin: NSObject, NativePlugin, PHPhotoLibraryChangeObserver, FlutterStreamHandler {
|
||||||
private let imageManager = PHCachingImageManager()
|
private let imageManager = PHCachingImageManager()
|
||||||
private var fetchCache: [String: PHFetchResult<PHAsset>] = [:]
|
private var fetchCache: [String: PHFetchResult<PHAsset>] = [:]
|
||||||
|
private var libraryObserverRegistered = false
|
||||||
|
|
||||||
|
/// Active Dart subscriber for the `ux/gallery/changes` event channel.
|
||||||
|
/// Push a `nil` event on every `photoLibraryDidChange` so callers
|
||||||
|
/// reload reactively against the committed library state — the
|
||||||
|
/// observer fires after iOS commits, sidestepping the
|
||||||
|
/// `presentLimitedLibraryPicker` completion-vs-commit race.
|
||||||
|
private var libraryEventSink: FlutterEventSink?
|
||||||
|
|
||||||
public func register(with registrar: FlutterPluginRegistrar) {
|
public func register(with registrar: FlutterPluginRegistrar) {
|
||||||
let channel = FlutterMethodChannel(
|
let channel = FlutterMethodChannel(
|
||||||
@@ -18,15 +26,69 @@ public class GalleryPlugin: NSObject, NativePlugin {
|
|||||||
channel.setMethodCallHandler { [weak self] call, result in
|
channel.setMethodCallHandler { [weak self] call, result in
|
||||||
self?.handle(call: call, result: result)
|
self?.handle(call: call, result: result)
|
||||||
}
|
}
|
||||||
|
let events = FlutterEventChannel(
|
||||||
|
name: "ux/gallery/changes",
|
||||||
|
binaryMessenger: registrar.messenger(),
|
||||||
|
)
|
||||||
|
events.setStreamHandler(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func onListen(
|
||||||
|
withArguments arguments: Any?,
|
||||||
|
eventSink events: @escaping FlutterEventSink,
|
||||||
|
) -> FlutterError? {
|
||||||
|
libraryEventSink = events
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||||
|
libraryEventSink = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `PHFetchResult` snapshots go stale after the limited-library
|
||||||
|
/// subset changes or any external Photos.app edit; observing
|
||||||
|
/// change events lets us drop the cache so the next `assets`
|
||||||
|
/// call re-fetches against the live library. Deferred to first
|
||||||
|
/// granted/limited authorization — registering at plugin init
|
||||||
|
/// triggers iOS's permission evaluation on app launch even before
|
||||||
|
/// the user touches the picker.
|
||||||
|
private func ensureLibraryObserver() {
|
||||||
|
guard !libraryObserverRegistered else { return }
|
||||||
|
PHPhotoLibrary.shared().register(self)
|
||||||
|
libraryObserverRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if libraryObserverRegistered {
|
||||||
|
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||||
|
// Callback runs on an arbitrary background thread; hop to main
|
||||||
|
// before touching `fetchCache` or pushing the Flutter event so
|
||||||
|
// we don't race the method-channel handler.
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.fetchCache.removeAll()
|
||||||
|
self?.libraryEventSink?(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handle(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
private func handle(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
switch call.method {
|
switch call.method {
|
||||||
case "permission":
|
case "permission":
|
||||||
result(Self.permissionString(Self.currentAuthorization()))
|
let status = Self.currentAuthorization()
|
||||||
|
if Self.isAuthorizedForLibrary(status) {
|
||||||
|
ensureLibraryObserver()
|
||||||
|
}
|
||||||
|
result(Self.permissionString(status))
|
||||||
case "requestPermission":
|
case "requestPermission":
|
||||||
Self.requestAuthorization { status in
|
Self.requestAuthorization { [weak self] status in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
if Self.isAuthorizedForLibrary(status) {
|
||||||
|
self?.ensureLibraryObserver()
|
||||||
|
}
|
||||||
result(Self.permissionString(status))
|
result(Self.permissionString(status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,10 +98,19 @@ public class GalleryPlugin: NSObject, NativePlugin {
|
|||||||
}
|
}
|
||||||
result(nil)
|
result(nil)
|
||||||
case "presentLimitedLibraryPicker":
|
case "presentLimitedLibraryPicker":
|
||||||
if #available(iOS 14, *), let vc = UxWindow.topViewController {
|
// Just dismissal signal — the actual reload trigger is the
|
||||||
|
// `ux/gallery/changes` event channel driven by the library
|
||||||
|
// observer (which fires after iOS commits the new subset).
|
||||||
|
if #available(iOS 15, *), let vc = UxWindow.topViewController {
|
||||||
|
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: vc) { _ in
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
|
} else if #available(iOS 14, *), let vc = UxWindow.topViewController {
|
||||||
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: vc)
|
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: vc)
|
||||||
|
result(nil)
|
||||||
|
} else {
|
||||||
|
result(nil)
|
||||||
}
|
}
|
||||||
result(nil)
|
|
||||||
case "albums":
|
case "albums":
|
||||||
handleAlbums(call: call, result: result)
|
handleAlbums(call: call, result: result)
|
||||||
case "assets":
|
case "assets":
|
||||||
@@ -70,6 +141,12 @@ public class GalleryPlugin: NSObject, NativePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func isAuthorizedForLibrary(_ status: PHAuthorizationStatus) -> Bool {
|
||||||
|
if status == .authorized { return true }
|
||||||
|
if #available(iOS 14, *), status == .limited { return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private static func permissionString(_ status: PHAuthorizationStatus) -> String {
|
private static func permissionString(_ status: PHAuthorizationStatus) -> String {
|
||||||
switch status {
|
switch status {
|
||||||
case .notDetermined: return "notDetermined"
|
case .notDetermined: return "notDetermined"
|
||||||
|
|||||||
@@ -91,6 +91,13 @@ abstract class UxGalleryBackend {
|
|||||||
});
|
});
|
||||||
Future<UxAssetThumbnail> thumbnail(String assetId, {required int sizePx});
|
Future<UxAssetThumbnail> thumbnail(String assetId, {required int sizePx});
|
||||||
Future<io.File> resolveFile(String assetId);
|
Future<io.File> resolveFile(String assetId);
|
||||||
|
|
||||||
|
/// Emits whenever the underlying photo library reports a change —
|
||||||
|
/// limited-subset edits via `presentLimitedLibraryPicker`, external
|
||||||
|
/// edits from Photos.app, or permission flips from Settings.
|
||||||
|
/// Subscribers should re-fetch any cached album / asset listings on
|
||||||
|
/// each event.
|
||||||
|
Stream<void> get libraryChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Static facade for the system photo library. All state lives in the
|
/// Static facade for the system photo library. All state lives in the
|
||||||
@@ -150,6 +157,11 @@ class UxGallery {
|
|||||||
/// app cache (since `dart:io` can't read content URIs directly).
|
/// app cache (since `dart:io` can't read content URIs directly).
|
||||||
static Future<io.File> resolveFile(String assetId) =>
|
static Future<io.File> resolveFile(String assetId) =>
|
||||||
backend.resolveFile(assetId);
|
backend.resolveFile(assetId);
|
||||||
|
|
||||||
|
/// Emits whenever the system photo library changes — limited-subset
|
||||||
|
/// edits, external edits, permission changes. Picker UIs subscribe
|
||||||
|
/// to reload their cached asset list reactively.
|
||||||
|
static Stream<void> get libraryChanges => backend.libraryChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default [UxGalleryBackend] — dispatches to native code via the
|
/// Default [UxGalleryBackend] — dispatches to native code via the
|
||||||
@@ -157,6 +169,14 @@ class UxGallery {
|
|||||||
/// after swapping to a fake.
|
/// after swapping to a fake.
|
||||||
class MethodChannelGalleryBackend implements UxGalleryBackend {
|
class MethodChannelGalleryBackend implements UxGalleryBackend {
|
||||||
static const _channel = MethodChannel('ux/gallery');
|
static const _channel = MethodChannel('ux/gallery');
|
||||||
|
static const _changesChannel = EventChannel('ux/gallery/changes');
|
||||||
|
|
||||||
|
/// Lazy broadcast view of `ux/gallery/changes`. The native side only
|
||||||
|
/// holds a single sink; a broadcast Stream lets multiple Dart
|
||||||
|
/// listeners (e.g. picker + selected-strip) subscribe independently.
|
||||||
|
@override
|
||||||
|
late final Stream<void> libraryChanges =
|
||||||
|
_changesChannel.receiveBroadcastStream().map((_) {});
|
||||||
|
|
||||||
static String _kindArg(UxAssetKind? k) => switch (k) {
|
static String _kindArg(UxAssetKind? k) => switch (k) {
|
||||||
null => 'any',
|
null => 'any',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
@@ -45,6 +46,17 @@ class FakeUxGalleryBackend implements UxGalleryBackend {
|
|||||||
final UxAssetThumbnail Function(String assetId, int sizePx) _thumbnailFor;
|
final UxAssetThumbnail Function(String assetId, int sizePx) _thumbnailFor;
|
||||||
final io.File Function(String assetId) _fileFor;
|
final io.File Function(String assetId) _fileFor;
|
||||||
|
|
||||||
|
final StreamController<void> _libraryChanges =
|
||||||
|
StreamController<void>.broadcast();
|
||||||
|
|
||||||
|
/// Tests call this to simulate the platform `photoLibraryDidChange`
|
||||||
|
/// event — drives any reactive reload wiring without going through
|
||||||
|
/// the real method channel.
|
||||||
|
void emitLibraryChange() => _libraryChanges.add(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<void> get libraryChanges => _libraryChanges.stream;
|
||||||
|
|
||||||
/// Optional hook fired on `requestPermission`. Default updates
|
/// Optional hook fired on `requestPermission`. Default updates
|
||||||
/// `permissionState` to [UxGalleryPermission.granted].
|
/// `permissionState` to [UxGalleryPermission.granted].
|
||||||
final UxGalleryPermission Function()? onRequestPermission;
|
final UxGalleryPermission Function()? onRequestPermission;
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import Photos
|
|||||||
/// macOS counterpart of the iOS gallery bridge — same `Photos.framework`
|
/// macOS counterpart of the iOS gallery bridge — same `Photos.framework`
|
||||||
/// data layer, with `NSImage` swapped in for `UIImage` and the
|
/// data layer, with `NSImage` swapped in for `UIImage` and the
|
||||||
/// limited-library picker dropped (macOS has no equivalent).
|
/// limited-library picker dropped (macOS has no equivalent).
|
||||||
public class GalleryPlugin: NSObject, NativePlugin {
|
public class GalleryPlugin: NSObject, NativePlugin, PHPhotoLibraryChangeObserver, FlutterStreamHandler {
|
||||||
private let imageManager = PHCachingImageManager()
|
private let imageManager = PHCachingImageManager()
|
||||||
private var fetchCache: [String: PHFetchResult<PHAsset>] = [:]
|
private var fetchCache: [String: PHFetchResult<PHAsset>] = [:]
|
||||||
|
private var libraryObserverRegistered = false
|
||||||
|
|
||||||
|
/// Sink for the `ux/gallery/changes` event channel — emits void on
|
||||||
|
/// every `photoLibraryDidChange` so picker UIs can reload reactively.
|
||||||
|
private var libraryEventSink: FlutterEventSink?
|
||||||
|
|
||||||
public func register(with registrar: FlutterPluginRegistrar) {
|
public func register(with registrar: FlutterPluginRegistrar) {
|
||||||
let channel = FlutterMethodChannel(
|
let channel = FlutterMethodChannel(
|
||||||
@@ -17,15 +22,68 @@ public class GalleryPlugin: NSObject, NativePlugin {
|
|||||||
channel.setMethodCallHandler { [weak self] call, result in
|
channel.setMethodCallHandler { [weak self] call, result in
|
||||||
self?.handle(call: call, result: result)
|
self?.handle(call: call, result: result)
|
||||||
}
|
}
|
||||||
|
let events = FlutterEventChannel(
|
||||||
|
name: "ux/gallery/changes",
|
||||||
|
binaryMessenger: registrar.messenger,
|
||||||
|
)
|
||||||
|
events.setStreamHandler(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func onListen(
|
||||||
|
withArguments arguments: Any?,
|
||||||
|
eventSink events: @escaping FlutterEventSink,
|
||||||
|
) -> FlutterError? {
|
||||||
|
libraryEventSink = events
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||||
|
libraryEventSink = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Photos delivers change events only after the user grants access;
|
||||||
|
/// defer registration until then so plugin init doesn't poke the
|
||||||
|
/// permission machinery at app launch.
|
||||||
|
private func ensureLibraryObserver() {
|
||||||
|
guard !libraryObserverRegistered else { return }
|
||||||
|
PHPhotoLibrary.shared().register(self)
|
||||||
|
libraryObserverRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if libraryObserverRegistered {
|
||||||
|
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.fetchCache.removeAll()
|
||||||
|
self?.libraryEventSink?(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isAuthorizedForLibrary(_ status: PHAuthorizationStatus) -> Bool {
|
||||||
|
if status == .authorized { return true }
|
||||||
|
if #available(macOS 11.0, *), status == .limited { return true }
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handle(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
private func handle(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
switch call.method {
|
switch call.method {
|
||||||
case "permission":
|
case "permission":
|
||||||
result(Self.permissionString(Self.currentAuthorization()))
|
let status = Self.currentAuthorization()
|
||||||
|
if Self.isAuthorizedForLibrary(status) {
|
||||||
|
ensureLibraryObserver()
|
||||||
|
}
|
||||||
|
result(Self.permissionString(status))
|
||||||
case "requestPermission":
|
case "requestPermission":
|
||||||
Self.requestAuthorization { status in
|
Self.requestAuthorization { [weak self] status in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
if Self.isAuthorizedForLibrary(status) {
|
||||||
|
self?.ensureLibraryObserver()
|
||||||
|
}
|
||||||
result(Self.permissionString(status))
|
result(Self.permissionString(status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user