mirror of
https://github.com/yattee/yattee.git
synced 2025-12-13 03:28:14 +00:00
Compare commits
14 Commits
v1.4-alpha
...
v1.4-alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
574c58b6e4 | ||
|
|
c1f3fecfe7 | ||
|
|
78834b1548 | ||
|
|
cf68c6c69f | ||
|
|
d7c8dce994 | ||
|
|
c431c20bc0 | ||
|
|
a9b057505c | ||
|
|
93943ecd83 | ||
|
|
a17cbf0085 | ||
|
|
8e74c3ec0a | ||
|
|
cc5f41807b | ||
|
|
a60a2a6744 | ||
|
|
0a36870480 | ||
|
|
48ab8b27c8 |
@@ -3,6 +3,6 @@ import AppKit
|
||||
extension NSTextField {
|
||||
override open var focusRingType: NSFocusRingType {
|
||||
get { .none }
|
||||
set {}
|
||||
set {} // swiftlint:disable:this unused_setter_value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ extension Thumbnail {
|
||||
}
|
||||
|
||||
private static var fixturesHost: String {
|
||||
"https://invidious.snopyta.org"
|
||||
"https://invidious.home.arekf.net"
|
||||
}
|
||||
|
||||
private static func fixtureUrl(videoId: String, quality: Thumbnail.Quality) -> URL {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
extension Video {
|
||||
static var fixtureID: Video.ID = "video-fixture"
|
||||
static var fixtureChannelID: Channel.ID = "channel-fixture"
|
||||
|
||||
static var fixture: Video {
|
||||
let id = "D2sxamzaHkM"
|
||||
let thumbnailURL = "https://yt3.ggpht.com/ytc/AKedOLR-pT_JEsz_hcaA4Gjx8DHcqJ8mS42aTRqcVy6P7w=s88-c-k-c0x00ffffff-no-rj-mo"
|
||||
|
||||
return Video(
|
||||
videoID: fixtureID,
|
||||
title: "Relaxing Piano Music to feel good",
|
||||
videoID: UUID().uuidString,
|
||||
title: "Relaxing Piano Music that will make you feel amazingly good",
|
||||
author: "Fancy Videotuber",
|
||||
length: 582,
|
||||
published: "7 years ago",
|
||||
@@ -17,13 +15,13 @@ extension Video {
|
||||
description: "Some relaxing live piano music",
|
||||
genre: "Music",
|
||||
channel: Channel(
|
||||
id: fixtureChannelID,
|
||||
id: "AbCdEFgHI",
|
||||
name: "The Channel",
|
||||
thumbnailURL: URL(string: thumbnailURL)!,
|
||||
subscriptionsCount: 2300,
|
||||
videos: []
|
||||
),
|
||||
thumbnails: [],
|
||||
thumbnails: Thumbnail.fixturesForAllQualities(videoId: id),
|
||||
live: false,
|
||||
upcoming: false,
|
||||
publishedAt: Date(),
|
||||
|
||||
@@ -53,7 +53,7 @@ final class InstancesModel: ObservableObject {
|
||||
}
|
||||
|
||||
static func remove(_ instance: Instance) {
|
||||
let accounts = Self.accounts(instance.id)
|
||||
let accounts = InstancesModel.accounts(instance.id)
|
||||
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
|
||||
Defaults[.instances].remove(at: index)
|
||||
accounts.forEach { AccountsModel.remove($0) }
|
||||
|
||||
@@ -92,18 +92,15 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> SearchPage in
|
||||
let results = content.json.arrayValue.compactMap { json -> ContentItem? in
|
||||
let results = content.json.arrayValue.compactMap { json -> ContentItem in
|
||||
let type = json.dictionaryValue["type"]?.stringValue
|
||||
|
||||
if type == "channel" {
|
||||
return ContentItem(channel: self.extractChannel(from: json))
|
||||
} else if type == "playlist" {
|
||||
return ContentItem(playlist: self.extractChannelPlaylist(from: json))
|
||||
} else if type == "video" {
|
||||
return ContentItem(video: self.extractVideo(from: json))
|
||||
}
|
||||
|
||||
return nil
|
||||
return ContentItem(video: self.extractVideo(from: json))
|
||||
}
|
||||
|
||||
return SearchPage(results: results, last: results.isEmpty)
|
||||
@@ -160,11 +157,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
private func pathPattern(_ path: String) -> String {
|
||||
"**\(Self.basePath)/\(path)"
|
||||
"**\(InvidiousAPI.basePath)/\(path)"
|
||||
}
|
||||
|
||||
private func basePathAppending(_ path: String) -> String {
|
||||
"\(Self.basePath)/\(path)"
|
||||
"\(InvidiousAPI.basePath)/\(path)"
|
||||
}
|
||||
|
||||
private var cookieHeader: String {
|
||||
@@ -172,11 +169,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
var popular: Resource? {
|
||||
resource(baseURL: account.url, path: "\(Self.basePath)/popular")
|
||||
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/popular")
|
||||
}
|
||||
|
||||
func trending(country: Country, category: TrendingCategory?) -> Resource {
|
||||
resource(baseURL: account.url, path: "\(Self.basePath)/trending")
|
||||
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/trending")
|
||||
.withParam("type", category?.name)
|
||||
.withParam("region", country.rawValue)
|
||||
}
|
||||
@@ -186,7 +183,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
var feed: Resource? {
|
||||
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
|
||||
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/auth/feed")
|
||||
}
|
||||
|
||||
var subscriptions: Resource? {
|
||||
@@ -239,66 +236,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
playlist(playlistID)?.child("videos").child(videoID)
|
||||
}
|
||||
|
||||
func addVideoToPlaylist(
|
||||
_ videoID: String,
|
||||
_ playlistID: String,
|
||||
onFailure: @escaping (RequestError) -> Void = { _ in },
|
||||
onSuccess: @escaping () -> Void = {}
|
||||
) {
|
||||
let resource = playlistVideos(playlistID)
|
||||
let body = ["videoId": videoID]
|
||||
|
||||
resource?
|
||||
.request(.post, json: body)
|
||||
.onSuccess { _ in onSuccess() }
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func removeVideoFromPlaylist(
|
||||
_ index: String,
|
||||
_ playlistID: String,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping () -> Void
|
||||
) {
|
||||
let resource = playlistVideo(playlistID, index)
|
||||
|
||||
resource?
|
||||
.request(.delete)
|
||||
.onSuccess { _ in onSuccess() }
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func playlistForm(
|
||||
_ name: String,
|
||||
_ visibility: String,
|
||||
playlist: Playlist?,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping (Playlist?) -> Void
|
||||
) {
|
||||
let body = ["title": name, "privacy": visibility]
|
||||
let resource = !playlist.isNil ? self.playlist(playlist!.id) : playlists
|
||||
|
||||
resource?
|
||||
.request(!playlist.isNil ? .patch : .post, json: body)
|
||||
.onSuccess { response in
|
||||
if let modifiedPlaylist: Playlist = response.typedContent() {
|
||||
onSuccess(modifiedPlaylist)
|
||||
}
|
||||
}
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func deletePlaylist(
|
||||
_ playlist: Playlist,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping () -> Void
|
||||
) {
|
||||
self.playlist(playlist.id)?
|
||||
.request(.delete)
|
||||
.onSuccess { _ in onSuccess() }
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func channelPlaylist(_ id: String) -> Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("playlists/\(id)"))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Siesta
|
||||
import SwiftyJSON
|
||||
|
||||
final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"]
|
||||
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe"]
|
||||
|
||||
@Published var account: Account!
|
||||
|
||||
@@ -43,11 +43,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
self.extractChannelPlaylist(from: content.json)
|
||||
}
|
||||
|
||||
configureTransformer(pathPattern("user/playlists/create")) { (_: Entity<JSON>) in }
|
||||
configureTransformer(pathPattern("user/playlists/delete")) { (_: Entity<JSON>) in }
|
||||
configureTransformer(pathPattern("user/playlists/add")) { (_: Entity<JSON>) in }
|
||||
configureTransformer(pathPattern("user/playlists/remove")) { (_: Entity<JSON>) in }
|
||||
|
||||
configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
|
||||
self.extractVideo(from: content.json)
|
||||
}
|
||||
@@ -86,10 +81,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
|
||||
}
|
||||
|
||||
configureTransformer(pathPattern("user/playlists")) { (content: Entity<JSON>) -> [Playlist] in
|
||||
content.json.arrayValue.map { self.extractUserPlaylist(from: $0)! }
|
||||
}
|
||||
|
||||
if account.token.isNil {
|
||||
updateToken()
|
||||
}
|
||||
@@ -161,11 +152,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
var signedIn: Bool {
|
||||
guard let account = account else {
|
||||
return false
|
||||
}
|
||||
|
||||
return !account.anonymous && !(account.token?.isEmpty ?? true)
|
||||
!account.anonymous && !(account.token?.isEmpty ?? true)
|
||||
}
|
||||
|
||||
var subscriptions: Resource? {
|
||||
@@ -179,9 +166,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
var home: Resource? { nil }
|
||||
var popular: Resource? { nil }
|
||||
var playlists: Resource? {
|
||||
resource(baseURL: account.instance.apiURL, path: "user/playlists")
|
||||
}
|
||||
var playlists: Resource? { nil }
|
||||
|
||||
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
|
||||
resource(baseURL: account.instance.apiURL, path: "subscribe")
|
||||
@@ -195,79 +180,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
.onCompletion { _ in onCompletion() }
|
||||
}
|
||||
|
||||
func playlist(_ id: String) -> Resource? {
|
||||
channelPlaylist(id)
|
||||
}
|
||||
|
||||
func playlist(_: String) -> Resource? { nil }
|
||||
func playlistVideo(_: String, _: String) -> Resource? { nil }
|
||||
func playlistVideos(_: String) -> Resource? { nil }
|
||||
|
||||
func addVideoToPlaylist(
|
||||
_ videoID: String,
|
||||
_ playlistID: String,
|
||||
onFailure: @escaping (RequestError) -> Void = { _ in },
|
||||
onSuccess: @escaping () -> Void = {}
|
||||
) {
|
||||
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/add")
|
||||
let body = ["videoId": videoID, "playlistId": playlistID]
|
||||
|
||||
resource
|
||||
.request(.post, json: body)
|
||||
.onSuccess { _ in onSuccess() }
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func removeVideoFromPlaylist(
|
||||
_ index: String,
|
||||
_ playlistID: String,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping () -> Void
|
||||
) {
|
||||
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/remove")
|
||||
let body: [String: Any] = ["index": Int(index)!, "playlistId": playlistID]
|
||||
|
||||
resource
|
||||
.request(.post, json: body)
|
||||
.onSuccess { _ in onSuccess() }
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func playlistForm(
|
||||
_ name: String,
|
||||
_: String,
|
||||
playlist: Playlist?,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping (Playlist?) -> Void
|
||||
) {
|
||||
let body = ["name": name]
|
||||
let resource = playlist.isNil ? resource(baseURL: account.instance.apiURL, path: "user/playlists/create") : nil
|
||||
|
||||
resource?
|
||||
.request(.post, json: body)
|
||||
.onSuccess { response in
|
||||
if let modifiedPlaylist: Playlist = response.typedContent() {
|
||||
onSuccess(modifiedPlaylist)
|
||||
} else {
|
||||
onSuccess(nil)
|
||||
}
|
||||
}
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func deletePlaylist(
|
||||
_ playlist: Playlist,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping () -> Void
|
||||
) {
|
||||
let resource = resource(baseURL: account.instance.apiURL, path: "user/playlists/delete")
|
||||
let body = ["playlistId": playlist.id]
|
||||
|
||||
resource
|
||||
.request(.post, json: body)
|
||||
.onSuccess { _ in onSuccess() }
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func comments(_ id: Video.ID, page: String?) -> Resource? {
|
||||
let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)"
|
||||
let resource = resource(baseURL: account.url, path: path)
|
||||
@@ -316,8 +232,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
if let channel = extractChannel(from: content) {
|
||||
return ContentItem(channel: channel)
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -364,7 +278,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
return ChannelPlaylist(
|
||||
id: id,
|
||||
title: details["name"]?.stringValue ?? "",
|
||||
title: details["name"]!.stringValue,
|
||||
thumbnailURL: thumbnailURL,
|
||||
channel: extractChannel(from: json)!,
|
||||
videos: videos,
|
||||
@@ -394,7 +308,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
|
||||
let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url
|
||||
let subscriptionsCount = details["uploaderSubscriberCount"]?.int
|
||||
|
||||
let uploaded = details["uploaded"]?.doubleValue
|
||||
var published = uploaded.isNil ? nil : (uploaded! / 1000).formattedAsRelativeTime()
|
||||
@@ -412,7 +325,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
published: published!,
|
||||
views: details["views"]!.intValue,
|
||||
description: extractDescription(from: content),
|
||||
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
|
||||
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL),
|
||||
thumbnails: thumbnails,
|
||||
live: live,
|
||||
likes: details["likes"]?.int,
|
||||
@@ -444,14 +357,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
)!
|
||||
}
|
||||
|
||||
private func extractUserPlaylist(from json: JSON) -> Playlist? {
|
||||
let id = json["id"].stringValue
|
||||
let title = json["name"].stringValue
|
||||
let visibility = Playlist.Visibility.private
|
||||
|
||||
return Playlist(id: id, title: title, visibility: visibility)
|
||||
}
|
||||
|
||||
private func extractDescription(from content: JSON) -> String? {
|
||||
guard var description = content.dictionaryValue["description"]?.string else {
|
||||
return nil
|
||||
|
||||
@@ -27,34 +27,6 @@ protocol VideosAPI {
|
||||
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
|
||||
func playlistVideos(_ id: String) -> Resource?
|
||||
|
||||
func addVideoToPlaylist(
|
||||
_ videoID: String,
|
||||
_ playlistID: String,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping () -> Void
|
||||
)
|
||||
|
||||
func removeVideoFromPlaylist(
|
||||
_ index: String,
|
||||
_ playlistID: String,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping () -> Void
|
||||
)
|
||||
|
||||
func playlistForm(
|
||||
_ name: String,
|
||||
_ visibility: String,
|
||||
playlist: Playlist?,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping (Playlist?) -> Void
|
||||
)
|
||||
|
||||
func deletePlaylist(
|
||||
_ playlist: Playlist,
|
||||
onFailure: @escaping (RequestError) -> Void,
|
||||
onSuccess: @escaping () -> Void
|
||||
)
|
||||
|
||||
func channelPlaylist(_ id: String) -> Resource?
|
||||
|
||||
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)
|
||||
@@ -102,8 +74,6 @@ extension VideosAPI {
|
||||
case .playlist:
|
||||
urlComponents.path = "/playlist"
|
||||
queryItems.append(.init(name: "list", value: item.playlist.id))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if !time.isNil, time!.seconds.isFinite {
|
||||
|
||||
@@ -32,18 +32,6 @@ enum VideosApp: String, CaseIterable {
|
||||
}
|
||||
|
||||
var supportsUserPlaylists: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
var userPlaylistsEndpointIncludesVideos: Bool {
|
||||
self == .invidious
|
||||
}
|
||||
|
||||
var userPlaylistsHaveVisibility: Bool {
|
||||
self == .invidious
|
||||
}
|
||||
|
||||
var userPlaylistsAreEditable: Bool {
|
||||
self == .invidious
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
struct ContentItem: Identifiable {
|
||||
enum ContentType: String {
|
||||
case video, playlist, channel, placeholder
|
||||
case video, playlist, channel
|
||||
|
||||
private var sortOrder: Int {
|
||||
switch self {
|
||||
@@ -35,6 +35,6 @@ struct ContentItem: Identifiable {
|
||||
}
|
||||
|
||||
var contentType: ContentType {
|
||||
video.isNil ? (channel.isNil ? (playlist.isNil ? .placeholder : .playlist) : .channel) : .video
|
||||
video.isNil ? (channel.isNil ? .playlist : .channel) : .video
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,31 +14,6 @@ final class NavigationModel: ObservableObject {
|
||||
case nowPlaying
|
||||
case search
|
||||
|
||||
var stringValue: String {
|
||||
switch self {
|
||||
case .favorites:
|
||||
return "favorites"
|
||||
case .subscriptions:
|
||||
return "subscriptions"
|
||||
case .popular:
|
||||
return "popular"
|
||||
case .trending:
|
||||
return "trending"
|
||||
case .playlists:
|
||||
return "playlists"
|
||||
case let .channel(string):
|
||||
return "channel\(string)"
|
||||
case let .playlist(string):
|
||||
return "playlist\(string)"
|
||||
case .recentlyOpened:
|
||||
return "recentlyOpened"
|
||||
case .search:
|
||||
return "search"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var playlistID: Playlist.ID? {
|
||||
if case let .playlist(id) = self {
|
||||
return id
|
||||
@@ -74,10 +49,6 @@ final class NavigationModel: ObservableObject {
|
||||
navigationStyle: NavigationStyle,
|
||||
delay: Bool = true
|
||||
) {
|
||||
guard channel.id != Video.fixtureChannelID else {
|
||||
return
|
||||
}
|
||||
|
||||
let recent = RecentItem(from: channel)
|
||||
#if os(macOS)
|
||||
Windows.main.open()
|
||||
@@ -170,12 +141,6 @@ final class NavigationModel: ObservableObject {
|
||||
channelToUnsubscribe = channel
|
||||
presentingUnsubscribeAlert = channelToUnsubscribe != nil
|
||||
}
|
||||
|
||||
func hideKeyboard() {
|
||||
#if os(iOS)
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
typealias TabSelection = NavigationModel.TabSelection
|
||||
|
||||
@@ -36,9 +36,8 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
private(set) var avPlayer = AVPlayer()
|
||||
|
||||
var controller: AppleAVPlayerViewController?
|
||||
var startPictureInPictureOnPlay = false
|
||||
var switchToMPVOnPipClose = false
|
||||
|
||||
private var asset: AVURLAsset?
|
||||
private var composition = AVMutableComposition()
|
||||
@@ -61,13 +60,15 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
addPlayerTimeControlStatusObserver()
|
||||
}
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution _: ResolutionSetting) -> Stream? {
|
||||
func bestPlayable(_ streams: [Stream]) -> Stream? {
|
||||
streams.first { $0.kind == .hls } ??
|
||||
streams.max { $0.resolution < $1.resolution }
|
||||
streams.filter { $0.kind == .adaptive }.max { $0.resolution < $1.resolution } ??
|
||||
streams.first
|
||||
}
|
||||
|
||||
func canPlay(_ stream: Stream) -> Bool {
|
||||
stream.kind == .hls || (stream.kind == .stream && stream.resolution.height <= 720)
|
||||
stream.kind == .hls || stream.kind == .stream || stream.videoFormat == "MPEG_4" ||
|
||||
(stream.videoFormat.starts(with: "video/mp4") && stream.encoding == "h264")
|
||||
}
|
||||
|
||||
func playStream(
|
||||
@@ -182,6 +183,11 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
#endif
|
||||
|
||||
func updateControls() {}
|
||||
func startControlsUpdates() {}
|
||||
func stopControlsUpdates() {}
|
||||
func setNeedsDrawing(_: Bool) {}
|
||||
|
||||
private func loadSingleAsset(
|
||||
_ url: URL,
|
||||
stream: Stream,
|
||||
@@ -323,8 +329,6 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.setRate(self.model.currentRate)
|
||||
}
|
||||
|
||||
let replaceItemAndSeek = {
|
||||
@@ -444,7 +448,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
self,
|
||||
selector: #selector(itemDidPlayToEndTime),
|
||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||
object: model.playerItem
|
||||
object: playerItem
|
||||
)
|
||||
}
|
||||
|
||||
@@ -452,7 +456,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||
object: model.playerItem
|
||||
object: playerItem
|
||||
)
|
||||
}
|
||||
|
||||
@@ -472,9 +476,6 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
model.hide()
|
||||
#endif
|
||||
} else {
|
||||
if model.playingInPictureInPicture {
|
||||
startPictureInPictureOnPlay = true
|
||||
}
|
||||
model.advanceToNextItem()
|
||||
}
|
||||
}
|
||||
@@ -541,26 +542,13 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
if player.timeControlStatus != .waitingToPlayAtSpecifiedRate {
|
||||
if let controller = self.model.pipController {
|
||||
if controller.isPictureInPicturePossible {
|
||||
if self.startPictureInPictureOnPlay {
|
||||
self.startPictureInPictureOnPlay = false
|
||||
DispatchQueue.main.async {
|
||||
self.model.pipController?.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.model.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
if player.timeControlStatus == .playing {
|
||||
if player.rate != self.model.currentRate {
|
||||
player.rate = self.model.currentRate
|
||||
}
|
||||
if player.timeControlStatus == .playing, player.rate != self.model.currentRate {
|
||||
player.rate = self.model.currentRate
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@@ -576,10 +564,4 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateControls() {}
|
||||
func startControlsUpdates() {}
|
||||
func stopControlsUpdates() {}
|
||||
func setNeedsDrawing(_: Bool) {}
|
||||
func setSize(_: Double, _: Double) {}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,7 @@ final class MPVBackend: PlayerBackend {
|
||||
var loadedVideo = false
|
||||
var isLoadingVideo = true { didSet {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.controls.isLoadingVideo = self.isLoadingVideo
|
||||
|
||||
if !self.isLoadingVideo {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
||||
self?.handleEOF = true
|
||||
}
|
||||
}
|
||||
self?.controls.isLoadingVideo = self?.isLoadingVideo ?? true
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -39,12 +29,6 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
updateControlsIsPlaying()
|
||||
|
||||
#if !os(macOS)
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.isIdleTimerDisabled = self.model.presentingPlayer && self.isPlaying
|
||||
}
|
||||
#endif
|
||||
}}
|
||||
var playerItemDuration: CMTime?
|
||||
|
||||
@@ -55,7 +39,6 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
private var clientTimer: RepeatingTimer!
|
||||
|
||||
private var handleEOF = false
|
||||
private var onFileLoaded: (() -> Void)?
|
||||
|
||||
private var controlsUpdates = false
|
||||
@@ -69,43 +52,17 @@ final class MPVBackend: PlayerBackend {
|
||||
clientTimer.eventHandler = getClientUpdates
|
||||
}
|
||||
|
||||
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
|
||||
streams
|
||||
.filter { $0.kind != .hls && $0.resolution <= maxResolution.value }
|
||||
.max { lhs, rhs in
|
||||
let predicates: [AreInIncreasingOrder] = [
|
||||
{ $0.resolution < $1.resolution },
|
||||
{ $0.format > $1.format }
|
||||
]
|
||||
|
||||
for predicate in predicates {
|
||||
if !predicate(lhs, rhs), !predicate(rhs, lhs) {
|
||||
continue
|
||||
}
|
||||
|
||||
return predicate(lhs, rhs)
|
||||
}
|
||||
|
||||
return false
|
||||
} ??
|
||||
func bestPlayable(_ streams: [Stream]) -> Stream? {
|
||||
streams.filter { $0.kind == .adaptive }.max { $0.resolution < $1.resolution } ??
|
||||
streams.first { $0.kind == .hls } ??
|
||||
streams.first
|
||||
}
|
||||
|
||||
func canPlay(_ stream: Stream) -> Bool {
|
||||
stream.resolution != .unknown && stream.format != .av1
|
||||
stream.resolution != .unknown && stream.format != "AV1"
|
||||
}
|
||||
|
||||
func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading _: Bool) {
|
||||
handleEOF = false
|
||||
#if !os(macOS)
|
||||
if model.presentingPlayer {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
}
|
||||
#endif
|
||||
|
||||
let updateCurrentStream = {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.stream = stream
|
||||
@@ -128,7 +85,7 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
if !preservingTime,
|
||||
let segment = self.model.sponsorBlock.segments.first,
|
||||
segment.start > 4,
|
||||
segment.start < 3,
|
||||
self.model.lastSkipped.isNil
|
||||
{
|
||||
self.seek(to: segment.endTime) { finished in
|
||||
@@ -189,20 +146,12 @@ final class MPVBackend: PlayerBackend {
|
||||
} else {
|
||||
replaceItem(nil)
|
||||
}
|
||||
|
||||
startClientUpdates()
|
||||
}
|
||||
|
||||
func play() {
|
||||
isPlaying = true
|
||||
startClientUpdates()
|
||||
|
||||
if controls.presentingControls {
|
||||
startControlsUpdates()
|
||||
}
|
||||
|
||||
setRate(model.currentRate)
|
||||
|
||||
client?.play()
|
||||
}
|
||||
|
||||
@@ -237,8 +186,8 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
}
|
||||
|
||||
func setRate(_ rate: Float) {
|
||||
client?.setDoubleAsync("speed", Double(rate))
|
||||
func setRate(_: Float) {
|
||||
// TODO: Implement rate change
|
||||
}
|
||||
|
||||
func closeItem() {}
|
||||
@@ -271,8 +220,6 @@ final class MPVBackend: PlayerBackend {
|
||||
clientTimer.resume()
|
||||
}
|
||||
|
||||
private var handleSegmentsThrottle = Throttle(interval: 1)
|
||||
|
||||
private func getClientUpdates() {
|
||||
self.logger.info("getting client updates")
|
||||
|
||||
@@ -285,10 +232,8 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
model.updateNowPlayingInfo()
|
||||
|
||||
handleSegmentsThrottle.execute {
|
||||
if let currentTime = currentTime {
|
||||
model.handleSegments(at: currentTime)
|
||||
}
|
||||
if let currentTime = currentTime {
|
||||
model.handleSegments(at: currentTime)
|
||||
}
|
||||
|
||||
timeObserverThrottle.execute {
|
||||
@@ -302,7 +247,7 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
private func updateControlsIsPlaying() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.controls?.isPlaying = self?.isPlaying ?? false
|
||||
self?.controls.isPlaying = self?.isPlaying ?? false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,13 +270,6 @@ final class MPVBackend: PlayerBackend {
|
||||
startClientUpdates()
|
||||
onFileLoaded = nil
|
||||
|
||||
case MPV_EVENT_PLAYBACK_RESTART:
|
||||
isLoadingVideo = false
|
||||
|
||||
onFileLoaded?()
|
||||
startClientUpdates()
|
||||
onFileLoaded = nil
|
||||
|
||||
case MPV_EVENT_UNPAUSE:
|
||||
isLoadingVideo = false
|
||||
|
||||
@@ -346,7 +284,7 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func handleEndOfFile(_: UnsafePointer<mpv_event>!) {
|
||||
guard handleEOF, !isLoadingVideo else {
|
||||
guard !isLoadingVideo else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -367,8 +305,4 @@ final class MPVBackend: PlayerBackend {
|
||||
func setNeedsDrawing(_ needsDrawing: Bool) {
|
||||
client?.setNeedsDrawing(needsDrawing)
|
||||
}
|
||||
|
||||
func setSize(_ width: Double, _ height: Double) {
|
||||
self.client?.setSize(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,23 +168,14 @@ final class MPVClient: ObservableObject {
|
||||
}
|
||||
|
||||
func setSize(_ width: Double, _ height: Double) {
|
||||
let roundedWidth = width.rounded()
|
||||
let roundedHeight = height.rounded()
|
||||
|
||||
guard width > 0, height > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("setting player size to \(roundedWidth),\(roundedHeight)")
|
||||
logger.info("setting player size to \(width),\(height)")
|
||||
#if !os(macOS)
|
||||
guard roundedWidth <= UIScreen.main.bounds.width, roundedHeight <= UIScreen.main.bounds.height else {
|
||||
guard width <= UIScreen.main.bounds.width, height <= UIScreen.main.bounds.height else {
|
||||
logger.info("requested size is greater than screen size, ignoring")
|
||||
logger.info("width: \(roundedWidth) <= \(UIScreen.main.bounds.width)")
|
||||
logger.info("height: \(roundedHeight) <= \(UIScreen.main.bounds.height)")
|
||||
return
|
||||
}
|
||||
|
||||
glView?.frame = CGRect(x: 0, y: 0, width: roundedWidth, height: roundedHeight)
|
||||
glView?.frame = CGRect(x: 0, y: 0, width: width, height: height)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -225,11 +216,6 @@ final class MPVClient: ObservableObject {
|
||||
mpv_set_property_async(mpv, 0, name, MPV_FORMAT_FLAG, &data)
|
||||
}
|
||||
|
||||
func setDoubleAsync(_ name: String, _ value: Double) {
|
||||
var data = value
|
||||
mpv_set_property_async(mpv, 0, name, MPV_FORMAT_DOUBLE, &data)
|
||||
}
|
||||
|
||||
private func getDouble(_ name: String) -> Double {
|
||||
var data = Double()
|
||||
mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &data)
|
||||
|
||||
@@ -16,7 +16,7 @@ protocol PlayerBackend {
|
||||
var isPlaying: Bool { get }
|
||||
var playerItemDuration: CMTime? { get }
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
|
||||
func bestPlayable(_ streams: [Stream]) -> Stream?
|
||||
func canPlay(_ stream: Stream) -> Bool
|
||||
|
||||
func playStream(
|
||||
@@ -50,7 +50,6 @@ protocol PlayerBackend {
|
||||
func stopControlsUpdates()
|
||||
|
||||
func setNeedsDrawing(_ needsDrawing: Bool)
|
||||
func setSize(_ width: Double, _ height: Double)
|
||||
}
|
||||
|
||||
extension PlayerBackend {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import AVKit
|
||||
import Foundation
|
||||
|
||||
final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
||||
var player: PlayerModel!
|
||||
|
||||
func pictureInPictureController(
|
||||
_: AVPictureInPictureController,
|
||||
failedToStartPictureInPictureWithError error: Error
|
||||
) {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
|
||||
func pictureInPictureControllerWillStartPictureInPicture(_: AVPictureInPictureController) {}
|
||||
|
||||
func pictureInPictureControllerDidStartPictureInPicture(_: AVPictureInPictureController) {
|
||||
player?.playingInPictureInPicture = true
|
||||
player?.avPlayerBackend.startPictureInPictureOnPlay = false
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
|
||||
if player?.avPlayerBackend.switchToMPVOnPipClose ?? false {
|
||||
DispatchQueue.main.async { [weak player] in
|
||||
player?.avPlayerBackend.switchToMPVOnPipClose = false
|
||||
player?.saveTime { [weak player] in
|
||||
player?.changeActiveBackend(from: .appleAVPlayer, to: .mpv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player?.playingInPictureInPicture = false
|
||||
}
|
||||
|
||||
func pictureInPictureControllerWillStopPictureInPicture(_: AVPictureInPictureController) {}
|
||||
|
||||
func pictureInPictureController(
|
||||
_: AVPictureInPictureController,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
||||
) {
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class PlayerControlsModel: ObservableObject {
|
||||
@Published var isLoadingVideo = false
|
||||
@Published var isLoadingVideo = true
|
||||
@Published var isPlaying = true
|
||||
@Published var currentTime = CMTime.zero
|
||||
@Published var duration = CMTime.zero
|
||||
@@ -11,8 +11,6 @@ final class PlayerControlsModel: ObservableObject {
|
||||
@Published var timer: Timer?
|
||||
@Published var playingFullscreen = false
|
||||
|
||||
private var throttle = Throttle(interval: 1)
|
||||
|
||||
var player: PlayerModel!
|
||||
|
||||
var playbackTime: String {
|
||||
@@ -54,13 +52,7 @@ final class PlayerControlsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func show() {
|
||||
guard !(player?.currentItem.isNil ?? true) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !presentingControls else {
|
||||
return
|
||||
}
|
||||
player.backend.updateControls()
|
||||
|
||||
withAnimation(PlayerControls.animation) {
|
||||
presentingControls = true
|
||||
@@ -68,36 +60,47 @@ final class PlayerControlsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func hide() {
|
||||
player?.backend.stopControlsUpdates()
|
||||
|
||||
guard !(player?.currentItem.isNil ?? true) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard presentingControls else {
|
||||
return
|
||||
}
|
||||
withAnimation(PlayerControls.animation) {
|
||||
presentingControls = false
|
||||
}
|
||||
}
|
||||
|
||||
func toggle() {
|
||||
if !presentingControls {
|
||||
player.backend.updateControls()
|
||||
}
|
||||
|
||||
withAnimation(PlayerControls.animation) {
|
||||
presentingControls.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
func toggleFullscreen(_ value: Bool) {
|
||||
withAnimation(Animation.easeOut) {
|
||||
resetTimer()
|
||||
withAnimation(PlayerControls.animation) {
|
||||
playingFullscreen = !value
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
if playingFullscreen {
|
||||
guard !(UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true) else {
|
||||
return
|
||||
}
|
||||
Orientation.lockOrientation(.landscape, andRotateTo: .landscapeRight)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
currentTime = .zero
|
||||
duration = .zero
|
||||
}
|
||||
|
||||
func resetTimer() {
|
||||
if !presentingControls {
|
||||
show()
|
||||
}
|
||||
|
||||
removeTimer()
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
|
||||
withAnimation(PlayerControls.animation) { [weak self] in
|
||||
@@ -111,10 +114,4 @@ final class PlayerControlsModel: ObservableObject {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
func update() {
|
||||
throttle.execute { [weak self] in
|
||||
self?.player?.backend.updateControls()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var playerSize: CGSize = .zero { didSet {
|
||||
backend.setSize(playerSize.width, playerSize.height)
|
||||
}}
|
||||
@Published var playerSize: CGSize = .zero
|
||||
@Published var stream: Stream?
|
||||
@Published var currentRate: Float = 1.0 { didSet { backend.setRate(currentRate) } }
|
||||
|
||||
@@ -66,6 +64,8 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
||||
|
||||
@Published var channelWithDetails: Channel?
|
||||
|
||||
#if os(iOS)
|
||||
@Published var motionManager: CMMotionManager!
|
||||
@Published var lockedOrientation: UIInterfaceOrientation?
|
||||
@@ -83,8 +83,6 @@ final class PlayerModel: ObservableObject {
|
||||
var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
|
||||
|
||||
@Published var playingInPictureInPicture = false
|
||||
var pipController: AVPictureInPictureController?
|
||||
var pipDelegate = PiPDelegate()
|
||||
|
||||
@Published var presentingErrorDetails = false
|
||||
var playerError: Error? { didSet {
|
||||
@@ -104,9 +102,6 @@ final class PlayerModel: ObservableObject {
|
||||
#endif
|
||||
|
||||
private var currentArtwork: MPMediaItemArtwork?
|
||||
#if !os(macOS)
|
||||
var playerLayerView: PlayerLayerView!
|
||||
#endif
|
||||
|
||||
init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil, controls: PlayerControlsModel? = nil) {
|
||||
self.accounts = accounts ?? AccountsModel()
|
||||
@@ -134,15 +129,8 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func hide() {
|
||||
controls.playingFullscreen = false
|
||||
presentingPlayer = false
|
||||
playerNavigationLinkActive = false
|
||||
|
||||
#if os(iOS)
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func togglePlayer() {
|
||||
@@ -206,7 +194,7 @@ final class PlayerModel: ObservableObject {
|
||||
backend.pause()
|
||||
}
|
||||
|
||||
func play(_ video: Video, at time: CMTime? = nil, inNavigationView: Bool = false) {
|
||||
func play(_ video: Video, at time: TimeInterval? = nil, inNavigationView: Bool = false) {
|
||||
playNow(video, at: time)
|
||||
|
||||
guard !playingInPictureInPicture else {
|
||||
@@ -234,7 +222,11 @@ final class PlayerModel: ObservableObject {
|
||||
self?.sponsorBlock.loadSegments(
|
||||
videoID: video.videoID,
|
||||
categories: Defaults[.sponsorBlockCategories]
|
||||
)
|
||||
) {
|
||||
if Defaults[.showChannelSubscribers] {
|
||||
self?.loadCurrentItemChannelDetails()
|
||||
}
|
||||
}
|
||||
|
||||
guard Defaults[.enableReturnYouTubeDislike] else {
|
||||
return
|
||||
@@ -300,10 +292,6 @@ final class PlayerModel: ObservableObject {
|
||||
backend.setNeedsDrawing(presentingPlayer)
|
||||
controls.hide()
|
||||
|
||||
#if !os(macOS)
|
||||
UIApplication.shared.isIdleTimerDisabled = presentingPlayer
|
||||
#endif
|
||||
|
||||
if presentingPlayer, closePiPOnOpeningPlayer, playingInPictureInPicture {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.closePiP()
|
||||
@@ -379,6 +367,36 @@ final class PlayerModel: ObservableObject {
|
||||
[activeBackend == PlayerBackendType.mpv ? avPlayerBackend : mpvBackend]
|
||||
}
|
||||
|
||||
func loadCurrentItemChannelDetails() {
|
||||
guard let video = currentVideo,
|
||||
!video.channel.detailsLoaded
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if restoreLoadedChannel() {
|
||||
return
|
||||
}
|
||||
|
||||
accounts.api.channel(video.channel.id).load().onSuccess { [weak self] response in
|
||||
if let channel: Channel = response.typedContent() {
|
||||
self?.channelWithDetails = channel
|
||||
withAnimation {
|
||||
self?.currentItem?.video?.channel = channel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult func restoreLoadedChannel() -> Bool {
|
||||
if !currentVideo.isNil, channelWithDetails?.id == currentVideo!.channel.id {
|
||||
currentItem.video.channel = channelWithDetails!
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func rateLabel(_ rate: Float) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.minimumFractionDigits = 0
|
||||
@@ -387,8 +405,8 @@ final class PlayerModel: ObservableObject {
|
||||
return "\(formatter.string(from: NSNumber(value: rate))!)×"
|
||||
}
|
||||
|
||||
func closeCurrentItem(finished: Bool = false) {
|
||||
prepareCurrentItemForHistory(finished: finished)
|
||||
func closeCurrentItem() {
|
||||
prepareCurrentItemForHistory()
|
||||
currentItem = nil
|
||||
|
||||
backend.closeItem()
|
||||
@@ -503,25 +521,4 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
currentArtwork = MPMediaItemArtwork(boundsSize: image!.size) { _ in image! }
|
||||
}
|
||||
|
||||
func toggleFullscreen(_ isFullScreen: Bool) {
|
||||
controls.resetTimer()
|
||||
|
||||
#if os(macOS)
|
||||
Windows.player.toggleFullScreen()
|
||||
#endif
|
||||
|
||||
controls.playingFullscreen = !isFullScreen
|
||||
|
||||
#if os(iOS)
|
||||
if controls.playingFullscreen {
|
||||
guard !(UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true) else {
|
||||
return
|
||||
}
|
||||
Orientation.lockOrientation(.landscape, andRotateTo: .landscapeRight)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
func playNow(_ video: Video, at time: CMTime? = nil) {
|
||||
func playNow(_ video: Video, at time: TimeInterval? = nil) {
|
||||
if playingInPictureInPicture, closePiPOnNavigation {
|
||||
closePiP()
|
||||
}
|
||||
@@ -54,7 +54,7 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: CMTime? = nil) {
|
||||
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) {
|
||||
if !playingInPictureInPicture {
|
||||
backend.closeItem()
|
||||
}
|
||||
@@ -64,7 +64,7 @@ extension PlayerModel {
|
||||
currentItem = item
|
||||
|
||||
if !time.isNil {
|
||||
currentItem.playbackTime = time
|
||||
currentItem.playbackTime = .secondsInDefaultTimescale(time!)
|
||||
} else if currentItem.playbackTime.isNil {
|
||||
currentItem.playbackTime = .zero
|
||||
}
|
||||
@@ -74,6 +74,7 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
preservedTime = currentItem.playbackTime
|
||||
restoreLoadedChannel()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let video = self?.currentVideo else {
|
||||
@@ -94,7 +95,13 @@ extension PlayerModel {
|
||||
|
||||
streams = streams.filter { backend.canPlay($0) }
|
||||
|
||||
return backend.bestPlayable(streams, maxResolution: quality)
|
||||
switch quality {
|
||||
case .best:
|
||||
return backend.bestPlayable(streams)
|
||||
default:
|
||||
let sorted = streams.filter { $0.kind != .hls }.sorted { $0.resolution > $1.resolution }.sorted { $0.kind < $1.kind }
|
||||
return sorted.first(where: { $0.resolution.height <= quality.value.height })
|
||||
}
|
||||
}
|
||||
|
||||
func advanceToNextItem() {
|
||||
@@ -105,12 +112,13 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
func advanceToItem(_ newItem: PlayerQueueItem, at time: CMTime? = nil) {
|
||||
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
||||
prepareCurrentItemForHistory()
|
||||
|
||||
remove(newItem)
|
||||
|
||||
currentItem = newItem
|
||||
pause()
|
||||
|
||||
accounts.api.loadDetails(newItem) { newItem in
|
||||
self.playItem(newItem, video: newItem.video, at: time)
|
||||
@@ -151,6 +159,7 @@ extension PlayerModel {
|
||||
if play {
|
||||
currentItem = item
|
||||
// pause playing current video as it's going to be replaced with next one
|
||||
pause()
|
||||
}
|
||||
|
||||
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
||||
@@ -175,8 +184,8 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
func playHistory(_ item: PlayerQueueItem, at time: CMTime? = nil) {
|
||||
var time = time ?? item.playbackTime
|
||||
func playHistory(_ item: PlayerQueueItem) {
|
||||
var time = item.playbackTime
|
||||
|
||||
if item.shouldRestartPlaying {
|
||||
time = .zero
|
||||
@@ -196,17 +205,8 @@ extension PlayerModel {
|
||||
return
|
||||
}
|
||||
|
||||
var restoredQueue = [PlayerQueueItem?]()
|
||||
|
||||
if let lastPlayed = Defaults[.lastPlayed],
|
||||
!Defaults[.queue].contains(where: { $0.videoID == lastPlayed.videoID })
|
||||
{
|
||||
restoredQueue.append(lastPlayed)
|
||||
Defaults[.lastPlayed] = nil
|
||||
}
|
||||
|
||||
restoredQueue.append(contentsOf: Defaults[.queue])
|
||||
queue = restoredQueue.compactMap { $0 }
|
||||
queue = ([Defaults[.lastPlayed]] + Defaults[.queue]).compactMap { $0 }
|
||||
Defaults[.lastPlayed] = nil
|
||||
|
||||
queue.forEach { item in
|
||||
accounts.api.loadDetails(item) { newItem in
|
||||
|
||||
@@ -50,8 +50,7 @@ extension PlayerModel {
|
||||
private func shouldSkip(_ segment: Segment, at time: CMTime) -> Bool {
|
||||
guard isPlaying,
|
||||
!restoredSegments.contains(segment),
|
||||
Defaults[.sponsorBlockCategories].contains(segment.category),
|
||||
segment.start > 4
|
||||
Defaults[.sponsorBlockCategories].contains(segment.category)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ extension PlayerModel {
|
||||
|
||||
func rebuildTVMenu() {
|
||||
#if os(tvOS)
|
||||
avPlayerBackend.controller?.playerView.transportBarCustomMenuItems = [
|
||||
controller?.playerView.transportBarCustomMenuItems = [
|
||||
restoreLastSkippedSegmentAction,
|
||||
rateMenu,
|
||||
streamsMenu
|
||||
|
||||
@@ -18,11 +18,11 @@ struct Playlist: Identifiable, Equatable, Hashable {
|
||||
var title: String
|
||||
var visibility: Visibility
|
||||
|
||||
var updated: TimeInterval?
|
||||
var updated: TimeInterval
|
||||
|
||||
var videos = [Video]()
|
||||
|
||||
init(id: String, title: String, visibility: Visibility, updated: TimeInterval? = nil, videos: [Video] = []) {
|
||||
init(id: String, title: String, visibility: Visibility, updated: TimeInterval, videos: [Video] = []) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.visibility = visibility
|
||||
|
||||
@@ -4,7 +4,6 @@ import SwiftUI
|
||||
|
||||
final class PlaylistsModel: ObservableObject {
|
||||
@Published var playlists = [Playlist]()
|
||||
@Published var reloadPlaylists = false
|
||||
|
||||
var accounts = AccountsModel()
|
||||
|
||||
@@ -59,20 +58,24 @@ final class PlaylistsModel: ObservableObject {
|
||||
onSuccess: @escaping () -> Void = {},
|
||||
onFailure: @escaping (RequestError) -> Void = { _ in }
|
||||
) {
|
||||
accounts.api.addVideoToPlaylist(videoID, playlistID, onFailure: onFailure) {
|
||||
self.load(force: true) {
|
||||
self.reloadPlaylists.toggle()
|
||||
let resource = accounts.api.playlistVideos(playlistID)
|
||||
let body = ["videoId": videoID]
|
||||
|
||||
resource?
|
||||
.request(.post, json: body)
|
||||
.onSuccess { _ in
|
||||
self.load(force: true)
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
.onFailure(onFailure)
|
||||
}
|
||||
|
||||
func removeVideo(index: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) {
|
||||
accounts.api.removeVideoFromPlaylist(index, playlistID, onFailure: { _ in }) {
|
||||
self.load(force: true) {
|
||||
self.reloadPlaylists.toggle()
|
||||
onSuccess()
|
||||
}
|
||||
func removeVideo(videoIndexID: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) {
|
||||
let resource = accounts.api.playlistVideo(playlistID, videoIndexID)
|
||||
|
||||
resource?.request(.delete).onSuccess { _ in
|
||||
self.load(force: true)
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ final class SearchModel: ObservableObject {
|
||||
|
||||
resource?.removeObservers(ownedBy: store)
|
||||
|
||||
resource = accounts.api.search(query, page: pageToLoad.nextPage)
|
||||
resource = accounts.api.search(query, page: page?.nextPage)
|
||||
resource.addObserver(store)
|
||||
|
||||
resource
|
||||
|
||||
@@ -13,7 +13,7 @@ final class SponsorBlockAPI: ObservableObject {
|
||||
@Published var segments = [Segment]()
|
||||
|
||||
static func categoryDescription(_ name: String) -> String? {
|
||||
guard Self.categories.contains(name) else {
|
||||
guard SponsorBlockAPI.categories.contains(name) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ final class SponsorBlockAPI: ObservableObject {
|
||||
}
|
||||
|
||||
static func categoryDetails(_ name: String) -> String? {
|
||||
guard Self.categories.contains(name) else {
|
||||
guard SponsorBlockAPI.categories.contains(name) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,29 +5,7 @@ import Foundation
|
||||
// swiftlint:disable:next final_class
|
||||
class Stream: Equatable, Hashable, Identifiable {
|
||||
enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable {
|
||||
case hd4320p60
|
||||
case hd4320p
|
||||
case hd2160p60
|
||||
case hd2160p50
|
||||
case hd2160p48
|
||||
case hd2160p
|
||||
case hd1440p60
|
||||
case hd1440p50
|
||||
case hd1440p48
|
||||
case hd1440p
|
||||
case hd1080p60
|
||||
case hd1080p50
|
||||
case hd1080p48
|
||||
case hd1080p
|
||||
case hd720p60
|
||||
case hd720p50
|
||||
case hd720p48
|
||||
case hd720p
|
||||
case sd480p
|
||||
case sd360p
|
||||
case sd240p
|
||||
case sd144p
|
||||
case unknown
|
||||
case hd2160p, hd1440p60, hd1440p, hd1080p60, hd1080p, hd720p60, hd720p, sd480p, sd360p, sd240p, sd144p, unknown
|
||||
|
||||
var name: String {
|
||||
"\(height)p\(refreshRate != -1 ? ", \(refreshRate) fps" : "")"
|
||||
@@ -79,49 +57,6 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
enum Format: String, Comparable {
|
||||
case webm
|
||||
case avc1
|
||||
case av1
|
||||
case mp4
|
||||
case unknown
|
||||
|
||||
private var sortOrder: Int {
|
||||
switch self {
|
||||
case .webm:
|
||||
return 0
|
||||
case .mp4:
|
||||
return 1
|
||||
case .avc1:
|
||||
return 2
|
||||
case .av1:
|
||||
return 3
|
||||
case .unknown:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
static func < (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.sortOrder < rhs.sortOrder
|
||||
}
|
||||
|
||||
static func from(_ string: String) -> Self {
|
||||
let lowercased = string.lowercased()
|
||||
|
||||
if lowercased.contains("webm") {
|
||||
return .webm
|
||||
} else if lowercased.contains("avc1") {
|
||||
return .avc1
|
||||
} else if lowercased.contains("av01") {
|
||||
return .av1
|
||||
} else if lowercased.contains("mpeg_4") || lowercased.contains("mp4") {
|
||||
return .mp4
|
||||
} else {
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id = UUID()
|
||||
|
||||
var instance: Instance!
|
||||
@@ -131,7 +66,6 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
|
||||
var resolution: Resolution!
|
||||
var kind: Kind!
|
||||
var format: Format!
|
||||
|
||||
var encoding: String!
|
||||
var videoFormat: String!
|
||||
@@ -153,7 +87,7 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
self.resolution = resolution
|
||||
self.kind = kind
|
||||
self.encoding = encoding
|
||||
format = .from(videoFormat ?? "")
|
||||
self.videoFormat = videoFormat
|
||||
}
|
||||
|
||||
var quality: String {
|
||||
@@ -164,8 +98,23 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
|
||||
}
|
||||
|
||||
var format: String {
|
||||
let lowercasedFormat = (videoFormat ?? "unknown").lowercased()
|
||||
if lowercasedFormat.contains("webm") {
|
||||
return "WEBM"
|
||||
} else if lowercasedFormat.contains("avc1") {
|
||||
return "avc1"
|
||||
} else if lowercasedFormat.contains("av01") {
|
||||
return "AV1"
|
||||
} else if lowercasedFormat.contains("mpeg_4") || lowercasedFormat.contains("mp4") {
|
||||
return "MP4"
|
||||
} else {
|
||||
return lowercasedFormat
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
let formatString = format == .unknown ? "" : " (\(format.rawValue))"
|
||||
let formatString = format == "unknown" ? "" : " (\(format))"
|
||||
return "\(quality)\(formatString) - \(instance?.description ?? "")"
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
var genre: String?
|
||||
|
||||
// index used when in the Playlist
|
||||
var indexID: String?
|
||||
let indexID: String?
|
||||
|
||||
var live: Bool
|
||||
var upcoming: Bool
|
||||
|
||||
10
README.md
10
README.md
@@ -19,18 +19,13 @@
|
||||
* Fullscreen playback, Picture in Picture and AirPlay support
|
||||
* Stream quality selection
|
||||
|
||||
### Features in alpha testing
|
||||
* New player component with custom controls, gestures and support for 4K playback
|
||||
|
||||
You can leave your feedback in [discussion on v1.4 release](https://github.com/yattee/yattee/discussions/93) or join [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat. Thanks!
|
||||
|
||||
### Availability
|
||||
|| Invidious | Piped |
|
||||
| Feature | Invidious | Piped |
|
||||
| - | - | - |
|
||||
| User Accounts | ✅ | ✅ |
|
||||
| Subscriptions | ✅ | ✅ |
|
||||
| Popular | ✅ | 🔴 |
|
||||
| User Playlists | ✅ | ✅ |
|
||||
| User Playlists | ✅ | 🔴 |
|
||||
| Trending | ✅ | ✅ |
|
||||
| Channels | ✅ | ✅ |
|
||||
| Channel Playlists | ✅ | ✅ |
|
||||
@@ -53,7 +48,6 @@ You can browse and use accounts from one app and play videos with another (for e
|
||||
## Contributing
|
||||
If you're interestred in contributing, you can browse the [issues](https://github.com/yattee/yattee/issues) list or create a new one to discuss your feature idea. Every contribution is very welcome.
|
||||
|
||||
Join [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat if you need an advice or want to discuss the project.
|
||||
## License and Liability
|
||||
|
||||
Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
|
||||
|
||||
@@ -51,6 +51,7 @@ extension Defaults.Keys {
|
||||
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
||||
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
||||
static let showHistoryInPlayer = Key<Bool>("showHistoryInPlayer", default: false)
|
||||
static let showChannelSubscribers = Key<Bool>("showChannelSubscribers", default: true)
|
||||
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: kavinPipedInstanceID)
|
||||
#if !os(tvOS)
|
||||
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
||||
@@ -95,26 +96,12 @@ extension Defaults.Keys {
|
||||
}
|
||||
|
||||
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
||||
case best
|
||||
case hd4320p60
|
||||
case hd4320p
|
||||
case hd2160p60
|
||||
case hd2160p
|
||||
case hd1440p60
|
||||
case hd1440p
|
||||
case hd1080p60
|
||||
case hd1080p
|
||||
case hd720p60
|
||||
case hd720p
|
||||
case sd480p
|
||||
case sd360p
|
||||
case sd240p
|
||||
case sd144p
|
||||
case best, hd720p, sd480p, sd360p, sd240p, sd144p
|
||||
|
||||
var value: Stream.Resolution {
|
||||
switch self {
|
||||
case .best:
|
||||
return .hd4320p60
|
||||
return .hd720p
|
||||
default:
|
||||
return Stream.Resolution(rawValue: rawValue)!
|
||||
}
|
||||
@@ -124,14 +111,6 @@ enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
||||
switch self {
|
||||
case .best:
|
||||
return "Best available quality"
|
||||
case .hd4320p60:
|
||||
return "8K, 60fps"
|
||||
case .hd4320p:
|
||||
return "8K"
|
||||
case .hd2160p60:
|
||||
return "4K, 60fps"
|
||||
case .hd2160p:
|
||||
return "4K"
|
||||
default:
|
||||
return value.name
|
||||
}
|
||||
|
||||
@@ -11,18 +11,10 @@ struct DropFavorite: DropDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
guard let current = current else {
|
||||
return
|
||||
}
|
||||
let from = favorites.firstIndex(of: current!)!
|
||||
let to = favorites.firstIndex(of: item)!
|
||||
|
||||
let from = favorites.firstIndex(of: current)
|
||||
let to = favorites.firstIndex(of: item)
|
||||
|
||||
guard let from = from, let to = to else {
|
||||
return
|
||||
}
|
||||
|
||||
guard favorites[to].id != current.id else {
|
||||
guard favorites[to].id != current!.id else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -12,29 +12,29 @@ struct MenuCommands: Commands {
|
||||
private var navigationMenu: some Commands {
|
||||
CommandGroup(before: .windowSize) {
|
||||
Button("Favorites") {
|
||||
setTabSelection(.favorites)
|
||||
model.navigation?.tabSelection = .favorites
|
||||
}
|
||||
.keyboardShortcut("1")
|
||||
|
||||
Button("Subscriptions") {
|
||||
setTabSelection(.subscriptions)
|
||||
model.navigation?.tabSelection = .subscriptions
|
||||
}
|
||||
.disabled(subscriptionsDisabled)
|
||||
.keyboardShortcut("2")
|
||||
|
||||
Button("Popular") {
|
||||
setTabSelection(.popular)
|
||||
model.navigation?.tabSelection = .popular
|
||||
}
|
||||
.disabled(!(model.accounts?.app.supportsPopular ?? false))
|
||||
.keyboardShortcut("3")
|
||||
|
||||
Button("Trending") {
|
||||
setTabSelection(.trending)
|
||||
model.navigation?.tabSelection = .trending
|
||||
}
|
||||
.keyboardShortcut("4")
|
||||
|
||||
Button("Search") {
|
||||
setTabSelection(.search)
|
||||
model.navigation?.tabSelection = .search
|
||||
}
|
||||
.keyboardShortcut("f")
|
||||
|
||||
@@ -42,15 +42,6 @@ struct MenuCommands: Commands {
|
||||
}
|
||||
}
|
||||
|
||||
private func setTabSelection(_ tabSelection: NavigationModel.TabSelection) {
|
||||
guard let navigation = model.navigation else {
|
||||
return
|
||||
}
|
||||
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
navigation.tabSelection = tabSelection
|
||||
}
|
||||
|
||||
private var subscriptionsDisabled: Bool {
|
||||
!(
|
||||
(model.accounts?.app.supportsSubscriptions ?? false) && model.accounts?.signedIn ?? false
|
||||
|
||||
@@ -14,7 +14,6 @@ struct AppSidebarNavigation: View {
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
@@ -53,7 +52,7 @@ struct AppSidebarNavigation: View {
|
||||
BrowserPlayerControls {
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
Image(systemName: "4k.tv")
|
||||
Image(systemName: "play.tv")
|
||||
.renderingMode(.original)
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.accentColor)
|
||||
@@ -72,7 +71,6 @@ struct AppSidebarNavigation: View {
|
||||
.environmentObject(instances)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(player)
|
||||
.environmentObject(playerControls)
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(recents)
|
||||
.environmentObject(subscriptions)
|
||||
|
||||
@@ -11,7 +11,9 @@ struct AppSidebarPlaylists: View {
|
||||
NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) {
|
||||
LazyView(PlaylistVideosView(playlist))
|
||||
} label: {
|
||||
playlistLabel(playlist)
|
||||
Label(playlist.title, systemImage: RecentsModel.symbolSystemImage(playlist.title))
|
||||
.backport
|
||||
.badge(Text("\(playlist.videos.count)"))
|
||||
}
|
||||
.id(playlist.id)
|
||||
.contextMenu {
|
||||
@@ -32,18 +34,6 @@ struct AppSidebarPlaylists: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func playlistLabel(_ playlist: Playlist) -> some View {
|
||||
let label = Label(playlist.title, systemImage: RecentsModel.symbolSystemImage(playlist.title))
|
||||
|
||||
if player.accounts.app.userPlaylistsEndpointIncludesVideos {
|
||||
label
|
||||
.backport
|
||||
.badge(Text("\(playlist.videos.count)"))
|
||||
} else {
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
var newPlaylistButton: some View {
|
||||
Button(action: { navigation.presentNewPlaylistForm() }) {
|
||||
Label("New Playlist", systemImage: "plus.circle")
|
||||
|
||||
@@ -45,7 +45,6 @@ struct Sidebar: View {
|
||||
Label("Favorites", systemImage: "heart")
|
||||
.accessibility(label: Text("Favorites"))
|
||||
}
|
||||
.id("favorites")
|
||||
}
|
||||
if visibleSections.contains(.subscriptions),
|
||||
accounts.app.supportsSubscriptions && accounts.signedIn
|
||||
@@ -54,7 +53,6 @@ struct Sidebar: View {
|
||||
Label("Subscriptions", systemImage: "star.circle")
|
||||
.accessibility(label: Text("Subscriptions"))
|
||||
}
|
||||
.id("subscriptions")
|
||||
}
|
||||
|
||||
if visibleSections.contains(.popular), accounts.app.supportsPopular {
|
||||
@@ -62,7 +60,6 @@ struct Sidebar: View {
|
||||
Label("Popular", systemImage: "arrow.up.right.circle")
|
||||
.accessibility(label: Text("Popular"))
|
||||
}
|
||||
.id("popular")
|
||||
}
|
||||
|
||||
if visibleSections.contains(.trending) {
|
||||
@@ -70,14 +67,12 @@ struct Sidebar: View {
|
||||
Label("Trending", systemImage: "chart.bar")
|
||||
.accessibility(label: Text("Trending"))
|
||||
}
|
||||
.id("trending")
|
||||
}
|
||||
|
||||
NavigationLink(destination: LazyView(SearchView()), tag: TabSelection.search, selection: $navigation.tabSelection) {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
.accessibility(label: Text("Search"))
|
||||
}
|
||||
.id("search")
|
||||
.keyboardShortcut("f")
|
||||
}
|
||||
}
|
||||
@@ -85,12 +80,8 @@ struct Sidebar: View {
|
||||
private func scrollScrollViewToItem(scrollView: ScrollViewProxy, for selection: TabSelection) {
|
||||
if case .recentlyOpened = selection {
|
||||
scrollView.scrollTo("recentlyOpened")
|
||||
return
|
||||
} else if case let .playlist(id) = selection {
|
||||
scrollView.scrollTo(id)
|
||||
return
|
||||
}
|
||||
|
||||
scrollView.scrollTo(selection.stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import AVKit
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct AppleAVPlayerView: UIViewRepresentable {
|
||||
struct AppleAVPlayerView: UIViewControllerRepresentable {
|
||||
@EnvironmentObject<CommentsModel> private var comments
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
func makeUIView(context _: Context) -> some UIView {
|
||||
player.playerLayerView = PlayerLayerView(frame: .zero)
|
||||
return player.playerLayerView
|
||||
func makeUIViewController(context _: Context) -> UIViewController {
|
||||
let controller = AppleAVPlayerViewController()
|
||||
|
||||
controller.commentsModel = comments
|
||||
controller.navigationModel = navigation
|
||||
controller.playerModel = player
|
||||
controller.subscriptionsModel = subscriptions
|
||||
player.avPlayerBackend.controller = controller
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIView(_: UIViewType, context _: Context) {}
|
||||
func updateUIViewController(_: UIViewController, context _: Context) {
|
||||
player.rebuildTVMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,6 @@ struct PlayerControls: View {
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
#elseif os(tvOS)
|
||||
enum Field: Hashable {
|
||||
case play
|
||||
case backward
|
||||
case forward
|
||||
}
|
||||
|
||||
@FocusState private var focusedField: Field?
|
||||
#endif
|
||||
|
||||
init(player: PlayerModel) {
|
||||
@@ -29,18 +21,14 @@ struct PlayerControls: View {
|
||||
ZStack(alignment: .bottom) {
|
||||
VStack(spacing: 0) {
|
||||
Group {
|
||||
HStack {
|
||||
statusBar
|
||||
.padding(3)
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBlur(material: .hudWindow))
|
||||
#elseif os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||
#endif
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
statusBar
|
||||
.padding(3)
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBlur(material: .hudWindow))
|
||||
#elseif os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||
#endif
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
|
||||
buttonsBar
|
||||
.padding(.top, 4)
|
||||
@@ -69,19 +57,13 @@ struct PlayerControls: View {
|
||||
}
|
||||
.opacity(model.presentingControls ? 1 : 0)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.onChange(of: model.presentingControls) { _ in
|
||||
if model.presentingControls {
|
||||
focusedField = .play
|
||||
}
|
||||
}
|
||||
.onChange(of: focusedField) { _ in
|
||||
model.resetTimer()
|
||||
}
|
||||
#else
|
||||
.background(PlayerGestures())
|
||||
#endif
|
||||
.environment(\.colorScheme, .dark)
|
||||
.background(controlsBackground)
|
||||
.environment(\.colorScheme, .dark)
|
||||
}
|
||||
|
||||
var controlsBackground: some View {
|
||||
PlayerGestures()
|
||||
.background(Color.black.opacity(model.presentingControls ? 0.5 : 0))
|
||||
}
|
||||
|
||||
var timeline: some View {
|
||||
@@ -109,18 +91,13 @@ struct PlayerControls: View {
|
||||
#endif
|
||||
Text(playbackStatus)
|
||||
|
||||
Spacer()
|
||||
|
||||
ToggleBackendButton()
|
||||
Text("•")
|
||||
|
||||
#if !os(tvOS)
|
||||
ToggleBackendButton()
|
||||
Text("•")
|
||||
|
||||
StreamControl()
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 300)
|
||||
#endif
|
||||
#else
|
||||
Text(player.stream?.description ?? "")
|
||||
StreamControl()
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 160)
|
||||
#endif
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
@@ -134,9 +111,7 @@ struct PlayerControls: View {
|
||||
} label: {
|
||||
Image(systemName: "chevron.down.circle.fill")
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var playbackStatus: String {
|
||||
@@ -171,15 +146,8 @@ struct PlayerControls: View {
|
||||
|
||||
var buttonsBar: some View {
|
||||
HStack {
|
||||
#if !os(tvOS)
|
||||
fullscreenButton
|
||||
#if os(iOS)
|
||||
pipButton
|
||||
#endif
|
||||
rateButton
|
||||
|
||||
Spacer()
|
||||
#endif
|
||||
fullscreenButton
|
||||
Spacer()
|
||||
// button("Music Mode", systemImage: "music.note")
|
||||
}
|
||||
}
|
||||
@@ -189,90 +157,17 @@ struct PlayerControls: View {
|
||||
"Fullscreen",
|
||||
systemImage: fullScreenLayout ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right"
|
||||
) {
|
||||
player.toggleFullscreen(fullScreenLayout)
|
||||
model.toggleFullscreen(fullScreenLayout)
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(fullScreenLayout ? .cancelAction : .defaultAction)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder private var rateButton: some View {
|
||||
#if os(macOS)
|
||||
ratePicker
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: 70)
|
||||
#elseif os(iOS)
|
||||
Menu {
|
||||
ratePicker
|
||||
.frame(width: 45, height: 30)
|
||||
#if os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||
#endif
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
} label: {
|
||||
Text(player.rateLabel(player.currentRate))
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.primary)
|
||||
.frame(width: 50, height: 30)
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBlur(material: .hudWindow))
|
||||
#elseif os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||
#endif
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
#endif
|
||||
}
|
||||
|
||||
var ratePicker: some View {
|
||||
Picker("Rate", selection: rateBinding) {
|
||||
ForEach(PlayerModel.availableRates, id: \.self) { rate in
|
||||
Text(player.rateLabel(rate)).tag(rate)
|
||||
}
|
||||
}
|
||||
.transaction { t in t.animation = .none }
|
||||
}
|
||||
|
||||
private var rateBinding: Binding<Float> {
|
||||
.init(get: { player.currentRate }, set: { rate in player.currentRate = rate })
|
||||
}
|
||||
|
||||
private var pipButton: some View {
|
||||
button("PiP", systemImage: "pip") {
|
||||
if player.activeBackend == .mpv {
|
||||
player.avPlayerBackend.switchToMPVOnPipClose = true
|
||||
}
|
||||
|
||||
if player.activeBackend != PlayerBackendType.appleAVPlayer {
|
||||
player.saveTime {
|
||||
player.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
print(player.pipController?.isPictureInPicturePossible ?? false ? "possible" : "NOT possible")
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = true
|
||||
player.pipController?.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mediumButtonsBar: some View {
|
||||
HStack {
|
||||
#if !os(tvOS)
|
||||
button("Seek Backward", systemImage: "gobackward.10", size: 50, cornerRadius: 10) {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .backward)
|
||||
#else
|
||||
.keyboardShortcut("k", modifiers: [])
|
||||
.keyboardShortcut(KeyEquivalent.leftArrow, modifiers: [])
|
||||
#endif
|
||||
|
||||
#endif
|
||||
button("Seek Backward", systemImage: "gobackward.10", size: 50, cornerRadius: 10) {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||
}
|
||||
.keyboardShortcut("k")
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -284,27 +179,15 @@ struct PlayerControls: View {
|
||||
) {
|
||||
player.backend.togglePlay()
|
||||
}
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .play)
|
||||
#else
|
||||
.keyboardShortcut("p")
|
||||
.keyboardShortcut(.space)
|
||||
#endif
|
||||
.disabled(model.isLoadingVideo)
|
||||
|
||||
Spacer()
|
||||
|
||||
#if !os(tvOS)
|
||||
button("Seek Forward", systemImage: "goforward.10", size: 50, cornerRadius: 10) {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||
}
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .forward)
|
||||
#else
|
||||
.keyboardShortcut("l", modifiers: [])
|
||||
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
|
||||
#endif
|
||||
#endif
|
||||
button("Seek Forward", systemImage: "goforward.10", size: 50, cornerRadius: 10) {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||
}
|
||||
.keyboardShortcut("l")
|
||||
}
|
||||
.font(.system(size: 30))
|
||||
.padding(.horizontal, 4)
|
||||
@@ -351,7 +234,7 @@ struct PlayerControls: View {
|
||||
}
|
||||
|
||||
var fullScreenLayout: Bool {
|
||||
#if os(iOS)
|
||||
#if !os(macOS)
|
||||
model.playingFullscreen || verticalSizeClass == .compact
|
||||
#else
|
||||
model.playingFullscreen
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import GLKit
|
||||
import Logging
|
||||
import OpenGLES
|
||||
|
||||
final class MPVOGLView: GLKView {
|
||||
private var logger = Logger(label: "stream.yattee.mpv.oglview")
|
||||
private var defaultFBO: GLint?
|
||||
|
||||
var mpvGL: UnsafeMutableRawPointer?
|
||||
var needsDrawing = true
|
||||
|
||||
override init(frame: CGRect) {
|
||||
guard let context = EAGLContext(api: .openGLES3) else {
|
||||
guard let context = EAGLContext(api: .openGLES2) else {
|
||||
print("Failed to initialize OpenGLES 2.0 context")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
logger.info("frame size: \(frame.width) x \(frame.height)")
|
||||
|
||||
super.init(frame: frame, context: context)
|
||||
contentMode = .redraw
|
||||
|
||||
@@ -37,17 +33,14 @@ final class MPVOGLView: GLKView {
|
||||
glClear(UInt32(GL_COLOR_BUFFER_BIT))
|
||||
}
|
||||
|
||||
override func draw(_: CGRect) {
|
||||
override func draw(_ rect: CGRect) {
|
||||
glGetIntegerv(UInt32(GL_FRAMEBUFFER_BINDING), &defaultFBO!)
|
||||
|
||||
var dims: [GLint] = [0, 0, 0, 0]
|
||||
glGetIntegerv(GLenum(GL_VIEWPORT), &dims)
|
||||
|
||||
if mpvGL != nil {
|
||||
var data = mpv_opengl_fbo(
|
||||
fbo: Int32(defaultFBO!),
|
||||
w: Int32(dims[2]),
|
||||
h: Int32(dims[3]),
|
||||
w: Int32(rect.size.width) * Int32(contentScaleFactor),
|
||||
h: Int32(rect.size.height) * Int32(contentScaleFactor),
|
||||
internal_format: 0
|
||||
)
|
||||
var flip: CInt = 1
|
||||
|
||||
@@ -14,9 +14,6 @@ struct PlayerGestures: View {
|
||||
},
|
||||
doubleTapAction: {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||
},
|
||||
anyTapAction: {
|
||||
model.update()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -28,9 +25,6 @@ struct PlayerGestures: View {
|
||||
},
|
||||
doubleTapAction: {
|
||||
player.backend.togglePlay()
|
||||
},
|
||||
anyTapAction: {
|
||||
model.update()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -42,9 +36,6 @@ struct PlayerGestures: View {
|
||||
},
|
||||
doubleTapAction: {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||
},
|
||||
anyTapAction: {
|
||||
model.update()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class PlayerLayerView: UIView {
|
||||
var playerLayer = AVPlayerLayer()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
layer.addSublayer(playerLayer)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
playerLayer.frame = bounds
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
@@ -12,30 +11,15 @@ struct PlayerQueueRow: View {
|
||||
|
||||
@Default(.closePiPOnNavigation) var closePiPOnNavigation
|
||||
|
||||
@FetchRequest private var watchRequest: FetchedResults<Watch>
|
||||
|
||||
init(item: PlayerQueueItem, history: Bool = false, fullScreen: Binding<Bool> = .constant(false)) {
|
||||
self.item = item
|
||||
self.history = history
|
||||
_fullScreen = fullScreen
|
||||
_watchRequest = FetchRequest<Watch>(
|
||||
entity: Watch.entity(),
|
||||
sortDescriptors: [],
|
||||
predicate: NSPredicate(format: "videoID = %@", item.videoID)
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Button {
|
||||
player.prepareCurrentItemForHistory()
|
||||
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||
|
||||
if history {
|
||||
player.playHistory(item, at: watchStoppedAt)
|
||||
player.playHistory(item)
|
||||
} else {
|
||||
player.advanceToItem(item, at: watchStoppedAt)
|
||||
player.advanceToItem(item)
|
||||
}
|
||||
|
||||
if fullScreen {
|
||||
@@ -48,21 +32,9 @@ struct PlayerQueueRow: View {
|
||||
player.closePiP()
|
||||
}
|
||||
} label: {
|
||||
VideoBanner(video: item.video, playbackTime: watchStoppedAt, videoDuration: watch?.videoDuration)
|
||||
VideoBanner(video: item.video, playbackTime: item.playbackTime, videoDuration: item.videoDuration)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
private var watch: Watch? {
|
||||
watchRequest.first
|
||||
}
|
||||
|
||||
private var watchStoppedAt: CMTime? {
|
||||
guard let seconds = watch?.stoppedAt else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .secondsInDefaultTimescale(seconds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ struct StreamControl: View {
|
||||
Section(header: Text(instance.longDescription)) {
|
||||
ForEach(kinds, id: \.self) { key in
|
||||
ForEach(instanceStreams[key] ?? []) { stream in
|
||||
Text(stream.description).tag(Stream?.some(stream))
|
||||
Text(stream.quality).tag(Stream?.some(stream))
|
||||
}
|
||||
|
||||
if kinds.count > 1 {
|
||||
|
||||
@@ -6,18 +6,11 @@ struct TapRecognizerViewModifier: ViewModifier {
|
||||
var tapSensitivity: Double
|
||||
var singleTapAction: () -> Void
|
||||
var doubleTapAction: () -> Void
|
||||
var anyTapAction: () -> Void
|
||||
|
||||
init(
|
||||
tapSensitivity: Double,
|
||||
singleTapAction: @escaping () -> Void,
|
||||
doubleTapAction: @escaping () -> Void,
|
||||
anyTapAction: @escaping () -> Void
|
||||
) {
|
||||
init(tapSensitivity: Double, singleTapAction: @escaping () -> Void, doubleTapAction: @escaping () -> Void) {
|
||||
self.tapSensitivity = tapSensitivity
|
||||
self.singleTapAction = singleTapAction
|
||||
self.doubleTapAction = doubleTapAction
|
||||
self.anyTapAction = anyTapAction
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
@@ -26,8 +19,6 @@ struct TapRecognizerViewModifier: ViewModifier {
|
||||
|
||||
private var singleTapGesture: some Gesture {
|
||||
TapGesture(count: 1).onEnded {
|
||||
anyTapAction()
|
||||
|
||||
singleTapIsTaped = true
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + tapSensitivity) {
|
||||
@@ -51,19 +42,7 @@ struct TapRecognizerViewModifier: ViewModifier {
|
||||
}
|
||||
|
||||
extension View {
|
||||
func tapRecognizer(
|
||||
tapSensitivity: Double,
|
||||
singleTapAction: @escaping () -> Void,
|
||||
doubleTapAction: @escaping () -> Void,
|
||||
anyTapAction: @escaping () -> Void = {}
|
||||
) -> some View {
|
||||
modifier(
|
||||
TapRecognizerViewModifier(
|
||||
tapSensitivity: tapSensitivity,
|
||||
singleTapAction: singleTapAction,
|
||||
doubleTapAction: doubleTapAction,
|
||||
anyTapAction: anyTapAction
|
||||
)
|
||||
)
|
||||
func tapRecognizer(tapSensitivity: Double, singleTapAction: @escaping () -> Void, doubleTapAction: @escaping () -> Void) -> some View {
|
||||
modifier(TapRecognizerViewModifier(tapSensitivity: tapSensitivity, singleTapAction: singleTapAction, doubleTapAction: doubleTapAction))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ struct TimelineView: View {
|
||||
|
||||
.frame(maxHeight: height * 2)
|
||||
|
||||
#if !os(tvOS)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
@@ -80,7 +79,6 @@ struct TimelineView: View {
|
||||
controls.resetTimer()
|
||||
}
|
||||
)
|
||||
#endif
|
||||
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
@@ -102,13 +100,11 @@ struct TimelineView: View {
|
||||
self.size = size
|
||||
}
|
||||
})
|
||||
#if !os(tvOS)
|
||||
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
|
||||
let target = (value.location.x / size.width) * units
|
||||
current = target
|
||||
player.backend.seek(to: target)
|
||||
})
|
||||
#endif
|
||||
}
|
||||
|
||||
var projectedValue: Double {
|
||||
|
||||
@@ -12,7 +12,6 @@ struct VideoDetails: View {
|
||||
@Binding var fullScreen: Bool
|
||||
|
||||
@State private var subscribed = false
|
||||
@State private var subscriptionToggleButtonDisabled = false
|
||||
@State private var presentingUnsubscribeAlert = false
|
||||
@State private var presentingAddToPlaylist = false
|
||||
@State private var presentingShareSheet = false
|
||||
@@ -30,6 +29,7 @@ struct VideoDetails: View {
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
@Default(.showChannelSubscribers) private var showChannelSubscribers
|
||||
@Default(.showKeywords) private var showKeywords
|
||||
|
||||
init(
|
||||
@@ -89,7 +89,7 @@ struct VideoDetails: View {
|
||||
if fullScreen {
|
||||
fullScreen = false
|
||||
} else {
|
||||
self.player.hide()
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,13 +203,15 @@ struct VideoDetails: View {
|
||||
.font(.system(size: 14))
|
||||
.bold()
|
||||
|
||||
Group {
|
||||
if let subscribers = video!.channel.subscriptionsString {
|
||||
Text("\(subscribers) subscribers")
|
||||
if showChannelSubscribers {
|
||||
Group {
|
||||
if let subscribers = video!.channel.subscriptionsString {
|
||||
Text("\(subscribers) subscribers")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +232,7 @@ struct VideoDetails: View {
|
||||
}
|
||||
}
|
||||
|
||||
if accounts.app.supportsSubscriptions, accounts.signedIn {
|
||||
if accounts.app.supportsSubscriptions {
|
||||
Spacer()
|
||||
|
||||
Section {
|
||||
@@ -248,13 +250,10 @@ struct VideoDetails: View {
|
||||
"Are you sure you want to unsubscribe from \(video!.channel.name)?"
|
||||
),
|
||||
primaryButton: .destructive(Text("Unsubscribe")) {
|
||||
subscriptionToggleButtonDisabled = true
|
||||
subscriptions.unsubscribe(video!.channel.id)
|
||||
|
||||
subscriptions.unsubscribe(video!.channel.id) {
|
||||
withAnimation {
|
||||
subscriptionToggleButtonDisabled = false
|
||||
subscribed.toggle()
|
||||
}
|
||||
withAnimation {
|
||||
subscribed.toggle()
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
@@ -262,20 +261,16 @@ struct VideoDetails: View {
|
||||
}
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
subscriptionToggleButtonDisabled = true
|
||||
subscriptions.subscribe(video!.channel.id)
|
||||
|
||||
subscriptions.subscribe(video!.channel.id) {
|
||||
withAnimation {
|
||||
subscriptionToggleButtonDisabled = false
|
||||
subscribed.toggle()
|
||||
}
|
||||
withAnimation {
|
||||
subscribed.toggle()
|
||||
}
|
||||
}
|
||||
.backport
|
||||
.tint(subscriptionToggleButtonDisabled ? .gray : .blue)
|
||||
.tint(.blue)
|
||||
}
|
||||
}
|
||||
.disabled(subscriptionToggleButtonDisabled)
|
||||
.font(.system(size: 13))
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ struct VideoDetailsPaddingModifier: ViewModifier {
|
||||
self.geometry = geometry
|
||||
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
|
||||
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
|
||||
self.additionalPadding = additionalPadding ?? Self.defaultAdditionalDetailsPadding
|
||||
self.additionalPadding = additionalPadding ?? VideoDetailsPaddingModifier.defaultAdditionalDetailsPadding
|
||||
self.fullScreen = fullScreen
|
||||
}
|
||||
|
||||
|
||||
@@ -93,26 +93,8 @@ struct VideoPlayerView: View {
|
||||
Group {
|
||||
Group {
|
||||
#if os(tvOS)
|
||||
playerView
|
||||
player.playerView
|
||||
.ignoresSafeArea(.all, edges: .all)
|
||||
.onMoveCommand { direction in
|
||||
if direction == .left {
|
||||
playerControls.resetTimer()
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||
}
|
||||
if direction == .right {
|
||||
playerControls.resetTimer()
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||
}
|
||||
if direction == .up {
|
||||
playerControls.show()
|
||||
playerControls.resetTimer()
|
||||
}
|
||||
if direction == .down {
|
||||
playerControls.show()
|
||||
playerControls.resetTimer()
|
||||
}
|
||||
}
|
||||
#else
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: 0) {
|
||||
@@ -121,8 +103,30 @@ struct VideoPlayerView: View {
|
||||
} else if player.playingInPictureInPicture {
|
||||
pictureInPicturePlaceholder(geometry: geometry)
|
||||
} else {
|
||||
playerView
|
||||
#if !os(tvOS)
|
||||
ZStack(alignment: .top) {
|
||||
switch player.activeBackend {
|
||||
case .mpv:
|
||||
player.mpvPlayerView
|
||||
.overlay(GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
player.playerSize = proxy.size
|
||||
// TODO: move to backend method
|
||||
player.mpvBackend.client?.setSize(proxy.size.width, proxy.size.height)
|
||||
}
|
||||
.onChange(of: proxy.size) { _ in
|
||||
player.playerSize = proxy.size
|
||||
player.mpvBackend.client?.setSize(proxy.size.width, proxy.size.height)
|
||||
}
|
||||
})
|
||||
case .appleAVPlayer:
|
||||
player.avPlayerView
|
||||
}
|
||||
|
||||
PlayerGestures()
|
||||
|
||||
PlayerControls(player: player)
|
||||
}
|
||||
.modifier(
|
||||
VideoPlayerSizeModifier(
|
||||
geometry: geometry,
|
||||
@@ -130,7 +134,6 @@ struct VideoPlayerView: View {
|
||||
fullScreen: playerControls.playingFullscreen
|
||||
)
|
||||
)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil)
|
||||
@@ -162,26 +165,24 @@ struct VideoPlayerView: View {
|
||||
|
||||
.background(Color.black)
|
||||
|
||||
#if !os(tvOS)
|
||||
if !playerControls.playingFullscreen {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
if verticalSizeClass == .regular {
|
||||
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
||||
}
|
||||
|
||||
#else
|
||||
if !playerControls.playingFullscreen {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
if verticalSizeClass == .regular {
|
||||
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
||||
#endif
|
||||
}
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
.modifier(VideoDetailsPaddingModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.avPlayerBackend.controller?.aspectRatio,
|
||||
fullScreen: fullScreenDetails
|
||||
))
|
||||
}
|
||||
|
||||
#else
|
||||
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
.modifier(VideoDetailsPaddingModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.avPlayerBackend.controller?.aspectRatio,
|
||||
fullScreen: fullScreenDetails
|
||||
))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -204,64 +205,14 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
||||
#if os(iOS)
|
||||
#if !os(macOS)
|
||||
.statusBar(hidden: playerControls.playingFullscreen)
|
||||
.navigationBarHidden(true)
|
||||
#endif
|
||||
}
|
||||
|
||||
var playerView: some View {
|
||||
ZStack(alignment: .top) {
|
||||
switch player.activeBackend {
|
||||
case .mpv:
|
||||
player.mpvPlayerView
|
||||
.overlay(GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
player.playerSize = proxy.size
|
||||
}
|
||||
.onChange(of: proxy.size) { _ in
|
||||
player.playerSize = proxy.size
|
||||
}
|
||||
})
|
||||
case .appleAVPlayer:
|
||||
player.avPlayerView
|
||||
#if os(iOS)
|
||||
.onAppear {
|
||||
player.pipController = .init(playerLayer: player.playerLayerView.playerLayer)
|
||||
let pipDelegate = PiPDelegate()
|
||||
pipDelegate.player = player
|
||||
|
||||
player.pipDelegate = pipDelegate
|
||||
player.pipController!.delegate = pipDelegate
|
||||
player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
PlayerGestures()
|
||||
#endif
|
||||
|
||||
PlayerControls(player: player)
|
||||
}
|
||||
#if os(iOS)
|
||||
.onAppear {
|
||||
// ugly patch for #78
|
||||
guard player.activeBackend == .mpv else {
|
||||
return
|
||||
}
|
||||
|
||||
player.activeBackend = .appleAVPlayer
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
player.activeBackend = .mpv
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var fullScreenLayout: Bool {
|
||||
#if os(iOS)
|
||||
#if !os(macOS)
|
||||
playerControls.playingFullscreen || verticalSizeClass == .compact
|
||||
#else
|
||||
playerControls.playingFullscreen
|
||||
@@ -285,7 +236,7 @@ struct VideoPlayerView: View {
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
|
||||
}
|
||||
|
||||
func pictureInPicturePlaceholder(geometry: GeometryProxy) -> some View {
|
||||
@@ -314,7 +265,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
|
||||
}
|
||||
|
||||
var sidebarQueue: Bool {
|
||||
|
||||
@@ -9,7 +9,6 @@ struct AddToPlaylistView: View {
|
||||
|
||||
@State private var error = ""
|
||||
@State private var presentingErrorAlert = false
|
||||
@State private var submitButtonDisabled = false
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@@ -123,7 +122,7 @@ struct AddToPlaylistView: View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Add to Playlist", action: addToPlaylist)
|
||||
.disabled(submitButtonDisabled || selectedPlaylist.isNil)
|
||||
.disabled(selectedPlaylist.isNil)
|
||||
.padding(.top, 30)
|
||||
.alert(isPresented: $presentingErrorAlert) {
|
||||
Alert(
|
||||
@@ -166,8 +165,6 @@ struct AddToPlaylistView: View {
|
||||
|
||||
Defaults[.lastUsedPlaylistID] = id
|
||||
|
||||
submitButtonDisabled = true
|
||||
|
||||
model.addVideo(
|
||||
playlistID: id,
|
||||
videoID: video.videoID,
|
||||
@@ -177,7 +174,6 @@ struct AddToPlaylistView: View {
|
||||
onFailure: { requestError in
|
||||
error = "(\(requestError.httpStatusCode ?? -1)) \(requestError.userMessage)"
|
||||
presentingErrorAlert = true
|
||||
submitButtonDisabled = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,12 +43,9 @@ struct PlaylistFormView: View {
|
||||
TextField("Name", text: $name, onCommit: validate)
|
||||
.frame(maxWidth: 450)
|
||||
.padding(.leading, 10)
|
||||
.disabled(editing && !accounts.app.userPlaylistsAreEditable)
|
||||
|
||||
if accounts.app.userPlaylistsHaveVisibility {
|
||||
visibilityFormItem
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
visibilityFormItem
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
@@ -62,7 +59,7 @@ struct PlaylistFormView: View {
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!valid || (editing && !accounts.app.userPlaylistsAreEditable))
|
||||
.disabled(!valid)
|
||||
.alert(isPresented: $presentingErrorAlert) {
|
||||
Alert(
|
||||
title: Text("Error when accessing playlist"),
|
||||
@@ -78,7 +75,7 @@ struct PlaylistFormView: View {
|
||||
#if os(iOS)
|
||||
.padding(.vertical)
|
||||
#else
|
||||
.frame(width: 400, height: accounts.app.userPlaylistsHaveVisibility ? 150 : 120)
|
||||
.frame(width: 400, height: 150)
|
||||
#endif
|
||||
|
||||
#else
|
||||
@@ -122,24 +119,20 @@ struct PlaylistFormView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
TextField("Playlist Name", text: $name, onCommit: validate)
|
||||
.disabled(editing && !accounts.app.userPlaylistsAreEditable)
|
||||
}
|
||||
|
||||
if accounts.app.userPlaylistsHaveVisibility {
|
||||
HStack {
|
||||
Text("Visibility")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
HStack {
|
||||
Text("Visibility")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
visibilityFormItem
|
||||
}
|
||||
.padding(.top, 10)
|
||||
visibilityFormItem
|
||||
}
|
||||
.padding(.top, 10)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!valid || (editing && !accounts.app.userPlaylistsAreEditable))
|
||||
Button("Save", action: submitForm).disabled(!valid)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
||||
@@ -179,15 +172,27 @@ struct PlaylistFormView: View {
|
||||
return
|
||||
}
|
||||
|
||||
accounts.api.playlistForm(name, visibility.rawValue, playlist: playlist, onFailure: { error in
|
||||
formError = "(\(error.httpStatusCode ?? -1)) \(error.userMessage)"
|
||||
presentingErrorAlert = true
|
||||
}) { modifiedPlaylist in
|
||||
self.playlist = modifiedPlaylist
|
||||
playlists.load(force: true)
|
||||
let body = ["title": name, "privacy": visibility.rawValue]
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
resource?
|
||||
.request(editing ? .patch : .post, json: body)
|
||||
.onSuccess { response in
|
||||
if let modifiedPlaylist: Playlist = response.typedContent() {
|
||||
playlist = modifiedPlaylist
|
||||
}
|
||||
|
||||
playlists.load(force: true)
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
.onFailure { error in
|
||||
formError = "(\(error.httpStatusCode ?? -1)) \(error.userMessage)"
|
||||
presentingErrorAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
var resource: Resource? {
|
||||
editing ? accounts.api.playlist(playlist.id) : accounts.api.playlists
|
||||
}
|
||||
|
||||
var visibilityFormItem: some View {
|
||||
@@ -231,14 +236,17 @@ struct PlaylistFormView: View {
|
||||
}
|
||||
|
||||
func deletePlaylistAndDismiss() {
|
||||
accounts.api.deletePlaylist(playlist, onFailure: { error in
|
||||
formError = "(\(error.httpStatusCode ?? -1)) \(error.localizedDescription)"
|
||||
presentingErrorAlert = true
|
||||
}) {
|
||||
playlist = nil
|
||||
playlists.load(force: true)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
accounts.api.playlist(playlist.id)?
|
||||
.request(.delete)
|
||||
.onSuccess { _ in
|
||||
playlist = nil
|
||||
playlists.load(force: true)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
.onFailure { error in
|
||||
formError = "(\(error.httpStatusCode ?? -1)) \(error.localizedDescription)"
|
||||
presentingErrorAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ struct PlaylistsView: View {
|
||||
@State private var showingEditPlaylist = false
|
||||
@State private var editedPlaylist: Playlist?
|
||||
|
||||
@StateObject private var store = Store<ChannelPlaylist>()
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlaylistsModel> private var model
|
||||
@@ -20,36 +18,7 @@ struct PlaylistsView: View {
|
||||
@Namespace private var focusNamespace
|
||||
|
||||
var items: [ContentItem] {
|
||||
var videos = currentPlaylist?.videos ?? []
|
||||
|
||||
if videos.isEmpty {
|
||||
videos = store.item?.videos ?? []
|
||||
if !player.accounts.app.userPlaylistsEndpointIncludesVideos {
|
||||
var i = 0
|
||||
|
||||
for index in videos.indices {
|
||||
var video = videos[index]
|
||||
video.indexID = "\(i)"
|
||||
i += 1
|
||||
videos[index] = video
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ContentItem.array(of: videos)
|
||||
}
|
||||
|
||||
private var resource: Resource? {
|
||||
guard !player.accounts.app.userPlaylistsEndpointIncludesVideos,
|
||||
let playlist = currentPlaylist
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let resource = player.accounts.api.playlist(playlist.id)
|
||||
resource?.addObserver(store)
|
||||
|
||||
return resource
|
||||
ContentItem.array(of: currentPlaylist?.videos ?? [])
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -122,12 +91,6 @@ struct PlaylistsView: View {
|
||||
.onChange(of: accounts.current) { _ in
|
||||
model.load(force: true)
|
||||
}
|
||||
.onChange(of: selectedPlaylistID) { _ in
|
||||
resource?.load()
|
||||
}
|
||||
.onChange(of: model.reloadPlaylists) { _ in
|
||||
resource?.load()
|
||||
}
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) {
|
||||
PlaylistFormView(playlist: $createdPlaylist)
|
||||
|
||||
@@ -29,10 +29,7 @@ struct SearchTextField: View {
|
||||
.opacity(0.8)
|
||||
#endif
|
||||
TextField("Search...", text: $state.queryText) {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
navigation.hideKeyboard()
|
||||
}
|
||||
state.changeQuery { query in query.query = state.queryText }
|
||||
recents.addQuery(state.queryText, navigation: navigation)
|
||||
}
|
||||
.onChange(of: state.queryText) { _ in
|
||||
|
||||
@@ -75,7 +75,6 @@ struct SearchSuggestions: View {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
state.fieldIsFocused = false
|
||||
navigation.hideKeyboard()
|
||||
}
|
||||
|
||||
recents.addQuery(state.queryText, navigation: navigation)
|
||||
|
||||
@@ -240,7 +240,7 @@ struct SearchView: View {
|
||||
}
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
#else
|
||||
VerticalCells(items: items, allowEmpty: state.query.isEmpty)
|
||||
VerticalCells(items: items)
|
||||
.environment(\.loadMoreContentHandler) { state.loadNextPage() }
|
||||
#endif
|
||||
|
||||
@@ -359,7 +359,7 @@ struct SearchView: View {
|
||||
|
||||
private var removeAllButton: some View {
|
||||
Button {
|
||||
recents.clear()
|
||||
recents.clearQueries()
|
||||
recentsChanged.toggle()
|
||||
} label: {
|
||||
Label("Remove All", systemImage: "trash.fill")
|
||||
|
||||
@@ -14,6 +14,7 @@ struct PlayerSettings: View {
|
||||
@Default(.playerSidebar) private var playerSidebar
|
||||
@Default(.showHistoryInPlayer) private var showHistory
|
||||
@Default(.showKeywords) private var showKeywords
|
||||
@Default(.showChannelSubscribers) private var channelSubscribers
|
||||
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||
#if os(iOS)
|
||||
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||
@@ -84,6 +85,7 @@ struct PlayerSettings: View {
|
||||
|
||||
keywordsToggle
|
||||
showHistoryToggle
|
||||
channelSubscribersToggle
|
||||
returnYouTubeDislikeToggle
|
||||
}
|
||||
|
||||
@@ -197,6 +199,10 @@ struct PlayerSettings: View {
|
||||
Toggle("Show history", isOn: $showHistory)
|
||||
}
|
||||
|
||||
private var channelSubscribersToggle: some View {
|
||||
Toggle("Show subscribers count", isOn: $channelSubscribers)
|
||||
}
|
||||
|
||||
private var returnYouTubeDislikeToggle: some View {
|
||||
Toggle("Enable Return YouTube Dislike", isOn: $enableReturnYouTubeDislike)
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ struct SettingsView: View {
|
||||
case .browsing:
|
||||
return 350
|
||||
case .player:
|
||||
return 450
|
||||
return 470
|
||||
case .history:
|
||||
return 480
|
||||
case .sponsorBlock:
|
||||
|
||||
@@ -16,9 +16,9 @@ struct TrendingCountry: View {
|
||||
#if !os(tvOS)
|
||||
HStack {
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
TextField("Country", text: $query, prompt: Text(Self.prompt))
|
||||
TextField("Country", text: $query, prompt: Text(TrendingCountry.prompt))
|
||||
} else {
|
||||
TextField(Self.prompt, text: $query)
|
||||
TextField(TrendingCountry.prompt, text: $query)
|
||||
}
|
||||
|
||||
Button("Done") { selectCountryAndDismiss() }
|
||||
@@ -30,7 +30,7 @@ struct TrendingCountry: View {
|
||||
countriesList
|
||||
}
|
||||
#if os(tvOS)
|
||||
.searchable(text: $query, placement: .automatic, prompt: Text(Self.prompt))
|
||||
.searchable(text: $query, placement: .automatic, prompt: Text(TrendingCountry.prompt))
|
||||
.background(Color.black)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import CoreMedia
|
||||
import Foundation
|
||||
|
||||
struct VideoURLParser {
|
||||
@@ -12,7 +11,7 @@ struct VideoURLParser {
|
||||
return queryItemValue("v")
|
||||
}
|
||||
|
||||
var time: CMTime? {
|
||||
var time: TimeInterval? {
|
||||
guard let time = queryItemValue("t") else {
|
||||
return nil
|
||||
}
|
||||
@@ -25,13 +24,13 @@ struct VideoURLParser {
|
||||
let seconds = TimeInterval(timeComponents["seconds"] ?? "0")
|
||||
else {
|
||||
if let time = TimeInterval(time) {
|
||||
return .secondsInDefaultTimescale(time)
|
||||
return time
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return .secondsInDefaultTimescale(seconds + (minutes * 60) + (hours * 60 * 60))
|
||||
return seconds + (minutes * 60) + (hours * 60 * 60)
|
||||
}
|
||||
|
||||
func queryItemValue(_ name: String) -> String? {
|
||||
|
||||
@@ -11,7 +11,7 @@ struct HorizontalCells: View {
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 20) {
|
||||
ForEach(contentItems) { item in
|
||||
ForEach(items) { item in
|
||||
ContentItemView(item: item)
|
||||
.environment(\.horizontalCells, true)
|
||||
.onAppear { loadMoreContentItemsIfNeeded(current: item) }
|
||||
@@ -36,14 +36,6 @@ struct HorizontalCells: View {
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
}
|
||||
|
||||
var contentItems: [ContentItem] {
|
||||
items.isEmpty ? placeholders : items
|
||||
}
|
||||
|
||||
var placeholders: [ContentItem] {
|
||||
(0 ..< 9).map { _ in .init() }
|
||||
}
|
||||
|
||||
func loadMoreContentItemsIfNeeded(current item: ContentItem) {
|
||||
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
|
||||
if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
|
||||
|
||||
@@ -10,12 +10,11 @@ struct VerticalCells: View {
|
||||
@Environment(\.loadMoreContentHandler) private var loadMoreContentHandler
|
||||
|
||||
var items = [ContentItem]()
|
||||
var allowEmpty = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: scrollViewShowsIndicators) {
|
||||
LazyVGrid(columns: columns, alignment: .center) {
|
||||
ForEach(contentItems) { item in
|
||||
ForEach(items.sorted { $0 < $1 }) { item in
|
||||
ContentItemView(item: item)
|
||||
.onAppear { loadMoreContentItemsIfNeeded(current: item) }
|
||||
}
|
||||
@@ -32,14 +31,6 @@ struct VerticalCells: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
var contentItems: [ContentItem] {
|
||||
items.isEmpty ? (allowEmpty ? items : placeholders) : items.sorted { $0 < $1 }
|
||||
}
|
||||
|
||||
var placeholders: [ContentItem] {
|
||||
(0 ..< 9).map { _ in .init() }
|
||||
}
|
||||
|
||||
func loadMoreContentItemsIfNeeded(current item: ContentItem) {
|
||||
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
|
||||
if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
|
||||
@@ -49,7 +40,7 @@ struct VerticalCells: View {
|
||||
|
||||
var columns: [GridItem] {
|
||||
#if os(tvOS)
|
||||
contentItems.count < 3 ? Array(repeating: GridItem(.fixed(500)), count: [contentItems.count, 1].max()!) : adaptiveItem
|
||||
items.count < 3 ? Array(repeating: GridItem(.fixed(500)), count: [items.count, 1].max()!) : adaptiveItem
|
||||
#else
|
||||
adaptiveItem
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import SDWebImageSwiftUI
|
||||
import SwiftUI
|
||||
@@ -7,7 +6,6 @@ struct VideoCell: View {
|
||||
private var video: Video
|
||||
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@@ -15,9 +13,7 @@ struct VideoCell: View {
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<ThumbnailsModel> private var thumbnails
|
||||
|
||||
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
||||
@@ -63,38 +59,30 @@ struct VideoCell: View {
|
||||
}
|
||||
|
||||
private func playAction() {
|
||||
DispatchQueue.main.async {
|
||||
guard video.videoID != Video.fixtureID else {
|
||||
return
|
||||
if watchingNow {
|
||||
if !player.playingInPictureInPicture {
|
||||
player.show()
|
||||
}
|
||||
|
||||
if watchingNow {
|
||||
if !player.playingInPictureInPicture {
|
||||
player.show()
|
||||
}
|
||||
|
||||
if !playNowContinues {
|
||||
player.backend.seek(to: .zero)
|
||||
}
|
||||
|
||||
player.play()
|
||||
|
||||
return
|
||||
if !playNowContinues {
|
||||
player.backend.seek(to: .zero)
|
||||
}
|
||||
|
||||
var playAt: CMTime?
|
||||
player.play()
|
||||
|
||||
if playNowContinues,
|
||||
!watch.isNil,
|
||||
!watch!.finished
|
||||
{
|
||||
playAt = .secondsInDefaultTimescale(watch!.stoppedAt)
|
||||
}
|
||||
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||
|
||||
player.play(video, at: playAt, inNavigationView: inNavigationView)
|
||||
return
|
||||
}
|
||||
|
||||
var playAt: TimeInterval?
|
||||
|
||||
if playNowContinues,
|
||||
!watch.isNil,
|
||||
!watch!.finished
|
||||
{
|
||||
playAt = watch!.stoppedAt
|
||||
}
|
||||
|
||||
player.play(video, at: playAt, inNavigationView: inNavigationView)
|
||||
}
|
||||
|
||||
private var playNowContinues: Bool {
|
||||
@@ -159,7 +147,9 @@ struct VideoCell: View {
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if !channelOnThumbnail {
|
||||
channelButton(badge: false)
|
||||
Text(video.channel.name)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if additionalDetailsAvailable {
|
||||
@@ -241,7 +231,9 @@ struct VideoCell: View {
|
||||
.frame(minHeight: 40, alignment: .top)
|
||||
#endif
|
||||
if !channelOnThumbnail {
|
||||
channelButton(badge: false)
|
||||
Text(video.channel.name)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 4)
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
@@ -295,29 +287,6 @@ struct VideoCell: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func channelButton(badge: Bool = true) -> some View {
|
||||
Button {
|
||||
NavigationModel.openChannel(
|
||||
video.channel,
|
||||
player: player,
|
||||
recents: recents,
|
||||
navigation: navigation,
|
||||
navigationStyle: navigationStyle
|
||||
)
|
||||
} label: {
|
||||
if badge {
|
||||
DetailBadge(text: video.author, style: .prominent)
|
||||
.foregroundColor(.primary)
|
||||
} else {
|
||||
Text(video.channel.name)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("\(video.channel.name) Channel")
|
||||
}
|
||||
|
||||
private var additionalDetailsAvailable: Bool {
|
||||
video.publishedDate != nil || video.views != 0 ||
|
||||
(!timeOnThumbnail && !video.length.formattedAsPlaybackTime().isNil)
|
||||
@@ -354,7 +323,7 @@ struct VideoCell: View {
|
||||
Spacer()
|
||||
|
||||
if channelOnThumbnail {
|
||||
channelButton()
|
||||
DetailBadge(text: video.author, style: .prominent)
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
@@ -444,7 +413,7 @@ struct VideoCell: View {
|
||||
stoppedAt.isFinite,
|
||||
let stoppedAtFormatted = stoppedAt.formattedAsPlaybackTime()
|
||||
{
|
||||
if (watch?.videoDuration ?? 0) > 0 {
|
||||
if watch?.videoDuration ?? 0 > 0 {
|
||||
videoTime = watch!.videoDuration.formattedAsPlaybackTime() ?? "?"
|
||||
}
|
||||
return "\(stoppedAtFormatted) / \(videoTime)"
|
||||
|
||||
@@ -99,7 +99,7 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
|
||||
.disabled(playerControls.isLoadingVideo)
|
||||
.font(.system(size: 30))
|
||||
.frame(minWidth: 30)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ struct ChannelVideosView: View {
|
||||
|
||||
@State private var presentingShareSheet = false
|
||||
@State private var shareURL: URL?
|
||||
@State private var subscriptionToggleButtonDisabled = false
|
||||
|
||||
@StateObject private var store = Store<Channel>()
|
||||
|
||||
@@ -147,25 +146,17 @@ struct ChannelVideosView: View {
|
||||
if accounts.app.supportsSubscriptions && accounts.signedIn {
|
||||
if subscriptions.isSubscribing(channel.id) {
|
||||
Button("Unsubscribe") {
|
||||
subscriptionToggleButtonDisabled = true
|
||||
|
||||
subscriptions.unsubscribe(channel.id) {
|
||||
subscriptionToggleButtonDisabled = false
|
||||
}
|
||||
navigation.presentUnsubscribeAlert(channel)
|
||||
}
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
subscriptionToggleButtonDisabled = true
|
||||
|
||||
subscriptions.subscribe(channel.id) {
|
||||
subscriptionToggleButtonDisabled = false
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(subscriptionToggleButtonDisabled)
|
||||
}
|
||||
|
||||
private var contentItem: ContentItem {
|
||||
|
||||
@@ -11,10 +11,8 @@ struct ContentItemView: View {
|
||||
ChannelPlaylistCell(playlist: item.playlist)
|
||||
case .channel:
|
||||
ChannelCell(channel: item.channel)
|
||||
case .video:
|
||||
VideoCell(video: item.video)
|
||||
default:
|
||||
PlaceholderCell()
|
||||
VideoCell(video: item.video)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct PlaceholderCell: View {
|
||||
var body: some View {
|
||||
VideoCell(video: .fixture)
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaceholderCell_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PlaceholderCell()
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
}
|
||||
@@ -6,35 +6,9 @@ struct PlaylistVideosView: View {
|
||||
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlaylistsModel> private var model
|
||||
|
||||
@StateObject private var store = Store<ChannelPlaylist>()
|
||||
|
||||
var contentItems: [ContentItem] {
|
||||
var videos = playlist.videos
|
||||
|
||||
if videos.isEmpty {
|
||||
videos = store.item?.videos ?? []
|
||||
if !player.accounts.app.userPlaylistsEndpointIncludesVideos {
|
||||
var i = 0
|
||||
|
||||
for index in videos.indices {
|
||||
var video = videos[index]
|
||||
video.indexID = "\(i)"
|
||||
i += 1
|
||||
videos[index] = video
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ContentItem.array(of: videos)
|
||||
}
|
||||
|
||||
private var resource: Resource? {
|
||||
let resource = player.accounts.api.playlist(playlist.id)
|
||||
resource?.addObserver(store)
|
||||
|
||||
return resource
|
||||
ContentItem.array(of: playlist.videos)
|
||||
}
|
||||
|
||||
var videos: [Video] {
|
||||
@@ -48,14 +22,6 @@ struct PlaylistVideosView: View {
|
||||
var body: some View {
|
||||
BrowserPlayerControls {
|
||||
VerticalCells(items: contentItems)
|
||||
.onAppear {
|
||||
if !player.accounts.app.userPlaylistsEndpointIncludesVideos {
|
||||
resource?.load()
|
||||
}
|
||||
}
|
||||
.onChange(of: model.reloadPlaylists) { _ in
|
||||
resource?.load()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle("\(playlist.title) Playlist")
|
||||
#endif
|
||||
|
||||
@@ -33,12 +33,6 @@ struct VideoContextMenuView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if video.videoID != Video.fixtureID {
|
||||
contextMenu
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var contextMenu: some View {
|
||||
if saveHistory {
|
||||
Section {
|
||||
if let watchedAtString = watchedAtString {
|
||||
@@ -111,7 +105,7 @@ struct VideoContextMenuView: View {
|
||||
|
||||
private var continueButton: some View {
|
||||
Button {
|
||||
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt), inNavigationView: inNavigationView)
|
||||
player.play(video, at: watch!.stoppedAt, inNavigationView: inNavigationView)
|
||||
} label: {
|
||||
Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause")
|
||||
}
|
||||
@@ -201,7 +195,7 @@ struct VideoContextMenuView: View {
|
||||
|
||||
func removeFromPlaylistButton(playlistID: String) -> some View {
|
||||
Button {
|
||||
playlists.removeVideo(index: video.indexID!, playlistID: playlistID)
|
||||
playlists.removeVideo(videoIndexID: video.indexID!, playlistID: playlistID)
|
||||
} label: {
|
||||
Label("Remove from Playlist", systemImage: "text.badge.minus")
|
||||
}
|
||||
|
||||
@@ -92,16 +92,6 @@ struct YatteeApp: App {
|
||||
.background(
|
||||
HostingWindowFinder { window in
|
||||
Windows.playerWindow = window
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSWindow.willExitFullScreenNotification,
|
||||
object: window,
|
||||
queue: OperationQueue.main
|
||||
) { _ in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.player.controls.playingFullscreen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.onAppear { player.presentingPlayer = true }
|
||||
|
||||
BIN
Vendor/mpv/tvOS/libavcodec.a
vendored
BIN
Vendor/mpv/tvOS/libavcodec.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libavdevice.a
vendored
BIN
Vendor/mpv/tvOS/libavdevice.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libavfilter.a
vendored
BIN
Vendor/mpv/tvOS/libavfilter.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libavformat.a
vendored
BIN
Vendor/mpv/tvOS/libavformat.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libavresample.a
vendored
BIN
Vendor/mpv/tvOS/libavresample.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libavutil.a
vendored
BIN
Vendor/mpv/tvOS/libavutil.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libmpv.a
vendored
BIN
Vendor/mpv/tvOS/libmpv.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libpostproc.a
vendored
BIN
Vendor/mpv/tvOS/libpostproc.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libswresample.a
vendored
BIN
Vendor/mpv/tvOS/libswresample.a
vendored
Binary file not shown.
BIN
Vendor/mpv/tvOS/libswscale.a
vendored
BIN
Vendor/mpv/tvOS/libswscale.a
vendored
Binary file not shown.
@@ -190,11 +190,6 @@
|
||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||
372915E82687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; };
|
||||
372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; };
|
||||
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F22838388A000CFD59 /* PlayerLayerView.swift */; };
|
||||
373031F32838388A000CFD59 /* PlayerLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F22838388A000CFD59 /* PlayerLayerView.swift */; };
|
||||
373031F528383A89000CFD59 /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; };
|
||||
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */; };
|
||||
3730F75A2733481E00F385FC /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
|
||||
373197D92732015300EF734F /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
|
||||
@@ -218,7 +213,6 @@
|
||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
||||
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
||||
374108D1272B11B2006C5CC8 /* PictureInPictureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */; };
|
||||
3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */; };
|
||||
3743B86927216D3600261544 /* ChannelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743B86727216D3600261544 /* ChannelCell.swift */; };
|
||||
3743B86A27216D3600261544 /* ChannelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743B86727216D3600261544 /* ChannelCell.swift */; };
|
||||
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
||||
@@ -350,24 +344,6 @@
|
||||
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; };
|
||||
376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; };
|
||||
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; };
|
||||
3772000F27E8EC8800CB2475 /* ToggleBackendButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371114EE27B951B800C2EF7B /* ToggleBackendButton.swift */; };
|
||||
3772002727E8EDF000CB2475 /* libmpv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001327E8ED1600CB2475 /* libmpv.a */; };
|
||||
3772002827E8EDF000CB2475 /* libswscale.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001A27E8ED1700CB2475 /* libswscale.a */; };
|
||||
3772002927E8EDF000CB2475 /* libavutil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001627E8ED1700CB2475 /* libavutil.a */; };
|
||||
3772002A27E8EDF000CB2475 /* libswresample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001727E8ED1700CB2475 /* libswresample.a */; };
|
||||
3772002B27E8EDF000CB2475 /* libavcodec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001427E8ED1700CB2475 /* libavcodec.a */; };
|
||||
3772002C27E8EDF000CB2475 /* libpostproc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001927E8ED1700CB2475 /* libpostproc.a */; };
|
||||
3772002D27E8EDF000CB2475 /* libavformat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001127E8ED1500CB2475 /* libavformat.a */; };
|
||||
3772002E27E8EDF000CB2475 /* libavdevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001527E8ED1700CB2475 /* libavdevice.a */; };
|
||||
3772002F27E8EDF000CB2475 /* libavresample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001227E8ED1500CB2475 /* libavresample.a */; };
|
||||
3772003027E8EDF000CB2475 /* libavfilter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772001827E8ED1700CB2475 /* libavfilter.a */; };
|
||||
3772003827E8EEB100CB2475 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003227E8EEA100CB2475 /* AudioToolbox.framework */; };
|
||||
3772003927E8EEB700CB2475 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003427E8EEA100CB2475 /* AVFoundation.framework */; };
|
||||
3772003A27E8EEBE00CB2475 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003527E8EEA100CB2475 /* CoreMedia.framework */; };
|
||||
3772003B27E8EEC800CB2475 /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003727E8EEA100CB2475 /* libbz2.tbd */; };
|
||||
3772003C27E8EED000CB2475 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003127E8EEA100CB2475 /* libz.tbd */; };
|
||||
3772003D27E8EEDB00CB2475 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003327E8EEA100CB2475 /* libiconv.tbd */; };
|
||||
3772003E27E8EEEB00CB2475 /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003627E8EEA100CB2475 /* VideoToolbox.framework */; };
|
||||
37732FF02703A26300F04329 /* AccountValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FEF2703A26300F04329 /* AccountValidationStatus.swift */; };
|
||||
37732FF12703A26300F04329 /* AccountValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FEF2703A26300F04329 /* AccountValidationStatus.swift */; };
|
||||
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FEF2703A26300F04329 /* AccountValidationStatus.swift */; };
|
||||
@@ -716,6 +692,7 @@
|
||||
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; };
|
||||
37F9619B27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */; };
|
||||
37F9619C27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */; };
|
||||
37F9619D27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */; };
|
||||
37F9619F27BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; };
|
||||
37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; };
|
||||
37F961A127BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; };
|
||||
@@ -737,9 +714,6 @@
|
||||
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD43E22704847C0073EE42 /* View+Fixtures.swift */; };
|
||||
37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD43E22704847C0073EE42 /* View+Fixtures.swift */; };
|
||||
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
||||
37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; };
|
||||
37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; };
|
||||
37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEF11227EFD8580033912F /* PlaceholderCell.swift */; };
|
||||
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
||||
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
||||
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
|
||||
@@ -869,8 +843,6 @@
|
||||
3727B74927872A920021C15E /* VisualEffectBlur-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisualEffectBlur-iOS.swift"; sourceTree = "<group>"; };
|
||||
3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = "<group>"; };
|
||||
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
|
||||
373031F22838388A000CFD59 /* PlayerLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLayerView.swift; sourceTree = "<group>"; };
|
||||
373031F428383A89000CFD59 /* PiPDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPDelegate.swift; sourceTree = "<group>"; };
|
||||
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; };
|
||||
373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; };
|
||||
37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; };
|
||||
@@ -942,24 +914,6 @@
|
||||
376BE50627347B57009AD608 /* SettingsHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeader.swift; sourceTree = "<group>"; };
|
||||
376BE50A27349108009AD608 /* BrowsingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettings.swift; sourceTree = "<group>"; };
|
||||
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = "<group>"; };
|
||||
3772001127E8ED1500CB2475 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libavformat.a; sourceTree = "<group>"; };
|
||||
3772001227E8ED1500CB2475 /* libavresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libavresample.a; sourceTree = "<group>"; };
|
||||
3772001327E8ED1600CB2475 /* libmpv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libmpv.a; sourceTree = "<group>"; };
|
||||
3772001427E8ED1700CB2475 /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libavcodec.a; sourceTree = "<group>"; };
|
||||
3772001527E8ED1700CB2475 /* libavdevice.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libavdevice.a; sourceTree = "<group>"; };
|
||||
3772001627E8ED1700CB2475 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libavutil.a; sourceTree = "<group>"; };
|
||||
3772001727E8ED1700CB2475 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libswresample.a; sourceTree = "<group>"; };
|
||||
3772001827E8ED1700CB2475 /* libavfilter.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libavfilter.a; sourceTree = "<group>"; };
|
||||
3772001927E8ED1700CB2475 /* libpostproc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libpostproc.a; sourceTree = "<group>"; };
|
||||
3772001A27E8ED1700CB2475 /* libswscale.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libswscale.a; sourceTree = "<group>"; };
|
||||
3772002527E8ED2600CB2475 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
|
||||
3772003127E8EEA100CB2475 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
3772003227E8EEA100CB2475 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/AudioToolbox.framework; sourceTree = DEVELOPER_DIR; };
|
||||
3772003327E8EEA100CB2475 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
3772003427E8EEA100CB2475 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
3772003527E8EEA100CB2475 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/CoreMedia.framework; sourceTree = DEVELOPER_DIR; };
|
||||
3772003627E8EEA100CB2475 /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/VideoToolbox.framework; sourceTree = DEVELOPER_DIR; };
|
||||
3772003727E8EEA100CB2475 /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/usr/lib/libbz2.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidationStatus.swift; sourceTree = "<group>"; };
|
||||
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
||||
37737785276F9858000521C1 /* Windows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
|
||||
@@ -1107,7 +1061,6 @@
|
||||
37FB285D272225E800A57617 /* ContentItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItemView.swift; sourceTree = "<group>"; };
|
||||
37FD43DB270470B70073EE42 /* InstancesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettings.swift; sourceTree = "<group>"; };
|
||||
37FD43E22704847C0073EE42 /* View+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Fixtures.swift"; sourceTree = "<group>"; };
|
||||
37FEF11227EFD8580033912F /* PlaceholderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderCell.swift; sourceTree = "<group>"; };
|
||||
37FFC43F272734C3009FFD26 /* Throttle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Throttle.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -1232,32 +1185,15 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3772003027E8EDF000CB2475 /* libavfilter.a in Frameworks */,
|
||||
3772003C27E8EED000CB2475 /* libz.tbd in Frameworks */,
|
||||
3772002A27E8EDF000CB2475 /* libswresample.a in Frameworks */,
|
||||
3772002F27E8EDF000CB2475 /* libavresample.a in Frameworks */,
|
||||
37FB2849272207F000A57617 /* SDWebImageWebPCoder in Frameworks */,
|
||||
37FB285427220D8400A57617 /* SDWebImagePINPlugin in Frameworks */,
|
||||
3772003927E8EEB700CB2475 /* AVFoundation.framework in Frameworks */,
|
||||
3772002D27E8EDF000CB2475 /* libavformat.a in Frameworks */,
|
||||
3772002827E8EDF000CB2475 /* libswscale.a in Frameworks */,
|
||||
3772003D27E8EEDB00CB2475 /* libiconv.tbd in Frameworks */,
|
||||
3772003827E8EEB100CB2475 /* AudioToolbox.framework in Frameworks */,
|
||||
37FB28462722054C00A57617 /* SDWebImageSwiftUI in Frameworks */,
|
||||
3772002B27E8EDF000CB2475 /* libavcodec.a in Frameworks */,
|
||||
3765917E27237D2A009F956E /* PINCache in Frameworks */,
|
||||
3772003E27E8EEEB00CB2475 /* VideoToolbox.framework in Frameworks */,
|
||||
3772003A27E8EEBE00CB2475 /* CoreMedia.framework in Frameworks */,
|
||||
3772002C27E8EDF000CB2475 /* libpostproc.a in Frameworks */,
|
||||
372915E42687E33E00F5A35B /* Defaults in Frameworks */,
|
||||
3772002927E8EDF000CB2475 /* libavutil.a in Frameworks */,
|
||||
3772003B27E8EEC800CB2475 /* libbz2.tbd in Frameworks */,
|
||||
37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */,
|
||||
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */,
|
||||
3772002727E8EDF000CB2475 /* libmpv.a in Frameworks */,
|
||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */,
|
||||
37B767E02678C5BF0098BAA8 /* Logging in Frameworks */,
|
||||
3772002E27E8EDF000CB2475 /* libavdevice.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1358,7 +1294,6 @@
|
||||
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
|
||||
37E8B0EB27B326C00024006F /* TimelineView.swift */,
|
||||
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */,
|
||||
373031F22838388A000CFD59 /* PlayerLayerView.swift */,
|
||||
);
|
||||
path = Player;
|
||||
sourceTree = "<group>";
|
||||
@@ -1415,7 +1350,6 @@
|
||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
|
||||
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
|
||||
37E70922271CD43000D34DDE /* WelcomeScreen.swift */,
|
||||
37FEF11227EFD8580033912F /* PlaceholderCell.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -1447,7 +1381,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37EBD8C227AF0D7C00F1C24B /* Backends */,
|
||||
373031F428383A89000CFD59 /* PiPDelegate.swift */,
|
||||
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
||||
37319F0427103F94004ECCD0 /* PlayerQueue.swift */,
|
||||
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */,
|
||||
@@ -1512,9 +1445,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3749BF6E27ADA135000480FF /* include */,
|
||||
3749BF6D27ADA135000480FF /* iOS */,
|
||||
370F4FAC27CC169D001B35DC /* macOS */,
|
||||
3772001027E8ED0300CB2475 /* tvOS */,
|
||||
3749BF6D27ADA135000480FF /* iOS */,
|
||||
);
|
||||
path = mpv;
|
||||
sourceTree = "<group>";
|
||||
@@ -1595,35 +1527,11 @@
|
||||
path = Modifiers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3772001027E8ED0300CB2475 /* tvOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3772001427E8ED1700CB2475 /* libavcodec.a */,
|
||||
3772001527E8ED1700CB2475 /* libavdevice.a */,
|
||||
3772001827E8ED1700CB2475 /* libavfilter.a */,
|
||||
3772001127E8ED1500CB2475 /* libavformat.a */,
|
||||
3772001227E8ED1500CB2475 /* libavresample.a */,
|
||||
3772001627E8ED1700CB2475 /* libavutil.a */,
|
||||
3772001327E8ED1600CB2475 /* libmpv.a */,
|
||||
3772001927E8ED1700CB2475 /* libpostproc.a */,
|
||||
3772001727E8ED1700CB2475 /* libswresample.a */,
|
||||
3772001A27E8ED1700CB2475 /* libswscale.a */,
|
||||
);
|
||||
path = tvOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
377FC7D1267A080300A6BBAF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37C2212A27ADA43700305B41 /* VideoToolbox.framework */,
|
||||
37C2212827ADA41400305B41 /* CoreMedia.framework */,
|
||||
3772003227E8EEA100CB2475 /* AudioToolbox.framework */,
|
||||
3772003427E8EEA100CB2475 /* AVFoundation.framework */,
|
||||
3772003527E8EEA100CB2475 /* CoreMedia.framework */,
|
||||
3772003727E8EEA100CB2475 /* libbz2.tbd */,
|
||||
3772003327E8EEA100CB2475 /* libiconv.tbd */,
|
||||
3772003127E8EEA100CB2475 /* libz.tbd */,
|
||||
3772003627E8EEA100CB2475 /* VideoToolbox.framework */,
|
||||
37C2212627ADA41000305B41 /* CoreFoundation.framework */,
|
||||
37C2212427ADA40A00305B41 /* AudioToolbox.framework */,
|
||||
37C2212227ADA3F200305B41 /* libiconv.tbd */,
|
||||
@@ -1839,7 +1747,6 @@
|
||||
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */,
|
||||
37D4B15E267164AF00C925CA /* Assets.xcassets */,
|
||||
37D4B1AE26729DEB00C925CA /* Info.plist */,
|
||||
3772002527E8ED2600CB2475 /* BridgingHeader.h */,
|
||||
);
|
||||
path = tvOS;
|
||||
sourceTree = "<group>";
|
||||
@@ -1962,50 +1869,11 @@
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
376ED59427F0C49700A0363B /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
37FF8BFC27F9A7AD0038199F /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
37FF8BFD27F9A7B20038199F /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
37FF8BFE27F9A7BA0038199F /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
37FF8BFF27F9A7BC0038199F /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
37A3B15627255E7F000FB5EE /* Open in Yattee (macOS) */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 37A3B17427255E7F000FB5EE /* Build configuration list for PBXNativeTarget "Open in Yattee (macOS)" */;
|
||||
buildPhases = (
|
||||
37FF8BFF27F9A7BC0038199F /* Headers */,
|
||||
37A3B15327255E7F000FB5EE /* Sources */,
|
||||
37A3B15427255E7F000FB5EE /* Frameworks */,
|
||||
37A3B15527255E7F000FB5EE /* Resources */,
|
||||
@@ -2023,7 +1891,6 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 37A3B1902725735F000FB5EE /* Build configuration list for PBXNativeTarget "Open in Yattee (iOS)" */;
|
||||
buildPhases = (
|
||||
37FF8BFE27F9A7BA0038199F /* Headers */,
|
||||
37A3B1752725735F000FB5EE /* Sources */,
|
||||
37A3B1762725735F000FB5EE /* Frameworks */,
|
||||
37A3B1772725735F000FB5EE /* Resources */,
|
||||
@@ -2042,7 +1909,6 @@
|
||||
buildConfigurationList = 37D4B0EC2671614900C925CA /* Build configuration list for PBXNativeTarget "Yattee (iOS)" */;
|
||||
buildPhases = (
|
||||
37CC3F48270CE89B00608308 /* ShellScript */,
|
||||
376ED59427F0C49700A0363B /* Headers */,
|
||||
37D4B0C52671614900C925CA /* Sources */,
|
||||
37D4B0C62671614900C925CA /* Frameworks */,
|
||||
37D4B0C72671614900C925CA /* Resources */,
|
||||
@@ -2073,7 +1939,6 @@
|
||||
buildConfigurationList = 37D4B0EF2671614900C925CA /* Build configuration list for PBXNativeTarget "Yattee (macOS)" */;
|
||||
buildPhases = (
|
||||
37CC3F4A270CE8D000608308 /* ShellScript */,
|
||||
37FF8BFC27F9A7AD0038199F /* Headers */,
|
||||
37D4B0CB2671614900C925CA /* Sources */,
|
||||
37D4B0CC2671614900C925CA /* Frameworks */,
|
||||
37D4B0CD2671614900C925CA /* Resources */,
|
||||
@@ -2148,7 +2013,6 @@
|
||||
buildConfigurationList = 37D4B177267164B000C925CA /* Build configuration list for PBXNativeTarget "Yattee (tvOS)" */;
|
||||
buildPhases = (
|
||||
37CC3F49270CE8CA00608308 /* ShellScript */,
|
||||
37FF8BFD27F9A7B20038199F /* Headers */,
|
||||
37D4B154267164AE00C925CA /* Sources */,
|
||||
37D4B155267164AE00C925CA /* Frameworks */,
|
||||
37D4B156267164AE00C925CA /* Resources */,
|
||||
@@ -2557,7 +2421,6 @@
|
||||
37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||
373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||
3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
||||
373031F32838388A000CFD59 /* PlayerLayerView.swift in Sources */,
|
||||
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||
377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */,
|
||||
37C3A251272366440087A57A /* ChannelPlaylistView.swift in Sources */,
|
||||
@@ -2610,12 +2473,10 @@
|
||||
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||
3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
|
||||
373031F528383A89000CFD59 /* PiPDelegate.swift in Sources */,
|
||||
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
|
||||
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
37141673267A8E10006CA35D /* Country.swift in Sources */,
|
||||
37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
||||
37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
||||
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||
376BE50B27349108009AD608 /* BrowsingSettings.swift in Sources */,
|
||||
@@ -2769,7 +2630,6 @@
|
||||
37A9965F26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
||||
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */,
|
||||
37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
||||
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */,
|
||||
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
|
||||
37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||
@@ -2778,7 +2638,6 @@
|
||||
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||
3784B23E2728B85300B09468 /* ShareButton.swift in Sources */,
|
||||
37BE0BDA26A214630092E2DB /* AppleAVPlayerViewController.swift in Sources */,
|
||||
37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
||||
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||
374108D1272B11B2006C5CC8 /* PictureInPictureDelegate.swift in Sources */,
|
||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||
@@ -2934,7 +2793,6 @@
|
||||
37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */,
|
||||
3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||
371114ED27B94C8800C2EF7B /* RepeatingTimer.swift in Sources */,
|
||||
3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */,
|
||||
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
||||
@@ -2961,6 +2819,7 @@
|
||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
|
||||
37F9619D27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */,
|
||||
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
||||
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
||||
@@ -3012,8 +2871,6 @@
|
||||
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||
37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */,
|
||||
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||
372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */,
|
||||
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */,
|
||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
||||
@@ -3023,7 +2880,6 @@
|
||||
37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||
37EBD8CC27AF26C200F1C24B /* MPVBackend.swift in Sources */,
|
||||
3711404126B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||
37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
||||
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */,
|
||||
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
|
||||
@@ -3049,7 +2905,6 @@
|
||||
37BC50AE2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||
37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||
37599F36272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||
3772000F27E8EC8800CB2475 /* ToggleBackendButton.swift in Sources */,
|
||||
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */,
|
||||
371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||
@@ -3103,7 +2958,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -3116,7 +2971,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -3137,7 +2992,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -3150,7 +3005,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -3169,7 +3024,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -3181,7 +3036,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -3201,7 +3056,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -3213,7 +3068,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -3365,7 +3220,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++14";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -3389,7 +3244,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Vendor/mpv/iOS/lib",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
OTHER_LDFLAGS = "-lstdc++";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
|
||||
PRODUCT_NAME = Yattee;
|
||||
@@ -3407,7 +3262,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
@@ -3427,7 +3282,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Vendor/mpv/iOS/lib",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
OTHER_LDFLAGS = "-lstdc++";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
|
||||
PRODUCT_NAME = Yattee;
|
||||
@@ -3449,7 +3304,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -3468,7 +3323,7 @@
|
||||
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = macosx;
|
||||
@@ -3487,7 +3342,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -3506,7 +3361,7 @@
|
||||
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = macosx;
|
||||
@@ -3623,7 +3478,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -3638,17 +3493,11 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Vendor/mpv",
|
||||
"$(PROJECT_DIR)/Vendor/mpv/tvOS",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = appletvos;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = tvOS/BridgingHeader.h;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -3661,7 +3510,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -3676,17 +3525,11 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Vendor/mpv",
|
||||
"$(PROJECT_DIR)/Vendor/mpv/tvOS",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4.alpha.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = appletvos;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = tvOS/BridgingHeader.h;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8",
|
||||
"version" : "5.6.1"
|
||||
"revision" : "f82c23a8a7ef8dc1a49a8bfc6a96883e79121864",
|
||||
"version" : "5.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -96,7 +96,7 @@
|
||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||
"state" : {
|
||||
"branch" : "2.x",
|
||||
"revision" : "503830bf24f679b7a9199be85ee7c8d012528d09"
|
||||
"revision" : "f250bead4b943ef9711c61274a1f52e380afa0e8"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
|
||||
Self.instance = self
|
||||
AppDelegate.instance = self
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,19 +33,11 @@ enum Windows: String, CaseIterable {
|
||||
func open() {
|
||||
switch self {
|
||||
case .player:
|
||||
if let window = Self.playerWindow {
|
||||
window.makeKeyAndOrderFront(self)
|
||||
} else {
|
||||
NSWorkspace.shared.open(URL(string: "yattee://\(location)")!)
|
||||
}
|
||||
NSWorkspace.shared.open(URL(string: "yattee://\(location)")!)
|
||||
case .main:
|
||||
Self.main.focus()
|
||||
}
|
||||
}
|
||||
|
||||
func toggleFullScreen() {
|
||||
window?.toggleFullScreen(nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct HostingWindowFinder: NSViewRepresentable {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
#import "../Vendor/mpv/include/client.h"
|
||||
#import "../Vendor/mpv/include/render.h"
|
||||
#import "../Vendor/mpv/include/render_gl.h"
|
||||
#import "../Vendor/mpv/include/stream_cb.h"
|
||||
Reference in New Issue
Block a user