Compare commits

..

14 Commits

Author SHA1 Message Date
Arkadiusz Fal
574c58b6e4 Bump version number 2022-03-20 22:49:09 +01:00
Arkadiusz Fal
c1f3fecfe7 Add toggle for dislikes 2022-03-20 21:32:05 +01:00
Arkadiusz Fal
78834b1548 Fix parsing subscriptions published date 2022-03-20 21:30:45 +01:00
Arkadiusz Fal
cf68c6c69f Bump version number 2022-03-20 00:32:31 +01:00
Arkadiusz Fal
d7c8dce994 Minor fixes 2022-03-20 00:31:49 +01:00
Arkadiusz Fal
c431c20bc0 Add ReturnYoutubeDislike API 2022-03-20 00:31:49 +01:00
Arkadiusz Fal
a9b057505c Fixes for MPV in macOS 2022-03-20 00:31:49 +01:00
Arkadiusz Fal
93943ecd83 Fix EOF handler 2022-02-27 21:25:29 +01:00
Arkadiusz Fal
a17cbf0085 Minor improvements 2022-02-26 12:21:49 +01:00
Arkadiusz Fal
8e74c3ec0a Add hide player button cancel action 2022-02-26 12:21:49 +01:00
Arkadiusz Fal
cc5f41807b Prevent multiple seeks 2022-02-26 12:21:49 +01:00
Arkadiusz Fal
a60a2a6744 Add Now Playing info center updates 2022-02-26 12:21:49 +01:00
Arkadiusz Fal
0a36870480 Hello, mpv! 🎉 2022-02-26 12:21:49 +01:00
Arkadiusz Fal
48ab8b27c8 Reorganize toolbars placement 2022-02-26 12:21:48 +01:00
82 changed files with 441 additions and 1518 deletions

View File

@@ -3,6 +3,6 @@ import AppKit
extension NSTextField {
override open var focusRingType: NSFocusRingType {
get { .none }
set {}
set {} // swiftlint:disable:this unused_setter_value
}
}

View File

@@ -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 {

View File

@@ -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(),

View File

View File

@@ -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) }

View File

@@ -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)"))
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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) {}
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -66,7 +66,7 @@ extension PlayerModel {
func rebuildTVMenu() {
#if os(tvOS)
avPlayerBackend.controller?.playerView.transportBarCustomMenuItems = [
controller?.playerView.transportBarCustomMenuItems = [
restoreLastSkippedSegmentAction,
rateMenu,
streamsMenu

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 ?? "")"
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}
)
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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))
}
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}
)
}

View File

@@ -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
}
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -179,7 +179,7 @@ struct SettingsView: View {
case .browsing:
return 350
case .player:
return 450
return 470
case .history:
return 480
case .sponsorBlock:

View File

@@ -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
}

View File

@@ -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? {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)"

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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 }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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;

View File

@@ -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"
}
},
{

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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"