-
Notifications
You must be signed in to change notification settings - Fork 3
Native Inserter: Add initial Photos integration #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro | |
| for scheme in CachedAssetSchemeHandler.supportedURLSchemes { | ||
| config.setURLSchemeHandler(schemeHandler, forURLScheme: scheme) | ||
| } | ||
| config.setURLSchemeHandler(MediaFileSchemeHandler(), forURLScheme: MediaFileSchemeHandler.scheme) | ||
|
|
||
| self.webView = GBWebView(frame: .zero, configuration: config) | ||
| self.webView.scrollView.keyboardDismissMode = .interactive | ||
|
|
@@ -263,8 +264,8 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro | |
| onBlockSelected: { [weak self] block in | ||
| self?.insertBlockFromInserter(block.id) | ||
| }, | ||
| onMediaSelected: { | ||
| print("insert media:", $0) | ||
| onMediaSelected: { [weak self] selection in | ||
| self?.insertMediaFromInserter(selection) | ||
| } | ||
| ) | ||
| }) | ||
|
|
@@ -278,6 +279,17 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro | |
| evaluate("window.blockInserter.insertBlock('\(blockID)')") | ||
| } | ||
|
|
||
| private func insertMediaFromInserter(_ selection: [MediaInfo]) { | ||
| guard !selection.isEmpty else { return } | ||
|
|
||
| guard let data = try? JSONEncoder().encode(selection), | ||
| let string = String(data: data, encoding: .utf8) else { | ||
| debugPrint("Failed to serialize media array to JSON") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should never happen. |
||
| return | ||
| } | ||
| evaluate("window.blockInserter.insertMedia(\(string))") | ||
| } | ||
|
|
||
| private func openMediaLibrary(_ config: OpenMediaLibraryAction) { | ||
| delegate?.editor(self, didRequestMediaFromSiteMediaLibrary: config) | ||
| } | ||
|
|
||
83 changes: 83 additions & 0 deletions
83
ios/Sources/GutenbergKit/Sources/Media/MediaFileManager.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import Foundation | ||
| import PhotosUI | ||
| import SwiftUI | ||
| import UniformTypeIdentifiers | ||
|
|
||
| /// Manages media files for the editor, handling imports, storage, and cleanup. | ||
| /// | ||
| /// Files are stored in the Library/GutenbergKit/Uploads directory and served via | ||
| /// a custom `gbk-media-file://` URL scheme. Old files are automatically cleaned up. | ||
| actor MediaFileManager { | ||
| /// Shared instance for app-wide media management | ||
| static let shared = MediaFileManager() | ||
|
|
||
| private let fileManager = FileManager.default | ||
| private let rootURL: URL | ||
| private let uploadsDirectory: URL | ||
|
|
||
| init(rootURL: URL = URL.libraryDirectory.appendingPathComponent("GutenbergKit")) { | ||
| self.rootURL = rootURL | ||
| self.uploadsDirectory = self.rootURL.appendingPathComponent("Uploads") | ||
| Task { | ||
| await cleanupOldFiles() | ||
| } | ||
| } | ||
|
|
||
| /// Imports a photo picker item and saves it to the uploads directory. | ||
| /// | ||
| /// - Returns: MediaInfo with a `gbk-media-file://` URL and detected media type | ||
| func `import`(_ item: PhotosPickerItem) async throws -> MediaInfo { | ||
| guard let data = try await item.loadTransferable(type: Data.self) else { | ||
| throw URLError(.unknown) | ||
| } | ||
| let contentType = item.supportedContentTypes.first | ||
| let fileExtension = contentType?.preferredFilenameExtension ?? "jpeg" | ||
|
|
||
| let fileURL = try await writeData(data, withExtension: fileExtension) | ||
| return MediaInfo(url: fileURL.absoluteString, type: contentType?.preferredMIMEType) | ||
| } | ||
|
|
||
| /// Saves media data to the uploads directory and returns a URL with a | ||
| /// custom scheme. | ||
| func writeData(_ data: Data, withExtension ext: String) async throws -> URL { | ||
| let fileName = "\(UUID().uuidString).\(ext)" | ||
| let destinationURL = uploadsDirectory.appendingPathComponent(fileName) | ||
|
|
||
| try fileManager.createDirectory(at: uploadsDirectory, withIntermediateDirectories: true) | ||
| try data.write(to: destinationURL) | ||
|
|
||
| return URL(string: "\(MediaFileSchemeHandler.scheme):///Uploads/\(fileName)")! | ||
| } | ||
|
|
||
| /// Gets URLResponse and data for a `gbk-media-file` URL | ||
| func getData(for url: URL) async throws -> Data { | ||
| // Convert `gbk-media-file:///Uploads/filename.jpg` to actual file path | ||
| let fileURL = rootURL.appendingPathComponent(url.path) | ||
| return try Data(contentsOf: fileURL) | ||
| } | ||
|
|
||
| /// Cleans up files older than 2 days | ||
| private func cleanupOldFiles() { | ||
| let sevenDaysAgo = Date().addingTimeInterval(-2 * 24 * 60 * 60) | ||
|
|
||
| do { | ||
| let contents = try fileManager.contentsOfDirectory( | ||
| at: uploadsDirectory, | ||
| includingPropertiesForKeys: [.creationDateKey], | ||
| options: .skipsHiddenFiles | ||
| ) | ||
|
|
||
| for fileURL in contents { | ||
| if let attributes = try? fileManager.attributesOfItem(atPath: fileURL.path), | ||
| let creationDate = attributes[.creationDate] as? Date, | ||
| creationDate < sevenDaysAgo { | ||
| try? fileManager.removeItem(at: fileURL) | ||
| } | ||
| } | ||
| } catch { | ||
| #if DEBUG | ||
| print("Failed to clean up old files: \(error)") | ||
| #endif | ||
| } | ||
| } | ||
| } |
52 changes: 52 additions & 0 deletions
52
ios/Sources/GutenbergKit/Sources/Media/MediaFileSchemeHandler.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import Foundation | ||
| import WebKit | ||
|
|
||
| /// Handles `gbk-media-file://` URL scheme requests in WKWebView. | ||
| /// Serves media files from MediaFileManager with appropriate CORS headers. | ||
| final class MediaFileSchemeHandler: NSObject, WKURLSchemeHandler { | ||
| /// The custom URL scheme handled by this class | ||
| nonisolated static let scheme = "gbk-media-file" | ||
|
|
||
| func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { | ||
| guard let url = urlSchemeTask.request.url else { | ||
| urlSchemeTask.didFailWithError(URLError(.badURL)) | ||
| return | ||
| } | ||
| Task { | ||
| do { | ||
| let (response, data) = try await getResponse(for: url) | ||
| urlSchemeTask.didReceive(response) | ||
| urlSchemeTask.didReceive(data) | ||
| urlSchemeTask.didFinish() | ||
| } catch { | ||
| urlSchemeTask.didFailWithError(error) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func getResponse(for url: URL) async throws -> (URLResponse, Data) { | ||
| let data = try await MediaFileManager.shared.getData(for: url) | ||
|
|
||
| let headers = [ | ||
| "Access-Control-Allow-Origin": "*", | ||
| "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", | ||
| "Access-Control-Allow-Headers": "*", | ||
| "Cache-Control": "no-cache" | ||
| ] | ||
|
|
||
| guard let response = HTTPURLResponse( | ||
| url: url, | ||
| statusCode: 200, | ||
| httpVersion: "HTTP/1.1", | ||
| headerFields: headers | ||
| ) else { | ||
| throw URLError(.unknown) | ||
| } | ||
|
|
||
| return (response, data) | ||
| } | ||
|
|
||
| func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { | ||
| // Nothing to do here for simple file serving | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same approach we use for the assets. It's currently the only way to let
WKWebViewaccess the files on disk, but I hope we can simplify it in the future by extracting the bundled editor on disk and pointingWKWebViewto that directly and not the readonly bundle.