213 lines · 8.3 KB · Swift
RawDownload
import ExpoModulesCoreimport Securityimport UIKit public final class T3NativeControlsModule: Module {  private let presentationSources = T3PresentationSources()  private var videoPresentation: T3NativeVideoPresentation?  private var filePresentation: T3NativeFilePresentation?   public func definition() -> ModuleDefinition {    Name("T3NativeControls")     AsyncFunction("presentVideo") { (url: URL, title: String, sourceIdentifier: String, identifier: String, promise: Promise) in      try self.presentVideo(        url: url,        title: title,        sourceIdentifier: sourceIdentifier,        identifier: identifier,        promise: promise      )    }.runOnQueue(.main)     AsyncFunction("dismissVideo") { (identifier: String) in      self.dismissVideo(identifier: identifier)    }.runOnQueue(.main)     AsyncFunction("presentFile") { (url: URL, title: String, sourceIdentifier: String, identifier: String, promise: Promise) in      try self.presentFile(url: url, title: title, sourceIdentifier: sourceIdentifier,                           identifier: identifier, promise: promise)    }.runOnQueue(.main)     AsyncFunction("dismissFile") { (identifier: String) in      self.dismissFile(identifier: identifier)    }.runOnQueue(.main)     OnDestroy {      let presentation = self.videoPresentation      let file = self.filePresentation      DispatchQueue.main.async {        presentation?.dismiss()        file?.dismiss()      }    }     View(T3PresentationSourceView.self) {      ViewName("PresentationSource")      Prop("identifier") { (view: T3PresentationSourceView, identifier: String) in        view.sources = self.presentationSources        view.identifier = identifier      }    }     AsyncFunction("shareFileFromSource") { (url: URL, title: String, identifier: String, promise: Promise) in      try self.shareFile(url: url, title: title, sourceIdentifier: identifier, promise: promise)    }.runOnQueue(.main)     Function("getShowcasePairingUrl") {      let arguments = ProcessInfo.processInfo.arguments      guard        let flagIndex = arguments.firstIndex(of: "--showcasePairingUrl"),        arguments.indices.contains(flagIndex + 1)      else {        return nil as String?      }      return arguments[flagIndex + 1]    }     Function("getShowcaseScene") { () -> String? in      let scenePath = NSHomeDirectory() + "/Library/Caches/T3ShowcaseScene"      if let storedScene = try? String(contentsOfFile: scenePath, encoding: .utf8)        .trimmingCharacters(in: .whitespacesAndNewlines), !storedScene.isEmpty {        return storedScene      }      let arguments = ProcessInfo.processInfo.arguments      guard        let flagIndex = arguments.firstIndex(of: "--showcaseScene"),        arguments.indices.contains(flagIndex + 1)      else {        return nil as String?      }      return arguments[flagIndex + 1]    }     // The palette is fixed for the whole capture, so it only ever arrives as a    // launch argument — unlike the scene, which the runner rewrites in place.    Function("getShowcaseTheme") { () -> String? in      let arguments = ProcessInfo.processInfo.arguments      guard        let flagIndex = arguments.firstIndex(of: "--showcaseTheme"),        arguments.indices.contains(flagIndex + 1)      else {        return nil as String?      }      return arguments[flagIndex + 1]    }     Function("getShowcaseOrientation") { () -> String? in      let arguments = ProcessInfo.processInfo.arguments      guard        let flagIndex = arguments.firstIndex(of: "--showcaseOrientation"),        arguments.indices.contains(flagIndex + 1)      else {        return nil as String?      }      return arguments[flagIndex + 1]    }     // Rotates the interface without Simulator menu UI scripting, which CI    // runners cannot perform (osascript is denied Accessibility access there).    AsyncFunction("applyShowcaseOrientation") { (orientation: String) in      guard #available(iOS 16.0, *) else { return }      let mask: UIInterfaceOrientationMask = orientation == "landscape" ? .landscapeRight : .portrait      for case let windowScene as UIWindowScene in UIApplication.shared.connectedScenes {        windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: mask)) { error in          NSLog("T3NativeControls applyShowcaseOrientation(\(orientation)) failed: \(error)")        }        for window in windowScene.windows {          window.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()        }      }    }.runOnQueue(.main)     // The geometry request above can fail transiently (for example before the    // scene is foreground-active), so callers poll this until it settles.    // Screen bounds — not the scene's interface orientation — decide the    // answer because they match the captured framebuffer: with iPadOS    // windowing active, a floating landscape window still reports a portrait    // screen, and screenshots would come out portrait.    AsyncFunction("getInterfaceOrientation") { () -> String in      guard        let windowScene = UIApplication.shared.connectedScenes          .compactMap({ $0 as? UIWindowScene })          .first      else {        return "unknown"      }      let bounds = windowScene.screen.coordinateSpace.bounds      return bounds.width > bounds.height ? "landscape" : "portrait"    }.runOnQueue(.main)     Function("prepareShowcaseCapture") {      for itemClass in [kSecClassGenericPassword, kSecClassInternetPassword] {        SecItemDelete([kSecClass as String: itemClass] as CFDictionary)      }    }     Function("markShowcaseReady") { (scene: String) in      let readyPath = NSHomeDirectory() + "/Library/Caches/T3ShowcaseReadyScene"      try? scene.write(toFile: readyPath, atomically: true, encoding: .utf8)    }  }   private func presentVideo(url: URL, title: String, sourceIdentifier: String, identifier: String, promise: Promise) throws {    let isPlayableURL = url.isFileURL      ? FileManager.default.isReadableFile(atPath: url.path)      : (["https", "http"].contains(url.scheme?.lowercased() ?? "") && url.host != nil)    guard videoPresentation == nil, filePresentation == nil,      let presenter = appContext?.utilities?.currentViewController(),      isPlayableURL    else {      throw NSError(        domain: "T3NativeVideo",        code: 2,        userInfo: [NSLocalizedDescriptionKey: "The video preview is no longer available."]      )    }    let presentation = T3NativeVideoPresentation(identifier: identifier, url: url, title: title) { [weak self] error in      self?.videoPresentation = nil      if let error { promise.reject(error) } else { promise.resolve(nil) }    }    videoPresentation = presentation    presentation.present(from: presenter, sources: presentationSources, sourceIdentifier: sourceIdentifier)  }   private func dismissVideo(identifier: String) {    if videoPresentation?.identifier == identifier { videoPresentation?.dismiss() }  }   private func presentFile(url: URL, title: String, sourceIdentifier: String,                           identifier: String, promise: Promise) throws {    guard filePresentation == nil, videoPresentation == nil,      let presenter = appContext?.utilities?.currentViewController()    else { throw URLError(.cannotLoadFromNetwork) }    let file = T3NativeFilePresentation(identifier: identifier, sources: presentationSources,                                        sourceIdentifier: sourceIdentifier) { [weak self] error in      self?.filePresentation = nil      if let error { promise.reject(error) } else { promise.resolve(nil) }    }    filePresentation = file    file.present(url: url, title: title, from: presenter)  }   private func dismissFile(identifier: String) {    if filePresentation?.identifier == identifier { filePresentation?.dismiss() }  }   private func shareFile(url: URL, title: String, sourceIdentifier: String, promise: Promise) throws {    guard let presenter = appContext?.utilities?.currentViewController() else {      throw NSError(        domain: "T3NativePresentation",        code: 2,        userInfo: [NSLocalizedDescriptionKey: "The presenting screen is no longer open."]      )    }    try presentFileShare(      url: url,      title: title,      source: presentationSources.view(for: sourceIdentifier),      presenter: presenter,      promise: promise    )  }}