Compare commits

..

35 Commits

Author SHA1 Message Date
Arkadiusz Fal
84a283ff1d Fix player size handling 2022-04-04 00:33:09 +02:00
Arkadiusz Fal
13688adb6e Minor fix 2022-04-04 00:18:49 +02:00
Arkadiusz Fal
f5b6c97003 Bump build and version number 2022-04-03 18:36:43 +02:00
Arkadiusz Fal
915cae2f5a Fix #86 2022-04-03 17:05:36 +02:00
Arkadiusz Fal
586fb2cee1 Improve EOF handling 2022-04-03 16:46:33 +02:00
Arkadiusz Fal
da0b25b14d Try to patch #78
Issue appears when app switches layout from tab to sidebar navigation
2022-04-03 15:36:58 +02:00
Arkadiusz Fal
d31aeea52a Limit formats available to AVPlayer 2022-04-03 15:36:58 +02:00
Arkadiusz Fal
812e2aef87 Fullscreen handling changes 2022-04-03 15:36:58 +02:00
Arkadiusz Fal
382cb30f2a Remove redunant update of player size 2022-04-03 15:36:58 +02:00
Arkadiusz Fal
91bc909b37 Fix project settings 2022-04-03 15:36:58 +02:00
Arkadiusz Fal
725dadf7c5 Improve keyboard shortcuts 2022-04-03 15:36:58 +02:00
Arkadiusz Fal
4d72844975 Minor fixes 2022-04-02 14:37:13 +02:00
Arkadiusz Fal
dc18e189aa Fix optional 2022-04-02 14:37:13 +02:00
Arkadiusz Fal
02349723a1 Add Package.resolved 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
4094abb8ba Bump build and version number 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
0b213e54ad Controls fixes 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
622dd5ba8a tvOS fixes 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
a77d72e2b6 Close fullscreen and restore portrait on closing player 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
755497eda5 Improve streams quality settings 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
48487f9707 Add tvOS mpv libraries 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
5ef47fc2a9 Fix player window on Mac 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
a26d02271d Minor improvements 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
10e5975b37 Bump version number 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
4178275e66 Add toggle for dislikes 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
207ba49977 Bump version number 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
4193972669 Minor fixes 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
2f395f19ad Add ReturnYoutubeDislike API 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
fc3ab0d8ae Fixes for MPV in macOS 2022-04-02 14:37:12 +02:00
Arkadiusz Fal
931107b8d5 Fix EOF handler 2022-04-02 14:36:45 +02:00
Arkadiusz Fal
a2d84a905c Minor improvements 2022-04-02 14:36:45 +02:00
Arkadiusz Fal
3327ee3fbd Add hide player button cancel action 2022-04-02 14:36:44 +02:00
Arkadiusz Fal
4227bb304e Prevent multiple seeks 2022-04-02 14:36:44 +02:00
Arkadiusz Fal
8d3e5b2a2d Add Now Playing info center updates 2022-04-02 14:36:44 +02:00
Arkadiusz Fal
ac1218a612 Hello, mpv! 🎉 2022-04-02 14:36:44 +02:00
Arkadiusz Fal
7a6820dde2 Reorganize toolbars placement 2022-04-02 14:36:16 +02:00
45 changed files with 285 additions and 792 deletions

View File

