127 lines
4.5 KiB
Swift
127 lines
4.5 KiB
Swift
import UIKit
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
// Null implementation used when BookStax is not configured (no keychain credentials).
|
|
private struct NullShareAPIService: ShareAPIServiceProtocol {
|
|
func fetchShelves() async throws -> [ShelfSummary] { [] }
|
|
func fetchBooks(shelfId: Int) async throws -> [BookSummary] { [] }
|
|
func fetchChapters(bookId: Int) async throws -> [ChapterSummary] { [] }
|
|
func createPage(bookId: Int, chapterId: Int?, title: String, markdown: String) async throws -> PageResult {
|
|
throw ShareAPIError.notConfigured
|
|
}
|
|
}
|
|
|
|
/// Entry point for the BookStax Share Extension.
|
|
/// `NSExtensionPrincipalClass` in Info.plist points to this class.
|
|
final class ShareViewController: UIViewController {
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
view.backgroundColor = .systemGroupedBackground
|
|
|
|
Task { @MainActor in
|
|
let text = await extractSharedText()
|
|
let viewModel = makeViewModel(for: text)
|
|
embedSwiftUI(viewModel: viewModel)
|
|
}
|
|
}
|
|
|
|
// MARK: - ViewModel factory
|
|
|
|
private func makeViewModel(for text: String) -> ShareViewModel {
|
|
if let creds = ShareExtensionKeychainService.loadCredentials() {
|
|
let api = ShareExtensionAPIService(
|
|
serverURL: creds.serverURL,
|
|
tokenId: creds.tokenId,
|
|
tokenSecret: creds.tokenSecret
|
|
)
|
|
let defaults = UserDefaults(suiteName: "group.de.hanold.bookstax")
|
|
return ShareViewModel(
|
|
sharedText: text,
|
|
apiService: api,
|
|
serverURL: creds.serverURL,
|
|
isConfigured: true,
|
|
defaults: defaults
|
|
)
|
|
} else {
|
|
return ShareViewModel(
|
|
sharedText: text,
|
|
apiService: NullShareAPIService(),
|
|
serverURL: "",
|
|
isConfigured: false,
|
|
defaults: nil
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - SwiftUI embedding
|
|
|
|
private func embedSwiftUI(viewModel: ShareViewModel) {
|
|
let contentView = ShareExtensionView(
|
|
viewModel: viewModel,
|
|
onCancel: { [weak self] in self?.cancel() },
|
|
onComplete: { [weak self] in self?.complete() },
|
|
onOpenURL: { [weak self] url in self?.open(url) }
|
|
)
|
|
let host = UIHostingController(rootView: contentView)
|
|
addChild(host)
|
|
view.addSubview(host.view)
|
|
host.view.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
host.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
])
|
|
host.didMove(toParent: self)
|
|
}
|
|
|
|
// MARK: - Extension context actions
|
|
|
|
private func cancel() {
|
|
extensionContext?.cancelRequest(
|
|
withError: NSError(
|
|
domain: NSCocoaErrorDomain,
|
|
code: NSUserCancelledError,
|
|
userInfo: [NSLocalizedDescriptionKey: "Abgebrochen"]
|
|
)
|
|
)
|
|
}
|
|
|
|
private func complete() {
|
|
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
|
}
|
|
|
|
private func open(_ url: URL) {
|
|
extensionContext?.open(url, completionHandler: nil)
|
|
}
|
|
|
|
// MARK: - Text extraction
|
|
|
|
/// Extracts plain text or a URL string from the incoming NSExtensionItems.
|
|
private func extractSharedText() async -> String {
|
|
guard let items = extensionContext?.inputItems as? [NSExtensionItem] else { return "" }
|
|
|
|
for item in items {
|
|
for provider in item.attachments ?? [] {
|
|
if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
|
|
if let text = try? await provider.loadItem(
|
|
forTypeIdentifier: UTType.plainText.identifier
|
|
) as? String {
|
|
return text
|
|
}
|
|
}
|
|
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
|
if let url = try? await provider.loadItem(
|
|
forTypeIdentifier: UTType.url.identifier
|
|
) as? URL {
|
|
return url.absoluteString
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
}
|