blob: b18b148d845582d631f1f71ad386e3a362bed48f [file] [log] [blame]
// Copyright (C) 2024 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
import CoreTransferable
import Foundation
import os
import Observation
import UniformTypeIdentifiers
@_spi(Private) import WebKit
struct PDF {
let data: Data
let title: String?
}
extension PDF: Transferable {
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(exportedContentType: .pdf, exporting: \.data)
}
}
struct OpenRequest: Equatable {
let request: URLRequest
}
@Observable
@MainActor
final class BrowserViewModel {
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: BrowserViewModel.self))
private static func decideSensorAuthorization(permission: WebPage.DeviceSensorAuthorization.Permission, frame: WebPage.FrameInfo, origin: WKSecurityOrigin) async -> WKPermissionDecision {
let mediaCaptureAuthorization = WKPermissionDecision(rawValue: UserDefaults.standard.integer(forKey: AppStorageKeys.mediaCaptureAuthorization))!
let orientationAndMotionAuthorization = WKPermissionDecision(rawValue: UserDefaults.standard.integer(forKey: AppStorageKeys.orientationAndMotionAuthorization))!
return switch permission {
case .deviceOrientationAndMotion: orientationAndMotionAuthorization
case .mediaCapture: mediaCaptureAuthorization
@unknown default:
fatalError()
}
}
init() {
var configuration = WebPage.Configuration()
configuration.deviceSensorAuthorization = WebPage.DeviceSensorAuthorization(decisionHandler: Self.decideSensorAuthorization(permission:frame:origin:))
self.page = WebPage(configuration: configuration, navigationDecider: self.navigationDecider, dialogPresenter: self.dialogPresenter)
self.navigationDecider.owner = self
self.dialogPresenter.owner = self
}
let page: WebPage
private let dialogPresenter = DialogPresenter()
private let navigationDecider = NavigationDecider()
var displayedURL: String = ""
var currentOpenRequest: OpenRequest? = nil
// MARK: PDF properties
var pdfExporterIsPresented = false {
didSet {
if !pdfExporterIsPresented {
exportedPDF = nil
}
}
}
private(set) var exportedPDF: PDF? = nil {
didSet {
if exportedPDF != nil {
pdfExporterIsPresented = true
}
}
}
// MARK: Dialog properties
var isPresentingDialog = false {
didSet {
if !isPresentingDialog {
currentDialog = nil
}
}
}
var currentDialog: DialogPresenter.Dialog? = nil {
didSet {
if currentDialog != nil {
isPresentingDialog = true
}
}
}
var isPresentingFilePicker = false
var currentFilePicker: DialogPresenter.FilePicker? = nil {
didSet {
if currentFilePicker != nil {
isPresentingFilePicker = true
}
}
}
// MARK: View model functions
func openURL(_ url: URL) {
assert(url.isFileURL)
let data = try! Data(contentsOf: url)
page.load(data, mimeType: "text/html", characterEncoding: .utf8, baseURL: URL(string: "about:blank")!)
}
func didReceiveNavigationEvent(_ event: WebPage.NavigationEvent) {
Self.logger.info("Did receive navigation event \(String(describing: event))")
if event == .committed {
displayedURL = page.url?.absoluteString ?? ""
}
}
func navigateToSubmittedURL() {
guard let url = URL(string: displayedURL) else {
return
}
let request = URLRequest(url: url)
page.load(request)
}
func exportAsPDF() {
Task {
let data = try await page.exported(as: .pdf)
exportedPDF = PDF(data: data, title: !page.title.isEmpty ? page.title : nil)
}
}
func didExportPDF(result: Result<URL, any Error>) {
switch result {
case let .success(url):
Self.logger.info("Exported PDF to \(url)")
case let .failure(error):
Self.logger.error("Failed to export PDF: \(error)")
}
}
func didImportFiles(result: Result<[URL], any Error>) {
precondition(currentFilePicker != nil)
switch result {
case let .success(urls):
currentFilePicker!.completion(.selected(urls))
case .failure:
currentFilePicker!.completion(.cancel)
}
currentFilePicker = nil
}
func setCameraCaptureState(_ state: WKMediaCaptureState) {
Task { @MainActor in
await page.setCameraCaptureState(state)
}
}
func setMicrophoneCaptureState(_ state: WKMediaCaptureState) {
Task { @MainActor in
await page.setMicrophoneCaptureState(state)
}
}
}