From e18bc87858406400d8d30ee0944e30fbedf61a19 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Thu, 23 Oct 2025 14:53:37 -0400 Subject: [PATCH] Add infrastructure for injecting media pickers --- .../Sources/EditorViewController.swift | 32 +++++++++--- .../EditorViewControllerDelegate.swift | 25 +-------- .../Sources/Media/MediaInfo.swift | 25 +++++++++ .../Sources/Media/MediaPickerController.swift | 51 +++++++++++++++++++ .../Sources/Media/MediaPickerMenu.swift | 37 ++++++++++++++ .../BlockInserter/BlockInserterView.swift | 49 +++++++++++++++--- 6 files changed, 183 insertions(+), 36 deletions(-) create mode 100644 ios/Sources/GutenbergKit/Sources/Media/MediaInfo.swift create mode 100644 ios/Sources/GutenbergKit/Sources/Media/MediaPickerController.swift create mode 100644 ios/Sources/GutenbergKit/Sources/Media/MediaPickerMenu.swift diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index eb7272aa..8eaec687 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -14,6 +14,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro public var configuration: EditorConfiguration private var _isEditorRendered = false private var _isEditorSetup = false + private let mediaPicker: MediaPickerController? private let controller: GutenbergEditorController private let timestampInit = CFAbsoluteTimeGetCurrent() @@ -28,8 +29,13 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro private let isWarmupMode: Bool /// Initalizes the editor with the initial content (Gutenberg). - public init(configuration: EditorConfiguration = .default, isWarmupMode: Bool = false) { + public init( + configuration: EditorConfiguration = .default, + mediaPicker: MediaPickerController? = nil, + isWarmupMode: Bool = false + ) { self.configuration = configuration + self.mediaPicker = mediaPicker self.assetsLibrary = EditorAssetsLibrary(configuration: configuration) self.controller = GutenbergEditorController(configuration: configuration) @@ -240,11 +246,25 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro // MARK: - Internal (Block Inserter) private func showBlockInserter(blocks: [EditorBlock]) { - present(UIHostingController(rootView: NavigationView { - BlockInserterView(blocks: blocks) { - print("did select:", $0) - } - }), animated: true) + let context = MediaPickerPresentationContext() + + let host = UIHostingController(rootView: NavigationStack { + BlockInserterView( + blocks: blocks, + mediaPicker: mediaPicker, + presentationContext: context, + onBlockSelected: { + print("insert blocks:", $0) + }, + onMediaSelected: { + print("insert media:", $0) + } + ) + }) + + context.viewController = host + + present(host, animated: true) } private func openMediaLibrary(_ config: OpenMediaLibraryAction) { diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift b/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift index 424903a7..dc574e7a 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewControllerDelegate.swift @@ -52,6 +52,7 @@ public protocol EditorViewControllerDelegate: AnyObject { /// - parameter dialogType: The type of modal dialog that closed (e.g., "block-inserter", "media-library"). func editor(_ viewController: EditorViewController, didCloseModalDialog dialogType: String) } + #endif public struct EditorState { @@ -150,27 +151,3 @@ public struct OpenMediaLibraryAction: Codable { case multiple([Int]) } } - -public struct MediaInfo: Codable { - public let id: Int32? - public let url: String? - public let type: String? - public let title: String? - public let caption: String? - public let alt: String? - public let metadata: [String: String] - - private enum CodingKeys: String, CodingKey { - case id, url, type, title, caption, alt, metadata - } - - public init(id: Int32?, url: String?, type: String?, caption: String? = nil, title: String? = nil, alt: String? = nil, metadata: [String: String] = [:]) { - self.id = id - self.url = url - self.type = type - self.caption = caption - self.title = title - self.alt = alt - self.metadata = metadata - } -} diff --git a/ios/Sources/GutenbergKit/Sources/Media/MediaInfo.swift b/ios/Sources/GutenbergKit/Sources/Media/MediaInfo.swift new file mode 100644 index 00000000..e86a1419 --- /dev/null +++ b/ios/Sources/GutenbergKit/Sources/Media/MediaInfo.swift @@ -0,0 +1,25 @@ +import Foundation + +public struct MediaInfo: Codable { + public let id: Int32? + public let url: String? + public let type: String? + public let title: String? + public let caption: String? + public let alt: String? + public let metadata: [String: String] + + private enum CodingKeys: String, CodingKey { + case id, url, type, title, caption, alt, metadata + } + + public init(id: Int32? = nil, url: String?, type: String?, caption: String? = nil, title: String? = nil, alt: String? = nil, metadata: [String: String] = [:]) { + self.id = id + self.url = url + self.type = type + self.caption = caption + self.title = title + self.alt = alt + self.metadata = metadata + } +} diff --git a/ios/Sources/GutenbergKit/Sources/Media/MediaPickerController.swift b/ios/Sources/GutenbergKit/Sources/Media/MediaPickerController.swift new file mode 100644 index 00000000..17a2cf12 --- /dev/null +++ b/ios/Sources/GutenbergKit/Sources/Media/MediaPickerController.swift @@ -0,0 +1,51 @@ +import UIKit + +public struct MediaPickerAction: Identifiable { + public let id: String + public let title: String + public let image: UIImage + + init(id: String, title: String, image: UIImage) { + self.id = id + self.title = title + self.image = image + } +} + +public struct MediaPickerActionGroup: Identifiable { + public let id: String + public let actions: [MediaPickerAction] +} + +/// Configuration parameters for media picker behavior. +public struct MediaPickerParameters { + /// Filter that determines which types of media can be selected. + public enum MediaFilter { + case images + case videos + case all + } + + /// Optional filter to restrict the types of media that can be selected. + public var filter: MediaFilter? + + /// Whether users can select multiple media items at once. + public var isMultipleSelectionEnabled: Bool + + public init(filter: MediaFilter? = nil, isMultipleSelectionEnabled: Bool = false) { + self.filter = filter + self.isMultipleSelectionEnabled = isMultipleSelectionEnabled + } +} + +public protocol MediaPickerController { + /// Returns a grouped list of media picker actions for the given parameters. + func getActions(for parameters: MediaPickerParameters) -> [MediaPickerActionGroup] + + /// Perform the action and return the selected media. + func perform(_ action: MediaPickerAction, parameters: MediaPickerParameters, from presentingViewController: UIViewController) async -> [MediaInfo] +} + +final class MediaPickerPresentationContext { + weak var viewController: UIViewController? +} diff --git a/ios/Sources/GutenbergKit/Sources/Media/MediaPickerMenu.swift b/ios/Sources/GutenbergKit/Sources/Media/MediaPickerMenu.swift new file mode 100644 index 00000000..d62d39a9 --- /dev/null +++ b/ios/Sources/GutenbergKit/Sources/Media/MediaPickerMenu.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct MediaPickerMenu: View { + let picker: MediaPickerController + let context: MediaPickerPresentationContext + var parameters = MediaPickerParameters() + let onMediaSelected: ([MediaInfo]) -> Void + + var body: some View { + Menu { + ForEach(picker.getActions(for: parameters)) { group in + Section { + ForEach(group.actions, content: makeButton) + } + } + } label: { + Image(systemName: "ellipsis") + } + } + + private func makeButton(for action: MediaPickerAction) -> some View { + Button { + Task { @MainActor in + if let viewController = context.viewController { + let selection = await picker.perform(action, parameters: parameters, from: viewController) + onMediaSelected(selection) + } + } + } label: { + Label { + Text(action.title) + } icon: { + Image(uiImage: action.image) + } + } + } +} diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift index b159b636..50b376d7 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift @@ -3,7 +3,10 @@ import PhotosUI import UIKit struct BlockInserterView: View { + let mediaPicker: MediaPickerController? + let presentationContext: MediaPickerPresentationContext let onBlockSelected: (EditorBlock) -> Void + let onMediaSelected: ([MediaInfo]) -> Void @StateObject private var viewModel: BlockInserterViewModel @StateObject private var iconCache = BlockIconCache() @@ -11,16 +14,22 @@ struct BlockInserterView: View { private let maxSelectionCount = 10 @Environment(\.dismiss) private var dismiss - + init( blocks: [EditorBlock], + mediaPicker: MediaPickerController?, + presentationContext: MediaPickerPresentationContext, onBlockSelected: @escaping (EditorBlock) -> Void, + onMediaSelected: @escaping ([MediaInfo]) -> Void ) { + self.mediaPicker = mediaPicker + self.presentationContext = presentationContext self.onBlockSelected = onBlockSelected - let viewModel = BlockInserterViewModel(blocks: blocks) - self._viewModel = StateObject(wrappedValue: viewModel) + self.onMediaSelected = onMediaSelected + + self._viewModel = StateObject(wrappedValue: BlockInserterViewModel(blocks: blocks)) } - + var body: some View { content .background(Material.ultraThin) @@ -57,13 +66,22 @@ struct BlockInserterView: View { } .tint(Color.primary) } + + ToolbarItemGroup(placement: .topBarTrailing) { + if let mediaPicker { + MediaPickerMenu(picker: mediaPicker, context: presentationContext) { + dismiss() + onMediaSelected($0) + } + } + } } // MARK: - Actions - private func insertBlock(_ blockType: EditorBlock) { + private func insertBlock(_ block: EditorBlock) { dismiss() - onBlockSelected(blockType) + onBlockSelected(block) } } @@ -74,10 +92,29 @@ struct BlockInserterView: View { NavigationStack { BlockInserterView( blocks: EditorBlock.mocks, + mediaPicker: MockMediaPickerController(), + presentationContext: MediaPickerPresentationContext(), onBlockSelected: { print("block selected: \($0.name)") + }, + onMediaSelected: { + print("media selected: \($0)") } ) } } + +struct MockMediaPickerController: MediaPickerController { + func getActions(for parameters: MediaPickerParameters) -> [MediaPickerActionGroup] { + let group = MediaPickerActionGroup(id: "extra", actions: [ + MediaPickerAction(id: "files", title: "Files", image: UIImage(systemName: "folder")!) + ]) + return [group] + } + + func perform(_ action: MediaPickerAction, parameters: MediaPickerParameters, from presentingViewController: UIViewController) async -> [MediaInfo] { + print("action selected:", action) + return [] + } +} #endif