2022-06-24 22:48:57 +00:00
|
|
|
import CoreMedia
|
2021-12-24 19:20:05 +00:00
|
|
|
import Foundation
|
2022-06-24 22:48:57 +00:00
|
|
|
import Siesta
|
2021-12-24 19:20:05 +00:00
|
|
|
|
|
|
|
struct OpenURLHandler {
|
2022-11-12 23:07:23 +00:00
|
|
|
static var firstHandle = true
|
|
|
|
static var shared = OpenURLHandler()
|
2022-06-24 22:48:57 +00:00
|
|
|
static let yatteeProtocol = "yattee://"
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
var accounts: AccountsModel { .shared }
|
|
|
|
var navigation: NavigationModel { .shared }
|
|
|
|
var recents: RecentsModel { .shared }
|
|
|
|
var player: PlayerModel { .shared }
|
|
|
|
var search: SearchModel { .shared }
|
2022-06-30 08:05:32 +00:00
|
|
|
var navigationStyle = NavigationStyle.sidebar
|
2021-12-24 19:20:05 +00:00
|
|
|
|
|
|
|
func handle(_ url: URL) {
|
2022-11-12 23:07:23 +00:00
|
|
|
if Self.firstHandle {
|
|
|
|
Self.firstHandle = false
|
|
|
|
|
|
|
|
Delay.by(1) { Self.shared.handle(url) }
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-12-24 19:20:05 +00:00
|
|
|
if accounts.current.isNil {
|
|
|
|
accounts.setCurrent(accounts.any)
|
|
|
|
}
|
|
|
|
|
|
|
|
guard !accounts.current.isNil else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
#if os(macOS)
|
2022-01-06 15:35:45 +00:00
|
|
|
guard url.host != Windows.player.location else {
|
2021-12-24 19:20:05 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2022-11-12 23:07:23 +00:00
|
|
|
guard let url = urlByReplacingYatteeProtocol(url) else { return }
|
2022-11-10 17:11:28 +00:00
|
|
|
|
2022-11-11 11:27:37 +00:00
|
|
|
let parser = URLParser(url: url)
|
2022-06-24 22:48:57 +00:00
|
|
|
|
|
|
|
switch parser.destination {
|
2022-11-11 11:27:37 +00:00
|
|
|
case .fileURL:
|
|
|
|
handleFileURLOpen(parser)
|
2022-06-24 22:48:57 +00:00
|
|
|
case .video:
|
|
|
|
handleVideoUrlOpen(parser)
|
|
|
|
case .playlist:
|
|
|
|
handlePlaylistUrlOpen(parser)
|
|
|
|
case .channel:
|
|
|
|
handleChannelUrlOpen(parser)
|
|
|
|
case .search:
|
|
|
|
handleSearchUrlOpen(parser)
|
|
|
|
case .favorites:
|
|
|
|
hideViewsAboveBrowser()
|
2022-11-09 13:34:04 +00:00
|
|
|
navigation.tabSelection = .home
|
2022-06-24 22:48:57 +00:00
|
|
|
#if os(macOS)
|
|
|
|
focusMainWindow()
|
|
|
|
#endif
|
|
|
|
case .subscriptions:
|
|
|
|
guard accounts.app.supportsSubscriptions, accounts.signedIn else { return }
|
|
|
|
hideViewsAboveBrowser()
|
|
|
|
navigation.tabSelection = .subscriptions
|
|
|
|
#if os(macOS)
|
|
|
|
focusMainWindow()
|
|
|
|
#endif
|
|
|
|
case .popular:
|
|
|
|
guard accounts.app.supportsPopular else { return }
|
|
|
|
hideViewsAboveBrowser()
|
|
|
|
navigation.tabSelection = .popular
|
|
|
|
#if os(macOS)
|
|
|
|
focusMainWindow()
|
|
|
|
#endif
|
|
|
|
case .trending:
|
|
|
|
hideViewsAboveBrowser()
|
|
|
|
navigation.tabSelection = .trending
|
|
|
|
#if os(macOS)
|
|
|
|
focusMainWindow()
|
|
|
|
#endif
|
|
|
|
default:
|
|
|
|
navigation.presentAlert(title: "Error", message: "This URL could not be opened")
|
2022-08-13 14:16:02 +00:00
|
|
|
#if os(macOS)
|
2022-08-13 14:46:45 +00:00
|
|
|
guard !Windows.main.isOpen else { return }
|
|
|
|
navigation.presentingAlertInVideoPlayer = true
|
2022-08-13 14:16:02 +00:00
|
|
|
#endif
|
2022-06-24 22:48:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func hideViewsAboveBrowser() {
|
|
|
|
player.hide()
|
|
|
|
navigation.presentingChannel = false
|
|
|
|
navigation.presentingPlaylist = false
|
2022-11-10 20:47:27 +00:00
|
|
|
navigation.presentingOpenVideos = false
|
2022-06-24 22:48:57 +00:00
|
|
|
}
|
|
|
|
|
2022-08-13 14:16:02 +00:00
|
|
|
private func urlByReplacingYatteeProtocol(_ url: URL, with urlProtocol: String = "https") -> URL! {
|
2022-06-24 22:48:57 +00:00
|
|
|
var urlAbsoluteString = url.absoluteString
|
2021-12-24 19:20:05 +00:00
|
|
|
|
2022-06-24 22:48:57 +00:00
|
|
|
guard urlAbsoluteString.hasPrefix(Self.yatteeProtocol) else {
|
|
|
|
return url
|
|
|
|
}
|
|
|
|
|
|
|
|
urlAbsoluteString = String(urlAbsoluteString.dropFirst(Self.yatteeProtocol.count))
|
2022-11-11 11:27:37 +00:00
|
|
|
if url.absoluteString.contains("://") {
|
|
|
|
return URL(string: urlAbsoluteString)
|
|
|
|
}
|
2022-06-24 22:48:57 +00:00
|
|
|
|
2022-08-13 14:16:02 +00:00
|
|
|
return URL(string: "\(urlProtocol)://\(urlAbsoluteString)")
|
2022-06-24 22:48:57 +00:00
|
|
|
}
|
|
|
|
|
2022-11-11 11:27:37 +00:00
|
|
|
private func handleFileURLOpen(_ parser: URLParser) {
|
|
|
|
guard let url = parser.fileURL else { return }
|
|
|
|
|
|
|
|
OpenVideosModel.shared.openURLs([url], removeQueueItems: false, playbackMode: .playNow)
|
|
|
|
}
|
|
|
|
|
2022-06-24 22:48:57 +00:00
|
|
|
private func handleVideoUrlOpen(_ parser: URLParser) {
|
2022-11-11 11:27:37 +00:00
|
|
|
guard let id = parser.videoID else {
|
2022-06-24 22:48:57 +00:00
|
|
|
navigation.presentAlert(title: "Could not open video", message: "Could not extract video ID")
|
2021-12-24 19:20:05 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-11 11:27:37 +00:00
|
|
|
guard id != player.currentVideo?.id else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-12-24 19:20:05 +00:00
|
|
|
#if os(macOS)
|
2022-01-06 15:35:45 +00:00
|
|
|
Windows.main.open()
|
2021-12-24 19:20:05 +00:00
|
|
|
#endif
|
|
|
|
|
2022-08-19 21:55:02 +00:00
|
|
|
player.videoBeingOpened = Video(videoID: id)
|
|
|
|
|
2022-08-16 21:16:35 +00:00
|
|
|
player.playerAPI.video(id)
|
2022-06-24 22:48:57 +00:00
|
|
|
.load()
|
|
|
|
.onSuccess { response in
|
|
|
|
if let video: Video = response.typedContent() {
|
|
|
|
let time = parser.time.isNil ? nil : CMTime.secondsInDefaultTimescale(TimeInterval(parser.time!))
|
|
|
|
self.player.playNow(video, at: time)
|
|
|
|
self.player.show()
|
|
|
|
} else {
|
|
|
|
navigation.presentAlert(title: "Error", message: "This video could not be opened")
|
|
|
|
}
|
2021-12-24 19:20:05 +00:00
|
|
|
}
|
2022-06-24 22:48:57 +00:00
|
|
|
.onFailure { responseError in
|
|
|
|
navigation.presentAlert(title: "Could not open video", message: responseError.userMessage)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handlePlaylistUrlOpen(_ parser: URLParser) {
|
|
|
|
#if os(macOS)
|
|
|
|
if alertIfNoMainWindowOpen() { return }
|
|
|
|
#endif
|
|
|
|
|
|
|
|
guard let playlistID = parser.playlistID else {
|
|
|
|
navigation.presentAlert(title: "Could not open playlist", message: "Could not extract playlist ID")
|
|
|
|
return
|
2021-12-24 19:20:05 +00:00
|
|
|
}
|
2022-06-24 22:48:57 +00:00
|
|
|
|
|
|
|
accounts.api.channelPlaylist(playlistID)?
|
|
|
|
.load()
|
|
|
|
.onSuccess { response in
|
|
|
|
if var playlist: ChannelPlaylist = response.typedContent() {
|
|
|
|
playlist.id = playlistID
|
|
|
|
DispatchQueue.main.async {
|
2022-11-24 20:36:05 +00:00
|
|
|
NavigationModel.shared.openChannelPlaylist(
|
2022-06-24 22:48:57 +00:00
|
|
|
playlist,
|
2022-06-30 08:05:32 +00:00
|
|
|
navigationStyle: navigationStyle
|
2022-06-24 22:48:57 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
navigation.presentAlert(title: "Could not open playlist", message: "Playlist could not be found")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onFailure { responseError in
|
|
|
|
navigation.presentAlert(title: "Could not open playlist", message: responseError.userMessage)
|
|
|
|
}
|
2021-12-24 19:20:05 +00:00
|
|
|
}
|
2022-06-24 22:48:57 +00:00
|
|
|
|
|
|
|
private func handleChannelUrlOpen(_ parser: URLParser) {
|
|
|
|
#if os(macOS)
|
|
|
|
if alertIfNoMainWindowOpen() { return }
|
|
|
|
#endif
|
|
|
|
|
|
|
|
guard let resource = resourceForChannelUrl(parser) else {
|
|
|
|
navigation.presentAlert(title: "Could not open channel", message: "Could not extract channel information")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
resource
|
|
|
|
.load()
|
|
|
|
.onSuccess { response in
|
|
|
|
if let channel: Channel = response.typedContent() {
|
|
|
|
DispatchQueue.main.async {
|
2022-11-24 20:36:05 +00:00
|
|
|
NavigationModel.shared.openChannel(
|
2022-06-24 22:48:57 +00:00
|
|
|
channel,
|
2022-06-30 08:05:32 +00:00
|
|
|
navigationStyle: navigationStyle
|
2022-06-24 22:48:57 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
navigation.presentAlert(title: "Could not open channel", message: "Channel could not be found")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onFailure { responseError in
|
|
|
|
navigation.presentAlert(title: "Could not open channel", message: responseError.userMessage)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func resourceForChannelUrl(_ parser: URLParser) -> Resource? {
|
|
|
|
if let id = parser.channelID {
|
|
|
|
return accounts.api.channel(id)
|
|
|
|
}
|
|
|
|
|
2022-06-29 23:31:51 +00:00
|
|
|
if let resource = resourceForUsernameUrl(parser) {
|
|
|
|
return resource
|
|
|
|
}
|
|
|
|
|
2022-06-24 22:48:57 +00:00
|
|
|
guard let name = parser.channelName else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if accounts.app.supportsOpeningChannelsByName {
|
|
|
|
return accounts.api.channelByName(name)
|
|
|
|
}
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
if let instance = InstancesModel.shared.all.first(where: { $0.app.supportsOpeningChannelsByName }) {
|
2022-06-24 22:48:57 +00:00
|
|
|
return instance.anonymous.channelByName(name)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-29 23:31:51 +00:00
|
|
|
private func resourceForUsernameUrl(_ parser: URLParser) -> Resource? {
|
|
|
|
guard let username = parser.username else { return nil }
|
|
|
|
|
|
|
|
if accounts.app.supportsOpeningChannelsByName {
|
|
|
|
return accounts.api.channelByUsername(username)
|
|
|
|
}
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
if let instance = InstancesModel.shared.all.first(where: { $0.app.supportsOpeningChannelsByName }) {
|
2022-06-29 23:31:51 +00:00
|
|
|
return instance.anonymous.channelByUsername(username)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-24 22:48:57 +00:00
|
|
|
private func handleSearchUrlOpen(_ parser: URLParser) {
|
|
|
|
#if os(macOS)
|
|
|
|
if alertIfNoMainWindowOpen() { return }
|
|
|
|
#endif
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
NavigationModel.shared.openSearchQuery(parser.searchQuery)
|
2022-06-24 22:48:57 +00:00
|
|
|
|
|
|
|
#if os(macOS)
|
|
|
|
focusMainWindow()
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
#if os(macOS)
|
|
|
|
private func focusMainWindow() {
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
|
|
Windows.main.focus()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func alertIfNoMainWindowOpen() -> Bool {
|
|
|
|
guard !Windows.main.isOpen else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
navigation.presentAlert(
|
|
|
|
title: "Restart the app to open this link",
|
|
|
|
message:
|
|
|
|
"To open this link in the app you need to close and open it manually to have browser window, " +
|
|
|
|
"then you can try opening links again.\n\nThis is a limitation of SwiftUI on macOS versions earlier than Ventura."
|
|
|
|
)
|
|
|
|
|
|
|
|
navigation.presentingAlertInVideoPlayer = true
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
#endif
|
2021-12-24 19:20:05 +00:00
|
|
|
}
|