Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation
import UniformTypeIdentifiers

/// Public boundary payload that app-target external pickers (Stock Photos)
/// construct and pass through `ExternalMediaPickerDelegate`. The
/// module's view model converts this to `UploadSource.remoteURL(_)` before
/// enqueueing — keeps the internal `UploadSource` enum out of the public API.
public struct ExternalRemoteMedia: Sendable {
public let url: URL
public let suggestedName: String
public let contentType: UTType
public let caption: String?

public init(url: URL, suggestedName: String, contentType: UTType, caption: String?) {
self.url = url
self.suggestedName = suggestedName
self.contentType = contentType
self.caption = caption
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ struct FailedUpload: Identifiable, Sendable {
/// `MediaCreateParams` / temp file were never produced — the
/// Uploads-screen row should offer Dismiss only.
let isRetryable: Bool
/// Materialized temp file on disk; non-nil exactly when `isRetryable`
/// (both derive from the materialized payload surviving the failure).
let localFileURL: URL?
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ struct PendingUpload: Identifiable, Sendable {
let displayName: String // basename of the temp file
let kind: MediaKind // for icon + Uploads-row rendering
let progress: Progress // bound to ProgressView directly
/// Materialized temp file on disk; nil until materialization completes.
/// Drives the Uploads-row thumbnail.
let localFileURL: URL?
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import WordPressShared
enum AspectRatioPreference {
private static let key = UPRUConstants.mediaAspectRatioModeEnabledKey

@MainActor
static func load(defaults: UserDefaults = .standard) -> Bool {
if let value = defaults.object(forKey: key) as? Bool { return value }
return UIDevice.current.userInterfaceIdiom == .pad
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@ private struct InternalPending {
id: id,
displayName: materialized?.displayName ?? displayName,
kind: materialized?.kind ?? kind,
progress: overallProgress
progress: overallProgress,
localFileURL: materialized?.tempFileURL
)
}
}
Expand All @@ -394,7 +395,8 @@ private struct InternalFailed {
displayName: materialized?.displayName ?? displayName,
kind: materialized?.kind ?? kind,
errorMessage: errorMessage,
isRetryable: materialized != nil
isRetryable: materialized != nil,
localFileURL: materialized?.tempFileURL
)
}
}
42 changes: 42 additions & 0 deletions Modules/Sources/WordPressMediaLibrary/Views/BannerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import SwiftUI

struct BannerView: View {
let summary: MediaLibraryViewModel.BannerSummary
let onTap: () -> Void

var body: some View {
Button(action: onTap) {
HStack(spacing: 12) {
if summary.pendingCount > 0 {
ProgressView()
.progressViewStyle(.circular)
.controlSize(.small)
}
Text(label)
.font(.subheadline)
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.tertiary)
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(.thinMaterial, in: .rect(cornerRadius: 12))
}
.buttonStyle(.plain)
.padding(.horizontal, 16)
.padding(.vertical, 8)
}

private var label: String {
switch (summary.pendingCount, summary.failedCount) {
case (let p, 0) where p > 0:
return String.localizedStringWithFormat(Strings.uploadBannerUploadingOnly, p)
case (let p, let f) where p > 0 && f > 0:
return String.localizedStringWithFormat(Strings.uploadBannerMixed, p, f)
case (0, let f) where f > 0:
return String.localizedStringWithFormat(Strings.uploadBannerFailedOnly, f)
default:
return ""
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import SwiftUI
import UIKit
import UniformTypeIdentifiers

struct CameraPickerRepresentable: UIViewControllerRepresentable {
enum Mode { case photo, video }
let mode: Mode
let onPicked: (UploadSource) -> Void
let onCancel: () -> Void

func makeUIViewController(context: Context) -> UIImagePickerController {
let controller = UIImagePickerController()
controller.sourceType = .camera
controller.mediaTypes = [
mode == .photo ? UTType.image.identifier : UTType.movie.identifier
]
if mode == .video {
controller.videoQuality = .typeHigh
}
controller.delegate = context.coordinator
return controller
}

func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}

func makeCoordinator() -> Coordinator { Coordinator(parent: self) }

final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: CameraPickerRepresentable

init(parent: CameraPickerRepresentable) {
self.parent = parent
}

func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
picker.dismiss(animated: true)
switch parent.mode {
case .photo:
if let image = info[.originalImage] as? UIImage {
parent.onPicked(.cameraImage(image, capturedAt: Date()))
} else {
parent.onCancel()
}
case .video:
if let url = info[.mediaURL] as? URL {
parent.onPicked(.cameraVideo(url, capturedAt: Date()))
} else {
parent.onCancel()
}
}
}

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
parent.onCancel()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import SwiftUI

/// Public delegate protocol the app-target picker sheets call into.
/// `MediaLibraryViewModel` conforms internally; the app target only sees
/// this protocol existential via `ExternalMediaPickerOption.sheetContent`.
@MainActor
public protocol ExternalMediaPickerDelegate: AnyObject {
func didPick(remoteMedia: [ExternalRemoteMedia])
func didPick(imagePlaygroundFile url: URL, suggestedName: String)
func didCancel()
}

/// Public extension point that `MediaLibraryView`'s add-menu iterates.
/// `MediaLibraryRouting` constructs one of these per external source the
/// app target wants to offer (Stock Photos, Image Playground).
public struct ExternalMediaPickerOption: Identifiable {
public let id: String
public let label: String
public let systemImage: String
public let sheetContent: @MainActor (_ delegate: any ExternalMediaPickerDelegate) -> AnyView

public init(
id: String,
label: String,
systemImage: String,
sheetContent: @escaping @MainActor (_ delegate: any ExternalMediaPickerDelegate) -> AnyView
) {
self.id = id
self.label = label
self.systemImage = systemImage
self.sheetContent = sheetContent
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ public enum MediaLibraryHostingController {
@MainActor
public static func make(
client: WordPressClient,
tracker: any MediaTracker
tracker: any MediaTracker,
uploader: MediaUploader,
externalPickerOptions: [ExternalMediaPickerOption] = []
) -> UIViewController {
let view = MediaLibraryContainerView(client: client, tracker: tracker)
let view = MediaLibraryContainerView(
client: client,
tracker: tracker,
uploader: uploader,
externalPickerOptions: externalPickerOptions
)
let host = UIHostingController(rootView: view)
host.navigationItem.largeTitleDisplayMode = .never
return host
Expand All @@ -30,6 +37,8 @@ public enum MediaLibraryHostingController {
private struct MediaLibraryContainerView: View {
let client: WordPressClient
let tracker: any MediaTracker
let uploader: MediaUploader
let externalPickerOptions: [ExternalMediaPickerOption]

@State private var resolved: Resolved?
@State private var error: Error?
Expand All @@ -48,7 +57,8 @@ private struct MediaLibraryContainerView: View {
viewModel: resolved.viewModel,
service: resolved.service,
client: client,
tracker: tracker
tracker: tracker,
externalPickerOptions: externalPickerOptions
)
} else if let error {
EmptyStateView.failure(error: error) {
Expand All @@ -66,7 +76,8 @@ private struct MediaLibraryContainerView: View {
viewModel: MediaLibraryViewModel(
service: service,
client: client,
tracker: tracker
tracker: tracker,
uploader: uploader
),
service: service
)
Expand Down
Loading