feat: import Chinese-localized Buzz source snapshot
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
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>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user