Unit tests, Lokalisierung, ShareExtension
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user