@@ -1,8 +1,9 @@
import Foundation import Foundation
extension Video { extension Video {
static var fixtureID: Video.ID = "video-fixture" static var fixtureID: Video.ID {
static var fixtureChannelID: Channel.ID = "channel-fixture" "FIXTURE"
}
static var fixture: Video { static var fixture: Video {
let thumbnailURL = "https://yt3.ggpht.com/ytc/AKedOLR-pT_JEsz_hcaA4Gjx8DHcqJ8mS42aTRqcVy6P7w=s88-c-k-c0x00ffffff-no-rj-mo" let thumbnailURL = "https://yt3.ggpht.com/ytc/AKedOLR-pT_JEsz_hcaA4Gjx8DHcqJ8mS42aTRqcVy6P7w=s88-c-k-c0x00ffffff-no-rj-mo"
@@ -17,7 +18,7 @@ extension Video {
description: "Some relaxing live piano music", description: "Some relaxing live piano music",
genre: "Music", genre: "Music",
channel: Channel( channel: Channel(
id: fixtureChannelID, id: "AbCdEFgHI",
name: "The Channel", name: "The Channel",
thumbnailURL: URL(string: thumbnailURL)!, thumbnailURL: URL(string: thumbnailURL)!,
subscriptionsCount: 2300, subscriptionsCount: 2300,

View File

@@ -53,7 +53,7 @@ final class InstancesModel: ObservableObject {
} }
static func remove(_ instance: Instance) { 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 }) { if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
Defaults[.instances].remove(at: index) Defaults[.instances].remove(at: index)
accounts.forEach { AccountsModel.remove($0) } accounts.forEach { AccountsModel.remove($0) }

View File

@@ -160,11 +160,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
private func pathPattern(_ path: String) -> String { private func pathPattern(_ path: String) -> String {
"**\(Self.basePath)/\(path)" "**\(InvidiousAPI.basePath)/\(path)"
} }
private func basePathAppending(_ path: String) -> String { private func basePathAppending(_ path: String) -> String {
"\(Self.basePath)/\(path)" "\(InvidiousAPI.basePath)/\(path)"
} }
private var cookieHeader: String { private var cookieHeader: String {
@@ -172,11 +172,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
var popular: Resource? { 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 { 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("type", category?.name)
.withParam("region", country.rawValue) .withParam("region", country.rawValue)
} }
@@ -186,7 +186,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
var feed: Resource? { var feed: Resource? {
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed") resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/auth/feed")
} }
var subscriptions: Resource? { var subscriptions: Resource? {
@@ -239,66 +239,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
playlist(playlistID)?.child("videos").child(videoID) 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? { func channelPlaylist(_ id: String) -> Resource? {
resource(baseURL: account.url, path: basePathAppending("playlists/\(id)")) resource(baseURL: account.url, path: basePathAppending("playlists/\(id)"))
} }

View File

@@ -4,7 +4,7 @@ import Siesta
import SwiftyJSON import SwiftyJSON
final class PipedAPI: Service, ObservableObject, VideosAPI { final class PipedAPI: Service, ObservableObject, VideosAPI {
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"] static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe"]
@Published var account: Account! @Published var account: Account!
@@ -43,11 +43,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
self.extractChannelPlaylist(from: content.json) 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 configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
self.extractVideo(from: content.json) self.extractVideo(from: content.json)
} }
@@ -86,10 +81,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled) 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 { if account.token.isNil {
updateToken() updateToken()
} }
@@ -179,9 +170,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
var home: Resource? { nil } var home: Resource? { nil }
var popular: Resource? { nil } var popular: Resource? { nil }
var playlists: Resource? { var playlists: Resource? { nil }
resource(baseURL: account.instance.apiURL, path: "user/playlists")
}
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) { func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
resource(baseURL: account.instance.apiURL, path: "subscribe") resource(baseURL: account.instance.apiURL, path: "subscribe")
@@ -195,79 +184,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
.onCompletion { _ in onCompletion() } .onCompletion { _ in onCompletion() }
} }
func playlist(_ id: String) -> Resource? { func playlist(_: String) -> Resource? { nil }
channelPlaylist(id)
}
func playlistVideo(_: String, _: String) -> Resource? { nil } func playlistVideo(_: String, _: String) -> Resource? { nil }
func playlistVideos(_: 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? { func comments(_ id: Video.ID, page: String?) -> Resource? {
let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)" let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)"
let resource = resource(baseURL: account.url, path: path) let resource = resource(baseURL: account.url, path: path)
@@ -364,7 +284,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
} }
return ChannelPlaylist( return ChannelPlaylist(
id: id, id: id,
title: details["name"]?.stringValue ?? "", title: details["name"]!.stringValue,
thumbnailURL: thumbnailURL, thumbnailURL: thumbnailURL,
channel: extractChannel(from: json)!, channel: extractChannel(from: json)!,
videos: videos, videos: videos,
@@ -394,7 +314,6 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url
let subscriptionsCount = details["uploaderSubscriberCount"]?.int
let uploaded = details["uploaded"]?.doubleValue let uploaded = details["uploaded"]?.doubleValue
var published = uploaded.isNil ? nil : (uploaded! / 1000).formattedAsRelativeTime() var published = uploaded.isNil ? nil : (uploaded! / 1000).formattedAsRelativeTime()
@@ -412,7 +331,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
published: published!, published: published!,
views: details["views"]!.intValue, views: details["views"]!.intValue,
description: extractDescription(from: content), description: extractDescription(from: content),
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount), channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL),
thumbnails: thumbnails, thumbnails: thumbnails,
live: live, live: live,
likes: details["likes"]?.int, likes: details["likes"]?.int,
@@ -444,14 +363,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? { private func extractDescription(from content: JSON) -> String? {
guard var description = content.dictionaryValue["description"]?.string else { guard var description = content.dictionaryValue["description"]?.string else {
return nil return nil

View File

@@ -27,34 +27,6 @@ protocol VideosAPI {
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource? func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
func playlistVideos(_ id: 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 channelPlaylist(_ id: String) -> Resource?
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void) func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)

View File

@@ -32,18 +32,6 @@ enum VideosApp: String, CaseIterable {
} }
var supportsUserPlaylists: Bool { var supportsUserPlaylists: Bool {
true
}
var userPlaylistsEndpointIncludesVideos: Bool {
self == .invidious
}
var userPlaylistsHaveVisibility: Bool {
self == .invidious
}
var userPlaylistsAreEditable: Bool {
self == .invidious self == .invidious
} }

View File

@@ -74,10 +74,6 @@ final class NavigationModel: ObservableObject {
navigationStyle: NavigationStyle, navigationStyle: NavigationStyle,
delay: Bool = true delay: Bool = true
) { ) {
guard channel.id != Video.fixtureChannelID else {
return
}
let recent = RecentItem(from: channel) let recent = RecentItem(from: channel)
#if os(macOS) #if os(macOS)
Windows.main.open() Windows.main.open()

View File

@@ -36,9 +36,8 @@ final class AVPlayerBackend: PlayerBackend {
} }
private(set) var avPlayer = AVPlayer() private(set) var avPlayer = AVPlayer()
var controller: AppleAVPlayerViewController? var controller: AppleAVPlayerViewController?
var startPictureInPictureOnPlay = false
var switchToMPVOnPipClose = false
private var asset: AVURLAsset? private var asset: AVURLAsset?
private var composition = AVMutableComposition() private var composition = AVMutableComposition()
@@ -323,8 +322,6 @@ final class AVPlayerBackend: PlayerBackend {
} }
} }
} }
self.setRate(self.model.currentRate)
} }
let replaceItemAndSeek = { let replaceItemAndSeek = {
@@ -444,7 +441,7 @@ final class AVPlayerBackend: PlayerBackend {
self, self,
selector: #selector(itemDidPlayToEndTime), selector: #selector(itemDidPlayToEndTime),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: model.playerItem object: playerItem
) )
} }
@@ -452,7 +449,7 @@ final class AVPlayerBackend: PlayerBackend {
NotificationCenter.default.removeObserver( NotificationCenter.default.removeObserver(
self, self,
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: model.playerItem object: playerItem
) )
} }
@@ -472,9 +469,6 @@ final class AVPlayerBackend: PlayerBackend {
model.hide() model.hide()
#endif #endif
} else { } else {
if model.playingInPictureInPicture {
startPictureInPictureOnPlay = true
}
model.advanceToNextItem() model.advanceToNextItem()
} }
} }
@@ -541,27 +535,14 @@ final class AVPlayerBackend: PlayerBackend {
} }
if player.timeControlStatus != .waitingToPlayAtSpecifiedRate { 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 DispatchQueue.main.async { [weak self] in
self?.model.objectWillChange.send() self?.model.objectWillChange.send()
} }
} }
if player.timeControlStatus == .playing { if player.timeControlStatus == .playing, player.rate != self.model.currentRate {
if player.rate != self.model.currentRate {
player.rate = self.model.currentRate player.rate = self.model.currentRate
} }
}
#if os(macOS) #if os(macOS)
if player.timeControlStatus == .playing { if player.timeControlStatus == .playing {

View File

@@ -41,9 +41,7 @@ final class MPVBackend: PlayerBackend {
updateControlsIsPlaying() updateControlsIsPlaying()
#if !os(macOS) #if !os(macOS)
DispatchQueue.main.async { UIApplication.shared.isIdleTimerDisabled = model.presentingPlayer && isPlaying
UIApplication.shared.isIdleTimerDisabled = self.model.presentingPlayer && self.isPlaying
}
#endif #endif
}} }}
var playerItemDuration: CMTime? var playerItemDuration: CMTime?
@@ -69,33 +67,16 @@ final class MPVBackend: PlayerBackend {
clientTimer.eventHandler = getClientUpdates clientTimer.eventHandler = getClientUpdates
} }
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? { func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
streams streams
.filter { $0.kind != .hls && $0.resolution <= maxResolution.value } .filter { $0.kind == .adaptive && $0.resolution <= maxResolution.value }
.max { lhs, rhs in .max { $0.resolution < $1.resolution } ??
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
} ??
streams.first { $0.kind == .hls } ?? streams.first { $0.kind == .hls } ??
streams.first streams.first
} }
func canPlay(_ stream: Stream) -> Bool { 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) { func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading _: Bool) {
@@ -128,7 +109,7 @@ final class MPVBackend: PlayerBackend {
if !preservingTime, if !preservingTime,
let segment = self.model.sponsorBlock.segments.first, let segment = self.model.sponsorBlock.segments.first,
segment.start > 4, segment.start < 3,
self.model.lastSkipped.isNil self.model.lastSkipped.isNil
{ {
self.seek(to: segment.endTime) { finished in self.seek(to: segment.endTime) { finished in
@@ -201,8 +182,6 @@ final class MPVBackend: PlayerBackend {
startControlsUpdates() startControlsUpdates()
} }
setRate(model.currentRate)
client?.play() client?.play()
} }
@@ -237,8 +216,8 @@ final class MPVBackend: PlayerBackend {
} }
} }
func setRate(_ rate: Float) { func setRate(_: Float) {
client?.setDoubleAsync("speed", Double(rate)) // TODO: Implement rate change
} }
func closeItem() {} func closeItem() {}
@@ -271,8 +250,6 @@ final class MPVBackend: PlayerBackend {
clientTimer.resume() clientTimer.resume()
} }
private var handleSegmentsThrottle = Throttle(interval: 1)
private func getClientUpdates() { private func getClientUpdates() {
self.logger.info("getting client updates") self.logger.info("getting client updates")
@@ -285,11 +262,9 @@ final class MPVBackend: PlayerBackend {
model.updateNowPlayingInfo() model.updateNowPlayingInfo()
handleSegmentsThrottle.execute {
if let currentTime = currentTime { if let currentTime = currentTime {
model.handleSegments(at: currentTime) model.handleSegments(at: currentTime)
} }
}
timeObserverThrottle.execute { timeObserverThrottle.execute {
self.model.updateWatch() self.model.updateWatch()

View File

@@ -225,11 +225,6 @@ final class MPVClient: ObservableObject {
mpv_set_property_async(mpv, 0, name, MPV_FORMAT_FLAG, &data) 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 { private func getDouble(_ name: String) -> Double {
var data = Double() var data = Double()
mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &data) mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &data)

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 import SwiftUI
final class PlayerControlsModel: ObservableObject { final class PlayerControlsModel: ObservableObject {
@Published var isLoadingVideo = false @Published var isLoadingVideo = true
@Published var isPlaying = true @Published var isPlaying = true
@Published var currentTime = CMTime.zero @Published var currentTime = CMTime.zero
@Published var duration = CMTime.zero @Published var duration = CMTime.zero

View File

@@ -66,6 +66,8 @@ final class PlayerModel: ObservableObject {
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI() @Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
@Published var channelWithDetails: Channel?
#if os(iOS) #if os(iOS)
@Published var motionManager: CMMotionManager! @Published var motionManager: CMMotionManager!
@Published var lockedOrientation: UIInterfaceOrientation? @Published var lockedOrientation: UIInterfaceOrientation?
@@ -83,8 +85,6 @@ final class PlayerModel: ObservableObject {
var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
@Published var playingInPictureInPicture = false @Published var playingInPictureInPicture = false
var pipController: AVPictureInPictureController?
var pipDelegate = PiPDelegate()
@Published var presentingErrorDetails = false @Published var presentingErrorDetails = false
var playerError: Error? { didSet { var playerError: Error? { didSet {
@@ -104,9 +104,6 @@ final class PlayerModel: ObservableObject {
#endif #endif
private var currentArtwork: MPMediaItemArtwork? private var currentArtwork: MPMediaItemArtwork?
#if !os(macOS)
var playerLayerView: PlayerLayerView!
#endif
init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil, controls: PlayerControlsModel? = nil) { init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil, controls: PlayerControlsModel? = nil) {
self.accounts = accounts ?? AccountsModel() self.accounts = accounts ?? AccountsModel()
@@ -206,7 +203,7 @@ final class PlayerModel: ObservableObject {
backend.pause() 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) playNow(video, at: time)
guard !playingInPictureInPicture else { guard !playingInPictureInPicture else {
@@ -234,7 +231,11 @@ final class PlayerModel: ObservableObject {
self?.sponsorBlock.loadSegments( self?.sponsorBlock.loadSegments(
videoID: video.videoID, videoID: video.videoID,
categories: Defaults[.sponsorBlockCategories] categories: Defaults[.sponsorBlockCategories]
) ) {
if Defaults[.showChannelSubscribers] {
self?.loadCurrentItemChannelDetails()
}
}
guard Defaults[.enableReturnYouTubeDislike] else { guard Defaults[.enableReturnYouTubeDislike] else {
return return
@@ -379,6 +380,36 @@ final class PlayerModel: ObservableObject {
[activeBackend == PlayerBackendType.mpv ? avPlayerBackend : mpvBackend] [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 { func rateLabel(_ rate: Float) -> String {
let formatter = NumberFormatter() let formatter = NumberFormatter()
formatter.minimumFractionDigits = 0 formatter.minimumFractionDigits = 0

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 { if playingInPictureInPicture, closePiPOnNavigation {
closePiP() 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 { if !playingInPictureInPicture {
backend.closeItem() backend.closeItem()
} }
@@ -64,7 +64,7 @@ extension PlayerModel {
currentItem = item currentItem = item
if !time.isNil { if !time.isNil {
currentItem.playbackTime = time currentItem.playbackTime = .secondsInDefaultTimescale(time!)
} else if currentItem.playbackTime.isNil { } else if currentItem.playbackTime.isNil {
currentItem.playbackTime = .zero currentItem.playbackTime = .zero
} }
@@ -74,6 +74,7 @@ extension PlayerModel {
} }
preservedTime = currentItem.playbackTime preservedTime = currentItem.playbackTime
restoreLoadedChannel()
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let video = self?.currentVideo else { guard let video = self?.currentVideo else {
@@ -105,12 +106,13 @@ extension PlayerModel {
} }
} }
func advanceToItem(_ newItem: PlayerQueueItem, at time: CMTime? = nil) { func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
prepareCurrentItemForHistory() prepareCurrentItemForHistory()
remove(newItem) remove(newItem)
currentItem = newItem currentItem = newItem
pause()
accounts.api.loadDetails(newItem) { newItem in accounts.api.loadDetails(newItem) { newItem in
self.playItem(newItem, video: newItem.video, at: time) self.playItem(newItem, video: newItem.video, at: time)
@@ -151,6 +153,7 @@ extension PlayerModel {
if play { if play {
currentItem = item currentItem = item
// pause playing current video as it's going to be replaced with next one // pause playing current video as it's going to be replaced with next one
pause()
} }
queue.insert(item, at: prepending ? 0 : queue.endIndex) queue.insert(item, at: prepending ? 0 : queue.endIndex)
@@ -175,8 +178,8 @@ extension PlayerModel {
} }
} }
func playHistory(_ item: PlayerQueueItem, at time: CMTime? = nil) { func playHistory(_ item: PlayerQueueItem) {
var time = time ?? item.playbackTime var time = item.playbackTime
if item.shouldRestartPlaying { if item.shouldRestartPlaying {
time = .zero time = .zero
@@ -196,17 +199,8 @@ extension PlayerModel {
return return
} }
var restoredQueue = [PlayerQueueItem?]() queue = ([Defaults[.lastPlayed]] + Defaults[.queue]).compactMap { $0 }
if let lastPlayed = Defaults[.lastPlayed],
!Defaults[.queue].contains(where: { $0.videoID == lastPlayed.videoID })
{
restoredQueue.append(lastPlayed)
Defaults[.lastPlayed] = nil Defaults[.lastPlayed] = nil
}
restoredQueue.append(contentsOf: Defaults[.queue])
queue = restoredQueue.compactMap { $0 }
queue.forEach { item in queue.forEach { item in
accounts.api.loadDetails(item) { newItem in accounts.api.loadDetails(item) { newItem in

View File

@@ -50,8 +50,7 @@ extension PlayerModel {
private func shouldSkip(_ segment: Segment, at time: CMTime) -> Bool { private func shouldSkip(_ segment: Segment, at time: CMTime) -> Bool {
guard isPlaying, guard isPlaying,
!restoredSegments.contains(segment), !restoredSegments.contains(segment),
Defaults[.sponsorBlockCategories].contains(segment.category), Defaults[.sponsorBlockCategories].contains(segment.category)
segment.start > 4
else { else {
return false return false
} }

View File

@@ -18,11 +18,11 @@ struct Playlist: Identifiable, Equatable, Hashable {
var title: String var title: String
var visibility: Visibility var visibility: Visibility
var updated: TimeInterval? var updated: TimeInterval
var videos = [Video]() 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.id = id
self.title = title self.title = title
self.visibility = visibility self.visibility = visibility

View File

@@ -4,7 +4,6 @@ import SwiftUI
final class PlaylistsModel: ObservableObject { final class PlaylistsModel: ObservableObject {
@Published var playlists = [Playlist]() @Published var playlists = [Playlist]()
@Published var reloadPlaylists = false
var accounts = AccountsModel() var accounts = AccountsModel()
@@ -59,22 +58,26 @@ final class PlaylistsModel: ObservableObject {
onSuccess: @escaping () -> Void = {}, onSuccess: @escaping () -> Void = {},
onFailure: @escaping (RequestError) -> Void = { _ in } onFailure: @escaping (RequestError) -> Void = { _ in }
) { ) {
accounts.api.addVideoToPlaylist(videoID, playlistID, onFailure: onFailure) { let resource = accounts.api.playlistVideos(playlistID)
self.load(force: true) { let body = ["videoId": videoID]
self.reloadPlaylists.toggle()
resource?
.request(.post, json: body)
.onSuccess { _ in
self.load(force: true)
onSuccess() onSuccess()
} }
} .onFailure(onFailure)
} }
func removeVideo(index: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) { func removeVideo(videoIndexID: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) {
accounts.api.removeVideoFromPlaylist(index, playlistID, onFailure: { _ in }) { let resource = accounts.api.playlistVideo(playlistID, videoIndexID)
self.load(force: true) {
self.reloadPlaylists.toggle() resource?.request(.delete).onSuccess { _ in
self.load(force: true)
onSuccess() onSuccess()
} }
} }
}
private var resource: Resource? { private var resource: Resource? {
accounts.api.playlists accounts.api.playlists

View File

@@ -13,7 +13,7 @@ final class SponsorBlockAPI: ObservableObject {
@Published var segments = [Segment]() @Published var segments = [Segment]()
static func categoryDescription(_ name: String) -> String? { static func categoryDescription(_ name: String) -> String? {
guard Self.categories.contains(name) else { guard SponsorBlockAPI.categories.contains(name) else {
return nil return nil
} }
@@ -30,7 +30,7 @@ final class SponsorBlockAPI: ObservableObject {
} }
static func categoryDetails(_ name: String) -> String? { static func categoryDetails(_ name: String) -> String? {
guard Self.categories.contains(name) else { guard SponsorBlockAPI.categories.contains(name) else {
return nil return nil
} }

View File

@@ -5,23 +5,12 @@ import Foundation
// swiftlint:disable:next final_class // swiftlint:disable:next final_class
class Stream: Equatable, Hashable, Identifiable { class Stream: Equatable, Hashable, Identifiable {
enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable { enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable {
case hd4320p60
case hd4320p
case hd2160p60
case hd2160p50
case hd2160p48
case hd2160p case hd2160p
case hd1440p60 case hd1440p60
case hd1440p50
case hd1440p48
case hd1440p case hd1440p
case hd1080p60 case hd1080p60
case hd1080p50
case hd1080p48
case hd1080p case hd1080p
case hd720p60 case hd720p60
case hd720p50
case hd720p48
case hd720p case hd720p
case sd480p case sd480p
case sd360p case sd360p
@@ -79,49 +68,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() let id = UUID()
var instance: Instance! var instance: Instance!
@@ -131,7 +77,6 @@ class Stream: Equatable, Hashable, Identifiable {
var resolution: Resolution! var resolution: Resolution!
var kind: Kind! var kind: Kind!
var format: Format!
var encoding: String! var encoding: String!
var videoFormat: String! var videoFormat: String!
@@ -153,7 +98,7 @@ class Stream: Equatable, Hashable, Identifiable {
self.resolution = resolution self.resolution = resolution
self.kind = kind self.kind = kind
self.encoding = encoding self.encoding = encoding
format = .from(videoFormat ?? "") self.videoFormat = videoFormat
} }
var quality: String { var quality: String {
@@ -164,8 +109,23 @@ class Stream: Equatable, Hashable, Identifiable {
return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")" 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 { var description: String {
let formatString = format == .unknown ? "" : " (\(format.rawValue))" let formatString = format == "unknown" ? "" : " (\(format))"
return "\(quality)\(formatString) - \(instance?.description ?? "")" return "\(quality)\(formatString) - \(instance?.description ?? "")"
} }

View File

@@ -17,7 +17,7 @@ struct Video: Identifiable, Equatable, Hashable {
var genre: String? var genre: String?
// index used when in the Playlist // index used when in the Playlist
var indexID: String? let indexID: String?
var live: Bool var live: Bool
var upcoming: Bool var upcoming: Bool

View File

@@ -30,7 +30,7 @@ You can leave your feedback in [discussion on v1.4 release](https://github.com/y
| User Accounts | ✅ | ✅ | | User Accounts | ✅ | ✅ |
| Subscriptions | ✅ | ✅ | | Subscriptions | ✅ | ✅ |
| Popular | ✅ | 🔴 | | Popular | ✅ | 🔴 |
| User Playlists | ✅ | | | User Playlists | ✅ | 🔴 |
| Trending | ✅ | ✅ | | Trending | ✅ | ✅ |
| Channels | ✅ | ✅ | | Channels | ✅ | ✅ |
| Channel Playlists | ✅ | ✅ | | Channel Playlists | ✅ | ✅ |

View File

@@ -51,6 +51,7 @@ extension Defaults.Keys {
static let playerInstanceID = Key<Instance.ID?>("playerInstance") static let playerInstanceID = Key<Instance.ID?>("playerInstance")
static let showKeywords = Key<Bool>("showKeywords", default: false) static let showKeywords = Key<Bool>("showKeywords", default: false)
static let showHistoryInPlayer = Key<Bool>("showHistoryInPlayer", 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) static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: kavinPipedInstanceID)
#if !os(tvOS) #if !os(tvOS)
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate) static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
@@ -96,9 +97,6 @@ extension Defaults.Keys {
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable { enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
case best case best
case hd4320p60
case hd4320p
case hd2160p60
case hd2160p case hd2160p
case hd1440p60 case hd1440p60
case hd1440p case hd1440p
@@ -114,7 +112,7 @@ enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
var value: Stream.Resolution { var value: Stream.Resolution {
switch self { switch self {
case .best: case .best:
return .hd4320p60 return .hd2160p
default: default:
return Stream.Resolution(rawValue: rawValue)! return Stream.Resolution(rawValue: rawValue)!
} }
@@ -124,14 +122,8 @@ enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
switch self { switch self {
case .best: case .best:
return "Best available quality" return "Best available quality"
case .hd4320p60:
return "8K, 60fps"
case .hd4320p:
return "8K"
case .hd2160p60:
return "4K, 60fps"
case .hd2160p: case .hd2160p:
return "4K" return "4K, 60fps"
default: default:
return value.name return value.name
} }

View File

@@ -11,7 +11,9 @@ struct AppSidebarPlaylists: View {
NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) { NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) {
LazyView(PlaylistVideosView(playlist)) LazyView(PlaylistVideosView(playlist))
} label: { } label: {
playlistLabel(playlist) Label(playlist.title, systemImage: RecentsModel.symbolSystemImage(playlist.title))
.backport
.badge(Text("\(playlist.videos.count)"))
} }
.id(playlist.id) .id(playlist.id)
.contextMenu { .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 { var newPlaylistButton: some View {
Button(action: { navigation.presentNewPlaylistForm() }) { Button(action: { navigation.presentNewPlaylistForm() }) {
Label("New Playlist", systemImage: "plus.circle") Label("New Playlist", systemImage: "plus.circle")

View File

@@ -1,14 +1,25 @@
import AVKit
import Defaults import Defaults
import SwiftUI import SwiftUI
struct AppleAVPlayerView: UIViewRepresentable { struct AppleAVPlayerView: UIViewControllerRepresentable {
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<SubscriptionsModel> private var subscriptions
func makeUIView(context _: Context) -> some UIView { func makeUIViewController(context _: Context) -> UIViewController {
player.playerLayerView = PlayerLayerView(frame: .zero) let controller = AppleAVPlayerViewController()
return player.playerLayerView
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

@@ -29,7 +29,6 @@ struct PlayerControls: View {
ZStack(alignment: .bottom) { ZStack(alignment: .bottom) {
VStack(spacing: 0) { VStack(spacing: 0) {
Group { Group {
HStack {
statusBar statusBar
.padding(3) .padding(3)
#if os(macOS) #if os(macOS)
@@ -39,9 +38,6 @@ struct PlayerControls: View {
#endif #endif
.mask(RoundedRectangle(cornerRadius: 3)) .mask(RoundedRectangle(cornerRadius: 3))
Spacer()
}
buttonsBar buttonsBar
.padding(.top, 4) .padding(.top, 4)
.padding(.horizontal, 4) .padding(.horizontal, 4)
@@ -79,11 +75,18 @@ struct PlayerControls: View {
model.resetTimer() model.resetTimer()
} }
#else #else
.background(PlayerGestures()) .background(controlsBackground)
#endif #endif
.environment(\.colorScheme, .dark) .environment(\.colorScheme, .dark)
} }
#if !os(tvOS)
var controlsBackground: some View {
PlayerGestures()
.background(Color.black.opacity(model.presentingControls ? 0.5 : 0))
}
#endif
var timeline: some View { var timeline: some View {
TimelineView(duration: durationBinding, current: currentTimeBinding, cornerRadius: 0) TimelineView(duration: durationBinding, current: currentTimeBinding, cornerRadius: 0)
} }
@@ -109,7 +112,7 @@ struct PlayerControls: View {
#endif #endif
Text(playbackStatus) Text(playbackStatus)
Text("") Spacer()
#if !os(tvOS) #if !os(tvOS)
ToggleBackendButton() ToggleBackendButton()
@@ -117,7 +120,7 @@ struct PlayerControls: View {
StreamControl() StreamControl()
#if os(macOS) #if os(macOS)
.frame(maxWidth: 300) .frame(maxWidth: 160)
#endif #endif
#else #else
Text(player.stream?.description ?? "") Text(player.stream?.description ?? "")
@@ -173,13 +176,8 @@ struct PlayerControls: View {
HStack { HStack {
#if !os(tvOS) #if !os(tvOS)
fullscreenButton fullscreenButton
#if os(iOS)
pipButton
#endif #endif
rateButton
Spacer() Spacer()
#endif
// button("Music Mode", systemImage: "music.note") // button("Music Mode", systemImage: "music.note")
} }
} }
@@ -196,68 +194,6 @@ struct PlayerControls: View {
#endif #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 { var mediumButtonsBar: some View {
HStack { HStack {
#if !os(tvOS) #if !os(tvOS)

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 Defaults
import Foundation import Foundation
import SwiftUI import SwiftUI
@@ -12,30 +11,15 @@ struct PlayerQueueRow: View {
@Default(.closePiPOnNavigation) var closePiPOnNavigation @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 { var body: some View {
Group { Group {
Button { Button {
player.prepareCurrentItemForHistory() player.prepareCurrentItemForHistory()
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
if history { if history {
player.playHistory(item, at: watchStoppedAt) player.playHistory(item)
} else { } else {
player.advanceToItem(item, at: watchStoppedAt) player.advanceToItem(item)
} }
if fullScreen { if fullScreen {
@@ -48,21 +32,9 @@ struct PlayerQueueRow: View {
player.closePiP() player.closePiP()
} }
} label: { } label: {
VideoBanner(video: item.video, playbackTime: watchStoppedAt, videoDuration: watch?.videoDuration) VideoBanner(video: item.video, playbackTime: item.playbackTime, videoDuration: item.videoDuration)
} }
.buttonStyle(.plain) .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)) { Section(header: Text(instance.longDescription)) {
ForEach(kinds, id: \.self) { key in ForEach(kinds, id: \.self) { key in
ForEach(instanceStreams[key] ?? []) { stream in ForEach(instanceStreams[key] ?? []) { stream in
Text(stream.description).tag(Stream?.some(stream)) Text(stream.quality).tag(Stream?.some(stream))
} }
if kinds.count > 1 { if kinds.count > 1 {

View File

@@ -30,6 +30,7 @@ struct VideoDetails: View {
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SubscriptionsModel> private var subscriptions @EnvironmentObject<SubscriptionsModel> private var subscriptions
@Default(.showChannelSubscribers) private var showChannelSubscribers
@Default(.showKeywords) private var showKeywords @Default(.showKeywords) private var showKeywords
init( init(
@@ -203,6 +204,7 @@ struct VideoDetails: View {
.font(.system(size: 14)) .font(.system(size: 14))
.bold() .bold()
if showChannelSubscribers {
Group { Group {
if let subscribers = video!.channel.subscriptionsString { if let subscribers = video!.channel.subscriptionsString {
Text("\(subscribers) subscribers") Text("\(subscribers) subscribers")
@@ -213,6 +215,7 @@ struct VideoDetails: View {
} }
} }
} }
}
.contentShape(RoundedRectangle(cornerRadius: 12)) .contentShape(RoundedRectangle(cornerRadius: 12))
.contextMenu { .contextMenu {
if let video = video { if let video = video {

View File

@@ -26,7 +26,7 @@ struct VideoDetailsPaddingModifier: ViewModifier {
self.geometry = geometry self.geometry = geometry
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
self.additionalPadding = additionalPadding ?? Self.defaultAdditionalDetailsPadding self.additionalPadding = additionalPadding ?? VideoDetailsPaddingModifier.defaultAdditionalDetailsPadding
self.fullScreen = fullScreen self.fullScreen = fullScreen
} }

View File

@@ -226,17 +226,6 @@ struct VideoPlayerView: View {
}) })
case .appleAVPlayer: case .appleAVPlayer:
player.avPlayerView 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) #if !os(tvOS)
@@ -285,7 +274,7 @@ struct VideoPlayerView: View {
Spacer() Spacer()
} }
.contentShape(Rectangle()) .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 { func pictureInPicturePlaceholder(geometry: GeometryProxy) -> some View {
@@ -314,7 +303,7 @@ struct VideoPlayerView: View {
} }
} }
.contentShape(Rectangle()) .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 { var sidebarQueue: Bool {

View File

@@ -9,7 +9,6 @@ struct AddToPlaylistView: View {
@State private var error = "" @State private var error = ""
@State private var presentingErrorAlert = false @State private var presentingErrorAlert = false
@State private var submitButtonDisabled = false
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
@@ -123,7 +122,7 @@ struct AddToPlaylistView: View {
HStack { HStack {
Spacer() Spacer()
Button("Add to Playlist", action: addToPlaylist) Button("Add to Playlist", action: addToPlaylist)
.disabled(submitButtonDisabled || selectedPlaylist.isNil) .disabled(selectedPlaylist.isNil)
.padding(.top, 30) .padding(.top, 30)
.alert(isPresented: $presentingErrorAlert) { .alert(isPresented: $presentingErrorAlert) {
Alert( Alert(
@@ -166,8 +165,6 @@ struct AddToPlaylistView: View {
Defaults[.lastUsedPlaylistID] = id Defaults[.lastUsedPlaylistID] = id
submitButtonDisabled = true
model.addVideo( model.addVideo(
playlistID: id, playlistID: id,
videoID: video.videoID, videoID: video.videoID,
@@ -177,7 +174,6 @@ struct AddToPlaylistView: View {
onFailure: { requestError in onFailure: { requestError in
error = "(\(requestError.httpStatusCode ?? -1)) \(requestError.userMessage)" error = "(\(requestError.httpStatusCode ?? -1)) \(requestError.userMessage)"
presentingErrorAlert = true presentingErrorAlert = true
submitButtonDisabled = false
} }
) )
} }

View File

@@ -43,13 +43,10 @@ struct PlaylistFormView: View {
TextField("Name", text: $name, onCommit: validate) TextField("Name", text: $name, onCommit: validate)
.frame(maxWidth: 450) .frame(maxWidth: 450)
.padding(.leading, 10) .padding(.leading, 10)
.disabled(editing && !accounts.app.userPlaylistsAreEditable)
if accounts.app.userPlaylistsHaveVisibility {
visibilityFormItem visibilityFormItem
.pickerStyle(.segmented) .pickerStyle(.segmented)
} }
}
#if os(macOS) #if os(macOS)
.padding(.horizontal) .padding(.horizontal)
#endif #endif
@@ -62,7 +59,7 @@ struct PlaylistFormView: View {
Spacer() Spacer()
Button("Save", action: submitForm) Button("Save", action: submitForm)
.disabled(!valid || (editing && !accounts.app.userPlaylistsAreEditable)) .disabled(!valid)
.alert(isPresented: $presentingErrorAlert) { .alert(isPresented: $presentingErrorAlert) {
Alert( Alert(
title: Text("Error when accessing playlist"), title: Text("Error when accessing playlist"),
@@ -78,7 +75,7 @@ struct PlaylistFormView: View {
#if os(iOS) #if os(iOS)
.padding(.vertical) .padding(.vertical)
#else #else
.frame(width: 400, height: accounts.app.userPlaylistsHaveVisibility ? 150 : 120) .frame(width: 400, height: 150)
#endif #endif
#else #else
@@ -122,10 +119,8 @@ struct PlaylistFormView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
TextField("Playlist Name", text: $name, onCommit: validate) TextField("Playlist Name", text: $name, onCommit: validate)
.disabled(editing && !accounts.app.userPlaylistsAreEditable)
} }
if accounts.app.userPlaylistsHaveVisibility {
HStack { HStack {
Text("Visibility") Text("Visibility")
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@@ -133,13 +128,11 @@ struct PlaylistFormView: View {
visibilityFormItem visibilityFormItem
} }
.padding(.top, 10) .padding(.top, 10)
}
HStack { HStack {
Spacer() Spacer()
Button("Save", action: submitForm) Button("Save", action: submitForm).disabled(!valid)
.disabled(!valid || (editing && !accounts.app.userPlaylistsAreEditable))
} }
.padding(.top, 40) .padding(.top, 40)
@@ -179,15 +172,27 @@ struct PlaylistFormView: View {
return return
} }
accounts.api.playlistForm(name, visibility.rawValue, playlist: playlist, onFailure: { error in let body = ["title": name, "privacy": visibility.rawValue]
formError = "(\(error.httpStatusCode ?? -1)) \(error.userMessage)"
presentingErrorAlert = true resource?
}) { modifiedPlaylist in .request(editing ? .patch : .post, json: body)
self.playlist = modifiedPlaylist .onSuccess { response in
if let modifiedPlaylist: Playlist = response.typedContent() {
playlist = modifiedPlaylist
}
playlists.load(force: true) playlists.load(force: true)
presentationMode.wrappedValue.dismiss() 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 { var visibilityFormItem: some View {
@@ -231,14 +236,17 @@ struct PlaylistFormView: View {
} }
func deletePlaylistAndDismiss() { func deletePlaylistAndDismiss() {
accounts.api.deletePlaylist(playlist, onFailure: { error in accounts.api.playlist(playlist.id)?
formError = "(\(error.httpStatusCode ?? -1)) \(error.localizedDescription)" .request(.delete)
presentingErrorAlert = true .onSuccess { _ in
}) {
playlist = nil playlist = nil
playlists.load(force: true) playlists.load(force: true)
presentationMode.wrappedValue.dismiss() 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 showingEditPlaylist = false
@State private var editedPlaylist: Playlist? @State private var editedPlaylist: Playlist?
@StateObject private var store = Store<ChannelPlaylist>()
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var model @EnvironmentObject<PlaylistsModel> private var model
@@ -20,36 +18,7 @@ struct PlaylistsView: View {
@Namespace private var focusNamespace @Namespace private var focusNamespace
var items: [ContentItem] { var items: [ContentItem] {
var videos = currentPlaylist?.videos ?? [] ContentItem.array(of: 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
} }
var body: some View { var body: some View {
@@ -122,12 +91,6 @@ struct PlaylistsView: View {
.onChange(of: accounts.current) { _ in .onChange(of: accounts.current) { _ in
model.load(force: true) model.load(force: true)
} }
.onChange(of: selectedPlaylistID) { _ in
resource?.load()
}
.onChange(of: model.reloadPlaylists) { _ in
resource?.load()
}
#if os(tvOS) #if os(tvOS)
.fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) { .fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) {
PlaylistFormView(playlist: $createdPlaylist) PlaylistFormView(playlist: $createdPlaylist)

View File

@@ -14,6 +14,7 @@ struct PlayerSettings: View {
@Default(.playerSidebar) private var playerSidebar @Default(.playerSidebar) private var playerSidebar
@Default(.showHistoryInPlayer) private var showHistory @Default(.showHistoryInPlayer) private var showHistory
@Default(.showKeywords) private var showKeywords @Default(.showKeywords) private var showKeywords
@Default(.showChannelSubscribers) private var channelSubscribers
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
#if os(iOS) #if os(iOS)
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock @Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
@@ -84,6 +85,7 @@ struct PlayerSettings: View {
keywordsToggle keywordsToggle
showHistoryToggle showHistoryToggle
channelSubscribersToggle
returnYouTubeDislikeToggle returnYouTubeDislikeToggle
} }
@@ -197,6 +199,10 @@ struct PlayerSettings: View {
Toggle("Show history", isOn: $showHistory) Toggle("Show history", isOn: $showHistory)
} }
private var channelSubscribersToggle: some View {
Toggle("Show subscribers count", isOn: $channelSubscribers)
}
private var returnYouTubeDislikeToggle: some View { private var returnYouTubeDislikeToggle: some View {
Toggle("Enable Return YouTube Dislike", isOn: $enableReturnYouTubeDislike) Toggle("Enable Return YouTube Dislike", isOn: $enableReturnYouTubeDislike)
} }

View File

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

View File

@@ -16,9 +16,9 @@ struct TrendingCountry: View {
#if !os(tvOS) #if !os(tvOS)
HStack { HStack {
if #available(iOS 15.0, macOS 12.0, *) { 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 { } else {
TextField(Self.prompt, text: $query) TextField(TrendingCountry.prompt, text: $query)
} }
Button("Done") { selectCountryAndDismiss() } Button("Done") { selectCountryAndDismiss() }
@@ -30,7 +30,7 @@ struct TrendingCountry: View {
countriesList countriesList
} }
#if os(tvOS) #if os(tvOS)
.searchable(text: $query, placement: .automatic, prompt: Text(Self.prompt)) .searchable(text: $query, placement: .automatic, prompt: Text(TrendingCountry.prompt))
.background(Color.black) .background(Color.black)
#endif #endif
} }

View File

@@ -1,4 +1,3 @@
import CoreMedia
import Foundation import Foundation
struct VideoURLParser { struct VideoURLParser {
@@ -12,7 +11,7 @@ struct VideoURLParser {
return queryItemValue("v") return queryItemValue("v")
} }
var time: CMTime? { var time: TimeInterval? {
guard let time = queryItemValue("t") else { guard let time = queryItemValue("t") else {
return nil return nil
} }
@@ -25,13 +24,13 @@ struct VideoURLParser {
let seconds = TimeInterval(timeComponents["seconds"] ?? "0") let seconds = TimeInterval(timeComponents["seconds"] ?? "0")
else { else {
if let time = TimeInterval(time) { if let time = TimeInterval(time) {
return .secondsInDefaultTimescale(time) return time
} }
return nil return nil
} }
return .secondsInDefaultTimescale(seconds + (minutes * 60) + (hours * 60 * 60)) return seconds + (minutes * 60) + (hours * 60 * 60)
} }
func queryItemValue(_ name: String) -> String? { func queryItemValue(_ name: String) -> String? {

View File

@@ -1,4 +1,3 @@
import CoreMedia
import Defaults import Defaults
import SDWebImageSwiftUI import SDWebImageSwiftUI
import SwiftUI import SwiftUI
@@ -63,7 +62,6 @@ struct VideoCell: View {
} }
private func playAction() { private func playAction() {
DispatchQueue.main.async {
guard video.videoID != Video.fixtureID else { guard video.videoID != Video.fixtureID else {
return return
} }
@@ -82,20 +80,17 @@ struct VideoCell: View {
return return
} }
var playAt: CMTime? var playAt: TimeInterval?
if playNowContinues, if playNowContinues,
!watch.isNil, !watch.isNil,
!watch!.finished !watch!.finished
{ {
playAt = .secondsInDefaultTimescale(watch!.stoppedAt) playAt = watch!.stoppedAt
} }
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
player.play(video, at: playAt, inNavigationView: inNavigationView) player.play(video, at: playAt, inNavigationView: inNavigationView)
} }
}
private var playNowContinues: Bool { private var playNowContinues: Bool {
watchedVideoPlayNowBehavior == .continue watchedVideoPlayNowBehavior == .continue

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)) .font(.system(size: 30))
.frame(minWidth: 30) .frame(minWidth: 30)

View File

@@ -6,35 +6,9 @@ struct PlaylistVideosView: View {
@Environment(\.inNavigationView) private var inNavigationView @Environment(\.inNavigationView) private var inNavigationView
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var model
@StateObject private var store = Store<ChannelPlaylist>()
var contentItems: [ContentItem] { var contentItems: [ContentItem] {
var videos = playlist.videos ContentItem.array(of: 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
} }
var videos: [Video] { var videos: [Video] {
@@ -48,14 +22,6 @@ struct PlaylistVideosView: View {
var body: some View { var body: some View {
BrowserPlayerControls { BrowserPlayerControls {
VerticalCells(items: contentItems) VerticalCells(items: contentItems)
.onAppear {
if !player.accounts.app.userPlaylistsEndpointIncludesVideos {
resource?.load()
}
}
.onChange(of: model.reloadPlaylists) { _ in
resource?.load()
}
#if !os(tvOS) #if !os(tvOS)
.navigationTitle("\(playlist.title) Playlist") .navigationTitle("\(playlist.title) Playlist")
#endif #endif

View File

@@ -111,7 +111,7 @@ struct VideoContextMenuView: View {
private var continueButton: some View { private var continueButton: some View {
Button { Button {
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt), inNavigationView: inNavigationView) player.play(video, at: watch!.stoppedAt, inNavigationView: inNavigationView)
} label: { } label: {
Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause") Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause")
} }
@@ -201,7 +201,7 @@ struct VideoContextMenuView: View {
func removeFromPlaylistButton(playlistID: String) -> some View { func removeFromPlaylistButton(playlistID: String) -> some View {
Button { Button {
playlists.removeVideo(index: video.indexID!, playlistID: playlistID) playlists.removeVideo(videoIndexID: video.indexID!, playlistID: playlistID)
} label: { } label: {
Label("Remove from Playlist", systemImage: "text.badge.minus") Label("Remove from Playlist", systemImage: "text.badge.minus")
} }

View File

@@ -190,11 +190,6 @@
372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; 372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
372915E72687E3B900F5A35B /* 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 */; }; 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 */; }; 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */; };
3730F75A2733481E00F385FC /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; }; 3730F75A2733481E00F385FC /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
373197D92732015300EF734F /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; }; 373197D92732015300EF734F /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
@@ -869,8 +864,6 @@
3727B74927872A920021C15E /* VisualEffectBlur-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisualEffectBlur-iOS.swift"; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; };
@@ -1358,7 +1351,6 @@
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */, 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
37E8B0EB27B326C00024006F /* TimelineView.swift */, 37E8B0EB27B326C00024006F /* TimelineView.swift */,
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */, 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */,
373031F22838388A000CFD59 /* PlayerLayerView.swift */,
); );
path = Player; path = Player;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1447,7 +1439,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
37EBD8C227AF0D7C00F1C24B /* Backends */, 37EBD8C227AF0D7C00F1C24B /* Backends */,
373031F428383A89000CFD59 /* PiPDelegate.swift */,
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */, 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
37319F0427103F94004ECCD0 /* PlayerQueue.swift */, 37319F0427103F94004ECCD0 /* PlayerQueue.swift */,
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */, 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */,
@@ -2557,7 +2548,6 @@
37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */, 37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */,
373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */, 373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */,
3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */,
373031F32838388A000CFD59 /* PlayerLayerView.swift in Sources */,
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, 37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */,
37C3A251272366440087A57A /* ChannelPlaylistView.swift in Sources */, 37C3A251272366440087A57A /* ChannelPlaylistView.swift in Sources */,
@@ -2610,7 +2600,6 @@
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */, 375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */, 3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
373031F528383A89000CFD59 /* PiPDelegate.swift in Sources */,
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */, 37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */, 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
@@ -2769,7 +2758,6 @@
37A9965F26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37A9965F26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */, 37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */,
37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */,
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, 37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
@@ -3012,8 +3000,6 @@
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */, 37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */,
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */, 37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */,
372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */,
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */,
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */, 37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
@@ -3103,7 +3089,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -3116,7 +3102,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -3137,7 +3123,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -3150,7 +3136,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -3169,7 +3155,7 @@
buildSettings = { buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -3181,7 +3167,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -3201,7 +3187,7 @@
buildSettings = { buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -3213,7 +3199,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -3365,7 +3351,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_CXX_LANGUAGE_STANDARD = "c++14"; CLANG_CXX_LANGUAGE_STANDARD = "c++14";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
@@ -3389,7 +3375,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Vendor/mpv/iOS/lib", "$(PROJECT_DIR)/Vendor/mpv/iOS/lib",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
OTHER_LDFLAGS = "-lstdc++"; OTHER_LDFLAGS = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
@@ -3407,7 +3393,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1"; GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
@@ -3427,7 +3413,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Vendor/mpv/iOS/lib", "$(PROJECT_DIR)/Vendor/mpv/iOS/lib",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
OTHER_LDFLAGS = "-lstdc++"; OTHER_LDFLAGS = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
@@ -3449,7 +3435,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -3468,7 +3454,7 @@
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib", "$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = macosx; SDKROOT = macosx;
@@ -3487,7 +3473,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -3506,7 +3492,7 @@
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib", "$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = macosx; SDKROOT = macosx;
@@ -3623,7 +3609,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -3643,7 +3629,7 @@
"$(PROJECT_DIR)/Vendor/mpv", "$(PROJECT_DIR)/Vendor/mpv",
"$(PROJECT_DIR)/Vendor/mpv/tvOS", "$(PROJECT_DIR)/Vendor/mpv/tvOS",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = appletvos; SDKROOT = appletvos;
@@ -3661,7 +3647,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -3681,7 +3667,7 @@
"$(PROJECT_DIR)/Vendor/mpv", "$(PROJECT_DIR)/Vendor/mpv",
"$(PROJECT_DIR)/Vendor/mpv/tvOS", "$(PROJECT_DIR)/Vendor/mpv/tvOS",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4.alpha.3;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = appletvos; SDKROOT = appletvos;

View File

@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git", "location" : "https://github.com/Alamofire/Alamofire.git",
"state" : { "state" : {
"revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", "revision" : "f82c23a8a7ef8dc1a49a8bfc6a96883e79121864",
"version" : "5.6.1" "version" : "5.5.0"
} }
}, },
{ {
@@ -96,7 +96,7 @@
"location" : "https://github.com/sparkle-project/Sparkle", "location" : "https://github.com/sparkle-project/Sparkle",
"state" : { "state" : {
"branch" : "2.x", "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 func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
Self.instance = self AppDelegate.instance = self
return true return true
} }
} }