9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
238 lines
7.1 KiB
Swift
238 lines
7.1 KiB
Swift
import Flutter
|
|
import PhotosUI
|
|
import UIKit
|
|
import UniformTypeIdentifiers
|
|
|
|
final class InlinePhotoPickerFactory: NSObject, FlutterPlatformViewFactory {
|
|
private let messenger: FlutterBinaryMessenger
|
|
private weak var parentViewController: UIViewController?
|
|
|
|
init(
|
|
messenger: FlutterBinaryMessenger,
|
|
parentViewController: UIViewController?
|
|
) {
|
|
self.messenger = messenger
|
|
self.parentViewController = parentViewController
|
|
super.init()
|
|
}
|
|
|
|
func create(
|
|
withFrame frame: CGRect,
|
|
viewIdentifier viewId: Int64,
|
|
arguments args: Any?
|
|
) -> FlutterPlatformView {
|
|
InlinePhotoPickerPlatformView(
|
|
frame: frame,
|
|
viewIdentifier: viewId,
|
|
messenger: messenger,
|
|
parentViewController: parentViewController
|
|
)
|
|
}
|
|
}
|
|
|
|
final class InlinePhotoPickerPlatformView: NSObject, FlutterPlatformView {
|
|
private let containerView: UIView
|
|
private let channel: FlutterMethodChannel
|
|
private weak var parentViewController: UIViewController?
|
|
private var pickerViewController: PHPickerViewController?
|
|
private var selectionGeneration = 0
|
|
private var selectionTask: Task<Void, Never>?
|
|
private var selectedTemporaryPaths: [String] = []
|
|
|
|
init(
|
|
frame: CGRect,
|
|
viewIdentifier viewId: Int64,
|
|
messenger: FlutterBinaryMessenger,
|
|
parentViewController: UIViewController?
|
|
) {
|
|
containerView = UIView(frame: frame)
|
|
channel = FlutterMethodChannel(
|
|
name: "buzz/inline_photo_picker/\(viewId)",
|
|
binaryMessenger: messenger
|
|
)
|
|
self.parentViewController = parentViewController
|
|
super.init()
|
|
|
|
channel.setMethodCallHandler { [weak self] call, result in
|
|
guard call.method == "claimSelection" else {
|
|
result(FlutterMethodNotImplemented)
|
|
return
|
|
}
|
|
let paths = call.arguments as? [String] ?? []
|
|
guard let self, paths == self.selectedTemporaryPaths else {
|
|
result(false)
|
|
return
|
|
}
|
|
self.selectedTemporaryPaths = []
|
|
result(true)
|
|
}
|
|
|
|
containerView.backgroundColor = .clear
|
|
containerView.clipsToBounds = true
|
|
if #available(iOS 17.0, *) {
|
|
installPicker()
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
selectionTask?.cancel()
|
|
Self.removeTemporaryFiles(selectedTemporaryPaths)
|
|
channel.setMethodCallHandler(nil)
|
|
pickerViewController?.willMove(toParent: nil)
|
|
pickerViewController?.view.removeFromSuperview()
|
|
pickerViewController?.removeFromParent()
|
|
}
|
|
|
|
func view() -> UIView {
|
|
containerView
|
|
}
|
|
|
|
@available(iOS 17.0, *)
|
|
private func installPicker() {
|
|
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
|
configuration.filter = .images
|
|
configuration.selectionLimit = 0
|
|
configuration.selection = .continuousAndOrdered
|
|
configuration.preferredAssetRepresentationMode = .compatible
|
|
configuration.disabledCapabilities = [
|
|
.search,
|
|
.stagingArea,
|
|
.collectionNavigation,
|
|
.selectionActions,
|
|
]
|
|
configuration.edgesWithoutContentMargins = .all
|
|
|
|
let picker = PHPickerViewController(configuration: configuration)
|
|
picker.delegate = self
|
|
picker.view.backgroundColor = .clear
|
|
picker.view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
if let parentViewController {
|
|
parentViewController.addChild(picker)
|
|
}
|
|
containerView.addSubview(picker.view)
|
|
NSLayoutConstraint.activate([
|
|
picker.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
|
picker.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
|
picker.view.topAnchor.constraint(equalTo: containerView.topAnchor),
|
|
picker.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
|
])
|
|
if parentViewController != nil {
|
|
picker.didMove(toParent: parentViewController)
|
|
}
|
|
pickerViewController = picker
|
|
containerView.layoutIfNeeded()
|
|
}
|
|
|
|
private func exportPickerResult(_ result: PHPickerResult) async throws -> String {
|
|
let provider = result.itemProvider
|
|
guard
|
|
let typeIdentifier = provider.registeredTypeIdentifiers.first(where: {
|
|
guard let type = UTType($0) else { return false }
|
|
return type.conforms(to: .image)
|
|
})
|
|
else {
|
|
throw InlinePhotoPickerError.unsupportedImage
|
|
}
|
|
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) {
|
|
sourceURL,
|
|
error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
return
|
|
}
|
|
guard let sourceURL else {
|
|
continuation.resume(throwing: InlinePhotoPickerError.missingFile)
|
|
return
|
|
}
|
|
|
|
do {
|
|
let fileExtension =
|
|
sourceURL.pathExtension.isEmpty
|
|
? (UTType(typeIdentifier)?.preferredFilenameExtension ?? "jpg")
|
|
: sourceURL.pathExtension
|
|
let destinationURL = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent(UUID().uuidString)
|
|
.appendingPathExtension(fileExtension)
|
|
try FileManager.default.copyItem(
|
|
at: sourceURL,
|
|
to: destinationURL
|
|
)
|
|
continuation.resume(returning: destinationURL.path)
|
|
} catch {
|
|
continuation.resume(throwing: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func removeTemporaryFiles(_ paths: [String]) {
|
|
for path in paths where !path.isEmpty {
|
|
try? FileManager.default.removeItem(atPath: path)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension InlinePhotoPickerPlatformView: PHPickerViewControllerDelegate {
|
|
func picker(
|
|
_ picker: PHPickerViewController,
|
|
didFinishPicking results: [PHPickerResult]
|
|
) {
|
|
selectionGeneration += 1
|
|
let generation = selectionGeneration
|
|
selectionTask?.cancel()
|
|
selectionTask = nil
|
|
Self.removeTemporaryFiles(selectedTemporaryPaths)
|
|
selectedTemporaryPaths = []
|
|
channel.invokeMethod(
|
|
"selectionCountChanged",
|
|
arguments: results.count
|
|
)
|
|
|
|
guard !results.isEmpty else {
|
|
channel.invokeMethod("selectionDidChange", arguments: [String]())
|
|
return
|
|
}
|
|
|
|
selectionTask = Task { [weak self] in
|
|
guard let self else { return }
|
|
var paths: [String] = []
|
|
do {
|
|
for result in results {
|
|
try Task.checkCancellation()
|
|
paths.append(try await self.exportPickerResult(result))
|
|
}
|
|
try Task.checkCancellation()
|
|
await MainActor.run {
|
|
guard generation == self.selectionGeneration else {
|
|
Self.removeTemporaryFiles(paths)
|
|
return
|
|
}
|
|
self.selectedTemporaryPaths = paths
|
|
self.selectionTask = nil
|
|
self.channel.invokeMethod("selectionDidChange", arguments: paths)
|
|
}
|
|
} catch is CancellationError {
|
|
Self.removeTemporaryFiles(paths)
|
|
} catch {
|
|
Self.removeTemporaryFiles(paths)
|
|
await MainActor.run {
|
|
guard generation == self.selectionGeneration else { return }
|
|
self.selectionTask = nil
|
|
self.channel.invokeMethod(
|
|
"didFail",
|
|
arguments: "Unable to prepare the selected photos."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum InlinePhotoPickerError: Error {
|
|
case missingFile
|
|
case unsupportedImage
|
|
}
|