Files
buzz/mobile/ios/Runner/InlinePhotoPicker.swift
T
cls 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
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

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
}