Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions ios/Sources/GutenbergKit/Sources/EditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
25 changes: 25 additions & 0 deletions ios/Sources/GutenbergKit/Sources/Media/MediaInfo.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
51 changes: 51 additions & 0 deletions ios/Sources/GutenbergKit/Sources/Media/MediaPickerController.swift
Original file line number Diff line number Diff line change
@@ -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?
}
37 changes: 37 additions & 0 deletions ios/Sources/GutenbergKit/Sources/Media/MediaPickerMenu.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,33 @@ 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()

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)
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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