mirror of
https://github.com/yattee/yattee.git
synced 2024-12-23 05:53:41 +00:00
Player controls UI changes
WIP on controls Chapters working Add previews variable Add lists ids WIP
This commit is contained in:
parent
a912079eac
commit
ba1115fe2a
@ -5,9 +5,19 @@ disabled_rules:
|
|||||||
- opening_brace
|
- opening_brace
|
||||||
- number_separator
|
- number_separator
|
||||||
- multiline_arguments
|
- multiline_arguments
|
||||||
|
opt_in_rules:
|
||||||
|
- conditional_returns_on_newline
|
||||||
|
- implicit_return
|
||||||
excluded:
|
excluded:
|
||||||
- Vendor
|
- Vendor
|
||||||
- Tests Apple TV
|
- Tests Apple TV
|
||||||
- Tests iOS
|
- Tests iOS
|
||||||
- Tests macOS
|
- Tests macOS
|
||||||
|
|
||||||
|
conditional_returns_on_newline:
|
||||||
|
if_only: true
|
||||||
|
|
||||||
|
implicit_return:
|
||||||
|
included:
|
||||||
|
- function
|
||||||
|
- getter
|
||||||
|
15
Extensions/Color+Debug.swift
Normal file
15
Extensions/Color+Debug.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension ShapeStyle where Self == Color {
|
||||||
|
static var debug: Color {
|
||||||
|
#if DEBUG
|
||||||
|
return Color(
|
||||||
|
red: .random(in: 0 ... 1),
|
||||||
|
green: .random(in: 0 ... 1),
|
||||||
|
blue: .random(in: 0 ... 1)
|
||||||
|
)
|
||||||
|
#else
|
||||||
|
return Color(.clear)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Double {
|
extension Double {
|
||||||
func formattedAsPlaybackTime() -> String? {
|
func formattedAsPlaybackTime(allowZero: Bool = false) -> String? {
|
||||||
guard !isZero, isFinite else {
|
guard allowZero || !isZero, isFinite else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import UIKit
|
|||||||
|
|
||||||
extension UIViewController {
|
extension UIViewController {
|
||||||
@objc var swizzle_prefersHomeIndicatorAutoHidden: Bool {
|
@objc var swizzle_prefersHomeIndicatorAutoHidden: Bool {
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
public class func swizzleHomeIndicatorProperty() {
|
public class func swizzleHomeIndicatorProperty() {
|
||||||
|
@ -6,6 +6,7 @@ extension Video {
|
|||||||
|
|
||||||
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"
|
||||||
|
let chapterImageURL = URL(string: "https://pipedproxy.kavin.rocks/vi/rr2XfL_df3o/hqdefault_29633.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg%3D%3D&rs=AOn4CLDFDm9D5SvsIA7D3v5n5KZahLs_UA&host=i.ytimg.com")!
|
||||||
|
|
||||||
return Video(
|
return Video(
|
||||||
videoID: fixtureID,
|
videoID: fixtureID,
|
||||||
@ -29,7 +30,12 @@ extension Video {
|
|||||||
publishedAt: Date(),
|
publishedAt: Date(),
|
||||||
likes: 37333,
|
likes: 37333,
|
||||||
dislikes: 30,
|
dislikes: 30,
|
||||||
keywords: ["very", "cool", "video", "msfs 2020", "757", "747", "A380", "737-900", "MOD", "Zibo", "MD80", "MD11", "Rotate", "Laminar", "787", "A350", "MSFS", "MS2020", "Microsoft Flight Simulator", "Microsoft", "Flight", "Simulator", "SIM", "World", "Ortho", "Flying", "Boeing MAX"]
|
keywords: ["very", "cool", "video", "msfs 2020", "757", "747", "A380", "737-900", "MOD", "Zibo", "MD80", "MD11", "Rotate", "Laminar", "787", "A350", "MSFS", "MS2020", "Microsoft Flight Simulator", "Microsoft", "Flight", "Simulator", "SIM", "World", "Ortho", "Flying", "Boeing MAX"],
|
||||||
|
chapters: [
|
||||||
|
.init(title: "A good chapter name", image: chapterImageURL, start: 20),
|
||||||
|
.init(title: "Other fine but incredibly too long chapter name, I don't know what else to say", image: chapterImageURL, start: 30),
|
||||||
|
.init(title: "Short", image: chapterImageURL, start: 60)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,9 +9,11 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
|||||||
.environmentObject(InstancesModel())
|
.environmentObject(InstancesModel())
|
||||||
.environmentObject(invidious)
|
.environmentObject(invidious)
|
||||||
.environmentObject(NavigationModel())
|
.environmentObject(NavigationModel())
|
||||||
|
.environmentObject(NetworkStateModel())
|
||||||
.environmentObject(PipedAPI())
|
.environmentObject(PipedAPI())
|
||||||
.environmentObject(player)
|
.environmentObject(player)
|
||||||
.environmentObject(PlayerControlsModel())
|
.environmentObject(playerControls)
|
||||||
|
.environmentObject(PlayerTimeModel())
|
||||||
.environmentObject(PlaylistsModel())
|
.environmentObject(PlaylistsModel())
|
||||||
.environmentObject(RecentsModel())
|
.environmentObject(RecentsModel())
|
||||||
.environmentObject(SearchModel())
|
.environmentObject(SearchModel())
|
||||||
@ -37,6 +39,10 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
|||||||
return player
|
return player
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var playerControls: PlayerControlsModel {
|
||||||
|
PlayerControlsModel(presentingControls: true, player: player)
|
||||||
|
}
|
||||||
|
|
||||||
private var subscriptions: SubscriptionsModel {
|
private var subscriptions: SubscriptionsModel {
|
||||||
let subscriptions = SubscriptionsModel()
|
let subscriptions = SubscriptionsModel()
|
||||||
|
|
||||||
|
@ -383,6 +383,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
id = videoID
|
id = videoID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let description = json["description"].stringValue
|
||||||
|
|
||||||
return Video(
|
return Video(
|
||||||
id: id,
|
id: id,
|
||||||
videoID: videoID,
|
videoID: videoID,
|
||||||
@ -391,7 +393,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
length: json["lengthSeconds"].doubleValue,
|
length: json["lengthSeconds"].doubleValue,
|
||||||
published: json["publishedText"].stringValue,
|
published: json["publishedText"].stringValue,
|
||||||
views: json["viewCount"].intValue,
|
views: json["viewCount"].intValue,
|
||||||
description: json["description"].stringValue,
|
description: description,
|
||||||
genre: json["genre"].stringValue,
|
genre: json["genre"].stringValue,
|
||||||
channel: extractChannel(from: json),
|
channel: extractChannel(from: json),
|
||||||
thumbnails: extractThumbnails(from: json),
|
thumbnails: extractThumbnails(from: json),
|
||||||
@ -403,7 +405,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
dislikes: json["dislikeCount"].int,
|
dislikes: json["dislikeCount"].int,
|
||||||
keywords: json["keywords"].arrayValue.compactMap { $0.string },
|
keywords: json["keywords"].arrayValue.compactMap { $0.string },
|
||||||
streams: extractStreams(from: json),
|
streams: extractStreams(from: json),
|
||||||
related: extractRelated(from: json)
|
related: extractRelated(from: json),
|
||||||
|
chapters: extractChapters(from: description)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,6 +409,13 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
let live = details["livestream"]?.bool ?? (details["duration"]?.int == -1)
|
let live = details["livestream"]?.bool ?? (details["duration"]?.int == -1)
|
||||||
|
|
||||||
|
let description = extractDescription(from: content) ?? ""
|
||||||
|
|
||||||
|
var chapters = extractChapters(from: content)
|
||||||
|
if chapters.isEmpty, !description.isEmpty {
|
||||||
|
chapters = extractChapters(from: description)
|
||||||
|
}
|
||||||
|
|
||||||
return Video(
|
return Video(
|
||||||
videoID: extractID(from: content),
|
videoID: extractID(from: content),
|
||||||
title: details["title"]?.string ?? "",
|
title: details["title"]?.string ?? "",
|
||||||
@ -416,14 +423,15 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
length: details["duration"]?.double ?? 0,
|
length: details["duration"]?.double ?? 0,
|
||||||
published: published ?? "",
|
published: published ?? "",
|
||||||
views: details["views"]?.int ?? 0,
|
views: details["views"]?.int ?? 0,
|
||||||
description: extractDescription(from: content),
|
description: description,
|
||||||
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
|
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
|
||||||
thumbnails: thumbnails,
|
thumbnails: thumbnails,
|
||||||
live: live,
|
live: live,
|
||||||
likes: details["likes"]?.int,
|
likes: details["likes"]?.int,
|
||||||
dislikes: details["dislikes"]?.int,
|
dislikes: details["dislikes"]?.int,
|
||||||
streams: extractStreams(from: content),
|
streams: extractStreams(from: content),
|
||||||
related: extractRelated(from: content)
|
related: extractRelated(from: content),
|
||||||
|
chapters: extractChapters(from: content)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,4 +579,21 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
channel: Channel(id: channelId, name: author)
|
channel: Channel(id: channelId, name: author)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func extractChapters(from content: JSON) -> [Chapter] {
|
||||||
|
guard let chapters = content.dictionaryValue["chapters"]?.array else {
|
||||||
|
return .init()
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters.compactMap { chapter in
|
||||||
|
guard let title = chapter["title"].string,
|
||||||
|
let image = chapter["image"].url,
|
||||||
|
let start = chapter["start"].double
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Chapter(title: title, image: image, start: start)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,4 +116,54 @@ extension VideosAPI {
|
|||||||
|
|
||||||
return urlComponents.url
|
return urlComponents.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractChapters(from description: String) -> [Chapter] {
|
||||||
|
guard let chaptersRegularExpression = try? NSRegularExpression(
|
||||||
|
pattern: "(?<start>(?:[0-9]+:){1,}(?:[0-9]+))(?:\\s)+(?:- ?)?(?<title>.*)",
|
||||||
|
options: .caseInsensitive
|
||||||
|
) else { return [] }
|
||||||
|
|
||||||
|
let chapterLines = chaptersRegularExpression.matches(
|
||||||
|
in: description,
|
||||||
|
range: NSRange(description.startIndex..., in: description)
|
||||||
|
)
|
||||||
|
|
||||||
|
return chapterLines.compactMap { line in
|
||||||
|
let titleRange = line.range(withName: "title")
|
||||||
|
let startRange = line.range(withName: "start")
|
||||||
|
|
||||||
|
guard let titleSubstringRange = Range(titleRange, in: description),
|
||||||
|
let startSubstringRange = Range(startRange, in: description),
|
||||||
|
let titleCapture = String(description[titleSubstringRange]),
|
||||||
|
let startCapture = String(description[startSubstringRange]) else { return nil }
|
||||||
|
|
||||||
|
let startComponents = startCapture.components(separatedBy: ":")
|
||||||
|
guard startComponents.count <= 3 else { return nil }
|
||||||
|
|
||||||
|
var hours: Double?
|
||||||
|
var minutes: Double?
|
||||||
|
var seconds: Double?
|
||||||
|
|
||||||
|
if startComponents.count == 3 {
|
||||||
|
hours = Double(startComponents[0])
|
||||||
|
minutes = Double(startComponents[1])
|
||||||
|
seconds = Double(startComponents[2])
|
||||||
|
} else if startComponents.count == 2 {
|
||||||
|
minutes = Double(startComponents[0])
|
||||||
|
seconds = Double(startComponents[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
guard var startSeconds = seconds else { return nil }
|
||||||
|
|
||||||
|
if let minutes = minutes {
|
||||||
|
startSeconds += 60 * minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
if let hours = hours {
|
||||||
|
startSeconds += 60 * 60 * hours
|
||||||
|
}
|
||||||
|
|
||||||
|
return .init(title: titleCapture, start: startSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
8
Model/Chapter.swift
Normal file
8
Model/Chapter.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Chapter: Identifiable, Equatable {
|
||||||
|
var id = UUID()
|
||||||
|
var title: String
|
||||||
|
var image: URL?
|
||||||
|
var start: Double
|
||||||
|
}
|
@ -40,12 +40,12 @@ final class CommentsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func load(page: String? = nil) {
|
func load(page: String? = nil) {
|
||||||
guard Self.enabled else {
|
guard Self.enabled, !loaded else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !Self.instance.isNil,
|
guard !Self.instance.isNil,
|
||||||
!(player?.currentVideo.isNil ?? true)
|
let video = player.currentVideo
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ final class CommentsModel: ObservableObject {
|
|||||||
|
|
||||||
firstPage = page.isNil || page!.isEmpty
|
firstPage = page.isNil || page!.isEmpty
|
||||||
|
|
||||||
api?.comments(player.currentVideo!.videoID, page: page)?
|
api?.comments(video.videoID, page: page)?
|
||||||
.load()
|
.load()
|
||||||
.onSuccess { [weak self] response in
|
.onSuccess { [weak self] response in
|
||||||
if let page: CommentsPage = response.typedContent() {
|
if let page: CommentsPage = response.typedContent() {
|
||||||
|
@ -66,6 +66,10 @@ final class NavigationModel: ObservableObject {
|
|||||||
@Published var presentingSettings = false
|
@Published var presentingSettings = false
|
||||||
@Published var presentingWelcomeScreen = false
|
@Published var presentingWelcomeScreen = false
|
||||||
|
|
||||||
|
@Published var presentingAlert = false
|
||||||
|
@Published var alertTitle = ""
|
||||||
|
@Published var alertMessage = ""
|
||||||
|
|
||||||
static func openChannel(
|
static func openChannel(
|
||||||
_ channel: Channel,
|
_ channel: Channel,
|
||||||
player: PlayerModel,
|
player: PlayerModel,
|
||||||
@ -181,6 +185,12 @@ final class NavigationModel: ObservableObject {
|
|||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentAlert(title: String, message: String) {
|
||||||
|
alertTitle = title
|
||||||
|
alertMessage = message
|
||||||
|
presentingAlert = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias TabSelection = NavigationModel.TabSelection
|
typealias TabSelection = NavigationModel.TabSelection
|
||||||
|
42
Model/NetworkStateModel.swift
Normal file
42
Model/NetworkStateModel.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class NetworkStateModel: ObservableObject {
|
||||||
|
@Published var pausedForCache = false
|
||||||
|
@Published var cacheDuration = 0.0
|
||||||
|
@Published var bufferingState = 0.0
|
||||||
|
|
||||||
|
var player: PlayerModel!
|
||||||
|
|
||||||
|
var fullStateText: String? {
|
||||||
|
guard let bufferingStateText = bufferingStateText,
|
||||||
|
let cacheDurationText = cacheDurationText
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(bufferingStateText) (\(cacheDurationText))"
|
||||||
|
}
|
||||||
|
|
||||||
|
var bufferingStateText: String? {
|
||||||
|
guard detailsAvailable else { return nil }
|
||||||
|
return String(format: "%.0f%%", bufferingState)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheDurationText: String? {
|
||||||
|
guard detailsAvailable else { return nil }
|
||||||
|
return String(format: "%.2fs", cacheDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
var detailsAvailable: Bool {
|
||||||
|
guard let player = player else { return false }
|
||||||
|
return player.activeBackend.supportsNetworkStateBufferingDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsUpdates: Bool {
|
||||||
|
if let player = player {
|
||||||
|
return pausedForCache || player.isSeeking || player.isLoadingVideo
|
||||||
|
}
|
||||||
|
|
||||||
|
return pausedForCache
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
|
|
||||||
var model: PlayerModel!
|
var model: PlayerModel!
|
||||||
var controls: PlayerControlsModel!
|
var controls: PlayerControlsModel!
|
||||||
|
var playerTime: PlayerTimeModel!
|
||||||
|
var networkState: NetworkStateModel!
|
||||||
|
|
||||||
var stream: Stream?
|
var stream: Stream?
|
||||||
var video: Video?
|
var video: Video?
|
||||||
@ -31,6 +33,11 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
avPlayer.timeControlStatus == .playing
|
avPlayer.timeControlStatus == .playing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isSeeking: Bool {
|
||||||
|
// TODO: implement this maybe?
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
var playerItemDuration: CMTime? {
|
var playerItemDuration: CMTime? {
|
||||||
avPlayer.currentItem?.asset.duration
|
avPlayer.currentItem?.asset.duration
|
||||||
}
|
}
|
||||||
@ -52,9 +59,10 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
|
|
||||||
private var timeObserverThrottle = Throttle(interval: 2)
|
private var timeObserverThrottle = Throttle(interval: 2)
|
||||||
|
|
||||||
init(model: PlayerModel, controls: PlayerControlsModel?) {
|
init(model: PlayerModel, controls: PlayerControlsModel?, playerTime: PlayerTimeModel?) {
|
||||||
self.model = model
|
self.model = model
|
||||||
self.controls = controls
|
self.controls = controls
|
||||||
|
self.playerTime = playerTime
|
||||||
|
|
||||||
addFrequentTimeObserver()
|
addFrequentTimeObserver()
|
||||||
addInfrequentTimeObserver()
|
addInfrequentTimeObserver()
|
||||||
@ -493,8 +501,8 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.controls.duration = self.playerItemDuration ?? .zero
|
self.playerTime.duration = self.playerItemDuration ?? .zero
|
||||||
self.controls.currentTime = self.currentTime ?? .zero
|
self.playerTime.currentTime = self.currentTime ?? .zero
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
self.model.updateNowPlayingInfo()
|
self.model.updateNowPlayingInfo()
|
||||||
@ -581,4 +589,5 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
func stopControlsUpdates() {}
|
func stopControlsUpdates() {}
|
||||||
func setNeedsDrawing(_: Bool) {}
|
func setNeedsDrawing(_: Bool) {}
|
||||||
func setSize(_: Double, _: Double) {}
|
func setSize(_: Double, _: Double) {}
|
||||||
|
func updateNetworkState() {}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ final class MPVBackend: PlayerBackend {
|
|||||||
|
|
||||||
var model: PlayerModel!
|
var model: PlayerModel!
|
||||||
var controls: PlayerControlsModel!
|
var controls: PlayerControlsModel!
|
||||||
|
var playerTime: PlayerTimeModel!
|
||||||
|
var networkState: NetworkStateModel!
|
||||||
|
|
||||||
var stream: Stream?
|
var stream: Stream?
|
||||||
var video: Video?
|
var video: Video?
|
||||||
@ -24,17 +26,22 @@ final class MPVBackend: PlayerBackend {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.controls.isLoadingVideo = self.isLoadingVideo
|
self.controls?.isLoadingVideo = self.isLoadingVideo
|
||||||
|
self.updateNetworkState()
|
||||||
|
|
||||||
if !self.isLoadingVideo {
|
if !self.isLoadingVideo {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
||||||
self?.handleEOF = true
|
self?.handleEOF = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.model?.objectWillChange.send()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
var isPlaying = true { didSet {
|
var isPlaying = true { didSet {
|
||||||
|
updateNetworkState()
|
||||||
|
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
startClientUpdates()
|
startClientUpdates()
|
||||||
} else {
|
} else {
|
||||||
@ -49,6 +56,15 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}}
|
}}
|
||||||
|
var isSeeking = false {
|
||||||
|
didSet {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.model.isSeeking = self.isSeeking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var playerItemDuration: CMTime?
|
var playerItemDuration: CMTime?
|
||||||
|
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
@ -88,9 +104,16 @@ final class MPVBackend: PlayerBackend {
|
|||||||
client?.cacheDuration ?? 0
|
client?.cacheDuration ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
init(model: PlayerModel, controls: PlayerControlsModel? = nil) {
|
init(
|
||||||
|
model: PlayerModel,
|
||||||
|
controls: PlayerControlsModel? = nil,
|
||||||
|
playerTime: PlayerTimeModel? = nil,
|
||||||
|
networkState: NetworkStateModel? = nil
|
||||||
|
) {
|
||||||
self.model = model
|
self.model = model
|
||||||
self.controls = controls
|
self.controls = controls
|
||||||
|
self.playerTime = playerTime
|
||||||
|
self.networkState = networkState
|
||||||
|
|
||||||
clientTimer = .init(timeInterval: Self.controlsUpdateInterval)
|
clientTimer = .init(timeInterval: Self.controlsUpdateInterval)
|
||||||
clientTimer.eventHandler = getClientUpdates
|
clientTimer.eventHandler = getClientUpdates
|
||||||
@ -155,7 +178,6 @@ final class MPVBackend: PlayerBackend {
|
|||||||
|
|
||||||
if !preservingTime,
|
if !preservingTime,
|
||||||
let segment = self.model.sponsorBlock.segments.first,
|
let segment = self.model.sponsorBlock.segments.first,
|
||||||
segment.end > 4,
|
|
||||||
self.model.lastSkipped.isNil
|
self.model.lastSkipped.isNil
|
||||||
{
|
{
|
||||||
self.seek(to: segment.endTime) { finished in
|
self.seek(to: segment.endTime) { finished in
|
||||||
@ -202,7 +224,7 @@ final class MPVBackend: PlayerBackend {
|
|||||||
let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url
|
let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url
|
||||||
let audioTrack = self.model.musicMode ? nil : stream.audioAsset.url
|
let audioTrack = self.model.musicMode ? nil : stream.audioAsset.url
|
||||||
|
|
||||||
self.client.loadFile(fileToLoad, audio: audioTrack, time: time) { [weak self] _ in
|
self.client?.loadFile(fileToLoad, audio: audioTrack, time: time) { [weak self] _ in
|
||||||
self?.isLoadingVideo = true
|
self?.isLoadingVideo = true
|
||||||
self?.pause()
|
self?.pause()
|
||||||
}
|
}
|
||||||
@ -229,7 +251,7 @@ final class MPVBackend: PlayerBackend {
|
|||||||
isPlaying = true
|
isPlaying = true
|
||||||
startClientUpdates()
|
startClientUpdates()
|
||||||
|
|
||||||
if controls.presentingControls {
|
if controls?.presentingControls ?? false {
|
||||||
startControlsUpdates()
|
startControlsUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +276,7 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)?) {
|
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)?) {
|
||||||
client.seek(to: time) { [weak self] _ in
|
client?.seek(to: time) { [weak self] _ in
|
||||||
self?.getClientUpdates()
|
self?.getClientUpdates()
|
||||||
self?.updateControls()
|
self?.updateControls()
|
||||||
completionHandler?(true)
|
completionHandler?(true)
|
||||||
@ -262,7 +284,7 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
||||||
client.seek(relative: time) { [weak self] _ in
|
client?.seek(relative: time) { [weak self] _ in
|
||||||
self?.getClientUpdates()
|
self?.getClientUpdates()
|
||||||
self?.updateControls()
|
self?.updateControls()
|
||||||
completionHandler?(true)
|
completionHandler?(true)
|
||||||
@ -280,13 +302,7 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func enterFullScreen() {
|
func enterFullScreen() {
|
||||||
model.toggleFullscreen(controls?.playingFullscreen ?? false)
|
model.toggleFullscreen(model?.playingFullScreen ?? false)
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
if Defaults[.lockOrientationInFullScreen] {
|
|
||||||
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitFullScreen() {}
|
func exitFullScreen() {}
|
||||||
@ -297,15 +313,13 @@ final class MPVBackend: PlayerBackend {
|
|||||||
guard model.presentingPlayer else {
|
guard model.presentingPlayer else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else {
|
guard let self = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.logger.info("updating controls")
|
self.playerTime.currentTime = self.currentTime ?? .zero
|
||||||
self.controls.currentTime = self.currentTime ?? .zero
|
self.playerTime.duration = self.playerItemDuration ?? .zero
|
||||||
self.controls.duration = self.playerItemDuration ?? .zero
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,13 +389,22 @@ final class MPVBackend: PlayerBackend {
|
|||||||
|
|
||||||
case MPV_EVENT_PLAYBACK_RESTART:
|
case MPV_EVENT_PLAYBACK_RESTART:
|
||||||
isLoadingVideo = false
|
isLoadingVideo = false
|
||||||
|
isSeeking = false
|
||||||
|
|
||||||
onFileLoaded?()
|
onFileLoaded?()
|
||||||
startClientUpdates()
|
startClientUpdates()
|
||||||
onFileLoaded = nil
|
onFileLoaded = nil
|
||||||
|
|
||||||
|
case MPV_EVENT_PAUSE:
|
||||||
|
updateNetworkState()
|
||||||
|
|
||||||
case MPV_EVENT_UNPAUSE:
|
case MPV_EVENT_UNPAUSE:
|
||||||
isLoadingVideo = false
|
isLoadingVideo = false
|
||||||
|
isSeeking = false
|
||||||
|
updateNetworkState()
|
||||||
|
|
||||||
|
case MPV_EVENT_SEEK:
|
||||||
|
isSeeking = true
|
||||||
|
|
||||||
case MPV_EVENT_END_FILE:
|
case MPV_EVENT_END_FILE:
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@ -417,18 +440,41 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setSize(_ width: Double, _ height: Double) {
|
func setSize(_ width: Double, _ height: Double) {
|
||||||
self.client?.setSize(width, height)
|
client?.setSize(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addVideoTrack(_ url: URL) {
|
func addVideoTrack(_ url: URL) {
|
||||||
self.client?.addVideoTrack(url)
|
client?.addVideoTrack(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setVideoToAuto() {
|
func setVideoToAuto() {
|
||||||
self.client?.setVideoToAuto()
|
client?.setVideoToAuto()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setVideoToNo() {
|
func setVideoToNo() {
|
||||||
self.client?.setVideoToNo()
|
client?.setVideoToNo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNetworkState() {
|
||||||
|
guard let client = client, let networkState = networkState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
networkState.pausedForCache = client.pausedForCache
|
||||||
|
networkState.cacheDuration = client.cacheDuration
|
||||||
|
networkState.bufferingState = client.bufferingState
|
||||||
|
}
|
||||||
|
|
||||||
|
if networkState.needsUpdates {
|
||||||
|
dispatchNetworkUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dispatchNetworkUpdate() {
|
||||||
|
print("dispatching network update")
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||||
|
self?.updateNetworkState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,9 @@ final class MPVClient: ObservableObject {
|
|||||||
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
|
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
checkError(mpv_set_option_string(mpv, "cache-pause-initial", "yes"))
|
||||||
|
checkError(mpv_set_option_string(mpv, "cache-secs", "20"))
|
||||||
|
checkError(mpv_set_option_string(mpv, "cache-pause-wait", "2"))
|
||||||
checkError(mpv_set_option_string(mpv, "hwdec", "auto-safe"))
|
checkError(mpv_set_option_string(mpv, "hwdec", "auto-safe"))
|
||||||
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
|
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
|
||||||
|
|
||||||
@ -167,6 +170,10 @@ final class MPVClient: ObservableObject {
|
|||||||
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration"))
|
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pausedForCache: Bool {
|
||||||
|
mpv.isNil ? false : getFlag("paused-for-cache")
|
||||||
|
}
|
||||||
|
|
||||||
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
||||||
guard !seeking else {
|
guard !seeking else {
|
||||||
logger.warning("ignoring seek, another in progress")
|
logger.warning("ignoring seek, another in progress")
|
||||||
@ -262,6 +269,12 @@ final class MPVClient: ObservableObject {
|
|||||||
Int(getString("track-list/count") ?? "-1") ?? -1
|
Int(getString("track-list/count") ?? "-1") ?? -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getFlag(_ name: String) -> Bool {
|
||||||
|
var data = Int64()
|
||||||
|
mpv_get_property(mpv, name, MPV_FORMAT_FLAG, &data)
|
||||||
|
return data > 0
|
||||||
|
}
|
||||||
|
|
||||||
private func setFlagAsync(_ name: String, _ flag: Bool) {
|
private func setFlagAsync(_ name: String, _ flag: Bool) {
|
||||||
var data: Int = flag ? 1 : 0
|
var data: Int = flag ? 1 : 0
|
||||||
mpv_set_property_async(mpv, 0, name, MPV_FORMAT_FLAG, &data)
|
mpv_set_property_async(mpv, 0, name, MPV_FORMAT_FLAG, &data)
|
||||||
|
@ -5,6 +5,8 @@ import Foundation
|
|||||||
protocol PlayerBackend {
|
protocol PlayerBackend {
|
||||||
var model: PlayerModel! { get set }
|
var model: PlayerModel! { get set }
|
||||||
var controls: PlayerControlsModel! { get set }
|
var controls: PlayerControlsModel! { get set }
|
||||||
|
var playerTime: PlayerTimeModel! { get set }
|
||||||
|
var networkState: NetworkStateModel! { get set }
|
||||||
|
|
||||||
var stream: Stream? { get set }
|
var stream: Stream? { get set }
|
||||||
var video: Video? { get set }
|
var video: Video? { get set }
|
||||||
@ -14,6 +16,7 @@ protocol PlayerBackend {
|
|||||||
var isLoadingVideo: Bool { get }
|
var isLoadingVideo: Bool { get }
|
||||||
|
|
||||||
var isPlaying: Bool { get }
|
var isPlaying: Bool { get }
|
||||||
|
var isSeeking: Bool { get }
|
||||||
var playerItemDuration: CMTime? { get }
|
var playerItemDuration: CMTime? { get }
|
||||||
|
|
||||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
|
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
|
||||||
@ -49,6 +52,8 @@ protocol PlayerBackend {
|
|||||||
func startControlsUpdates()
|
func startControlsUpdates()
|
||||||
func stopControlsUpdates()
|
func stopControlsUpdates()
|
||||||
|
|
||||||
|
func updateNetworkState()
|
||||||
|
|
||||||
func setNeedsDrawing(_ needsDrawing: Bool)
|
func setNeedsDrawing(_ needsDrawing: Bool)
|
||||||
func setSize(_ width: Double, _ height: Double)
|
func setSize(_ width: Double, _ height: Double)
|
||||||
}
|
}
|
||||||
|
@ -13,4 +13,8 @@ enum PlayerBackendType: String, CaseIterable, Defaults.Serializable {
|
|||||||
return "AVPlayer"
|
return "AVPlayer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var supportsNetworkStateBufferingDetails: Bool {
|
||||||
|
self == .mpv
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,37 +5,26 @@ import SwiftUI
|
|||||||
final class PlayerControlsModel: ObservableObject {
|
final class PlayerControlsModel: ObservableObject {
|
||||||
@Published var isLoadingVideo = false
|
@Published var isLoadingVideo = false
|
||||||
@Published var isPlaying = true
|
@Published var isPlaying = true
|
||||||
@Published var currentTime = CMTime.zero
|
|
||||||
@Published var duration = CMTime.zero
|
|
||||||
@Published var presentingControls = false { didSet { handlePresentationChange() } }
|
@Published var presentingControls = false { didSet { handlePresentationChange() } }
|
||||||
|
@Published var presentingControlsOverlay = false
|
||||||
@Published var timer: Timer?
|
@Published var timer: Timer?
|
||||||
@Published var playingFullscreen = false
|
|
||||||
|
|
||||||
var player: PlayerModel!
|
var player: PlayerModel!
|
||||||
|
|
||||||
var playbackTime: String {
|
init(
|
||||||
guard let current = currentTime.seconds.formattedAsPlaybackTime(),
|
isLoadingVideo: Bool = false,
|
||||||
let duration = duration.seconds.formattedAsPlaybackTime()
|
isPlaying: Bool = true,
|
||||||
else {
|
presentingControls: Bool = false,
|
||||||
return "--:-- / --:--"
|
presentingControlsOverlay: Bool = false,
|
||||||
}
|
timer: Timer? = nil,
|
||||||
|
player: PlayerModel? = nil
|
||||||
var withoutSegments = ""
|
) {
|
||||||
if let withoutSegmentsDuration = playerItemDurationWithoutSponsorSegments,
|
self.isLoadingVideo = isLoadingVideo
|
||||||
self.duration.seconds != withoutSegmentsDuration
|
self.isPlaying = isPlaying
|
||||||
{
|
self.presentingControls = presentingControls
|
||||||
withoutSegments = " (\(withoutSegmentsDuration.formattedAsPlaybackTime() ?? "--:--"))"
|
self.presentingControlsOverlay = presentingControlsOverlay
|
||||||
}
|
self.timer = timer
|
||||||
|
self.player = player
|
||||||
return "\(current) / \(duration)\(withoutSegments)"
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerItemDurationWithoutSponsorSegments: Double? {
|
|
||||||
guard let duration = player.playerItemDurationWithoutSponsorSegments else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return duration.seconds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePresentationChange() {
|
func handlePresentationChange() {
|
||||||
@ -45,7 +34,7 @@ final class PlayerControlsModel: ObservableObject {
|
|||||||
self?.resetTimer()
|
self?.resetTimer()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
player.backend.stopControlsUpdates()
|
player?.backend.stopControlsUpdates()
|
||||||
timer?.invalidate()
|
timer?.invalidate()
|
||||||
timer = nil
|
timer = nil
|
||||||
}
|
}
|
||||||
@ -91,11 +80,6 @@ final class PlayerControlsModel: ObservableObject {
|
|||||||
presentingControls ? hide() : show()
|
presentingControls ? hide() : show()
|
||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
|
||||||
currentTime = .zero
|
|
||||||
duration = .zero
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetTimer() {
|
func resetTimer() {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
if !presentingControls {
|
if !presentingControls {
|
||||||
|
@ -53,6 +53,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
@Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } }
|
@Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } }
|
||||||
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
|
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
|
||||||
|
@Published var videoBeingOpened: Video?
|
||||||
@Published var historyVideos = [Video]()
|
@Published var historyVideos = [Video]()
|
||||||
|
|
||||||
@Published var preservedTime: CMTime?
|
@Published var preservedTime: CMTime?
|
||||||
@ -65,6 +66,10 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var musicMode = false
|
@Published var musicMode = false
|
||||||
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
||||||
|
|
||||||
|
@Published var isSeeking = false { didSet {
|
||||||
|
backend.updateNetworkState()
|
||||||
|
}}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Published var motionManager: CMMotionManager!
|
@Published var motionManager: CMMotionManager!
|
||||||
@Published var lockedOrientation: UIInterfaceOrientation?
|
@Published var lockedOrientation: UIInterfaceOrientation?
|
||||||
@ -79,9 +84,24 @@ final class PlayerModel: ObservableObject {
|
|||||||
backend.controls = controls
|
backend.controls = controls
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
var playerTime: PlayerTimeModel { didSet {
|
||||||
|
backends.forEach { backend in
|
||||||
|
var backend = backend
|
||||||
|
backend.playerTime = playerTime
|
||||||
|
backend.playerTime.player = self
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
var networkState: NetworkStateModel { didSet {
|
||||||
|
backends.forEach { backend in
|
||||||
|
var backend = backend
|
||||||
|
backend.networkState = networkState
|
||||||
|
backend.networkState.player = self
|
||||||
|
}
|
||||||
|
}}
|
||||||
var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
|
var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
|
||||||
var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
||||||
|
|
||||||
|
@Published var playingFullScreen = false
|
||||||
@Published var playingInPictureInPicture = false
|
@Published var playingInPictureInPicture = false
|
||||||
var pipController: AVPictureInPictureController?
|
var pipController: AVPictureInPictureController?
|
||||||
var pipDelegate = PiPDelegate()
|
var pipDelegate = PiPDelegate()
|
||||||
@ -108,13 +128,31 @@ final class PlayerModel: ObservableObject {
|
|||||||
var playerLayerView: PlayerLayerView!
|
var playerLayerView: PlayerLayerView!
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil, controls: PlayerControlsModel? = nil) {
|
var onPresentPlayer: (() -> Void)?
|
||||||
self.accounts = accounts ?? AccountsModel()
|
|
||||||
self.comments = comments ?? CommentsModel()
|
|
||||||
self.controls = controls ?? PlayerControlsModel()
|
|
||||||
|
|
||||||
self.avPlayerBackend = AVPlayerBackend(model: self, controls: controls)
|
init(
|
||||||
self.mpvBackend = MPVBackend(model: self)
|
accounts: AccountsModel = AccountsModel(),
|
||||||
|
comments: CommentsModel = CommentsModel(),
|
||||||
|
controls: PlayerControlsModel = PlayerControlsModel(),
|
||||||
|
playerTime: PlayerTimeModel = PlayerTimeModel(),
|
||||||
|
networkState: NetworkStateModel = NetworkStateModel()
|
||||||
|
) {
|
||||||
|
self.accounts = accounts
|
||||||
|
self.comments = comments
|
||||||
|
self.controls = controls
|
||||||
|
self.playerTime = playerTime
|
||||||
|
self.networkState = networkState
|
||||||
|
|
||||||
|
self.avPlayerBackend = AVPlayerBackend(
|
||||||
|
model: self,
|
||||||
|
controls: controls,
|
||||||
|
playerTime: playerTime
|
||||||
|
)
|
||||||
|
self.mpvBackend = MPVBackend(
|
||||||
|
model: self,
|
||||||
|
playerTime: playerTime,
|
||||||
|
networkState: networkState
|
||||||
|
)
|
||||||
|
|
||||||
Defaults[.activeBackend] = .mpv
|
Defaults[.activeBackend] = .mpv
|
||||||
}
|
}
|
||||||
@ -136,7 +174,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hide() {
|
func hide() {
|
||||||
controls.playingFullscreen = false
|
playingFullScreen = false
|
||||||
presentingPlayer = false
|
presentingPlayer = false
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@ -176,11 +214,19 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playerItemDuration: CMTime? {
|
var playerItemDuration: CMTime? {
|
||||||
backend.playerItemDuration
|
guard !currentItem.isNil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return backend.playerItemDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerItemDurationWithoutSponsorSegments: CMTime? {
|
var playerItemDurationWithoutSponsorSegments: CMTime? {
|
||||||
(backend.playerItemDuration ?? .zero) - .secondsInDefaultTimescale(
|
guard let playerItemDuration = playerItemDuration, !playerItemDuration.seconds.isZero else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerItemDuration - .secondsInDefaultTimescale(
|
||||||
sponsorBlock.segments.reduce(0) { $0 + $1.duration }
|
sponsorBlock.segments.reduce(0) { $0 + $1.duration }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -212,18 +258,15 @@ final class PlayerModel: ObservableObject {
|
|||||||
func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) {
|
func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) {
|
||||||
pause()
|
pause()
|
||||||
|
|
||||||
var delay = 0.0
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
delay = 0.5
|
if !playingInPictureInPicture, showingPlayer {
|
||||||
#endif
|
onPresentPlayer = { [weak self] in self?.playNow(video, at: time) }
|
||||||
|
show()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
||||||
guard let self = self else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
self.playNow(video, at: time)
|
playNow(video, at: time)
|
||||||
}
|
|
||||||
|
|
||||||
guard !playingInPictureInPicture else {
|
guard !playingInPictureInPicture else {
|
||||||
return
|
return
|
||||||
@ -260,7 +303,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
controls.reset()
|
playerTime.reset()
|
||||||
|
|
||||||
backend.playStream(
|
backend.playStream(
|
||||||
stream,
|
stream,
|
||||||
@ -468,10 +511,13 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
func handleEnterBackground() {
|
func handleEnterBackground() {
|
||||||
setNeedsDrawing(false)
|
setNeedsDrawing(false)
|
||||||
|
if !playingInPictureInPicture, !musicMode {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func enterFullScreen() {
|
func enterFullScreen() {
|
||||||
guard !controls.playingFullscreen else {
|
guard !playingFullScreen else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,13 +527,13 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func exitFullScreen() {
|
func exitFullScreen() {
|
||||||
guard controls.playingFullscreen else {
|
guard playingFullScreen else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("exiting fullscreen")
|
logger.info("exiting fullscreen")
|
||||||
|
|
||||||
if controls.playingFullscreen {
|
if playingFullScreen {
|
||||||
toggleFullscreen(true)
|
toggleFullscreen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,14 +605,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
setNeedsDrawing(false)
|
setNeedsDrawing(false)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
controls.playingFullscreen = !isFullScreen
|
playingFullScreen = !isFullScreen
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||||
self?.setNeedsDrawing(true)
|
self?.setNeedsDrawing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if controls.playingFullscreen {
|
if playingFullScreen {
|
||||||
guard !(UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true) else {
|
guard !(UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -590,12 +636,6 @@ final class PlayerModel: ObservableObject {
|
|||||||
avPlayerBackend.switchToMPVOnPipClose = false
|
avPlayerBackend.switchToMPVOnPipClose = false
|
||||||
closePiP()
|
closePiP()
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
|
||||||
// TODO: initialize mpv on startup on mac
|
|
||||||
if mpvBackend.client.isNil {
|
|
||||||
Windows.player.open()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
changeActiveBackend(from: .appleAVPlayer, to: .mpv)
|
changeActiveBackend(from: .appleAVPlayer, to: .mpv)
|
||||||
controls.presentingControls = true
|
controls.presentingControls = true
|
||||||
controls.removeTimer()
|
controls.removeTimer()
|
||||||
|
@ -20,22 +20,14 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
videosToPlay.dropFirst().reversed().forEach { video in
|
videosToPlay.dropFirst().reversed().forEach { video in
|
||||||
enqueueVideo(video, prepending: true) { _, item in
|
enqueueVideo(video, prepending: true, loadDetails: false)
|
||||||
if item.video == first {
|
|
||||||
self.advanceToItem(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
|
|
||||||
func playNext(_ video: Video) {
|
func playNext(_ video: Video) {
|
||||||
enqueueVideo(video, prepending: true) { _, item in
|
enqueueVideo(video, play: currentItem.isNil, prepending: true)
|
||||||
if self.currentItem.isNil {
|
|
||||||
self.advanceToItem(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func playNow(_ video: Video, at time: CMTime? = nil) {
|
func playNow(_ video: Video, at time: CMTime? = nil) {
|
||||||
@ -45,12 +37,12 @@ extension PlayerModel {
|
|||||||
|
|
||||||
prepareCurrentItemForHistory()
|
prepareCurrentItemForHistory()
|
||||||
|
|
||||||
enqueueVideo(video, prepending: true) { _, item in
|
enqueueVideo(video, play: true, atTime: time, prepending: true) { _, item in
|
||||||
self.advanceToItem(item, at: time)
|
self.advanceToItem(item, at: time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: CMTime? = nil) {
|
func playItem(_ item: PlayerQueueItem, at time: CMTime? = nil) {
|
||||||
if !playingInPictureInPicture {
|
if !playingInPictureInPicture {
|
||||||
backend.closeItem()
|
backend.closeItem()
|
||||||
}
|
}
|
||||||
@ -65,32 +57,25 @@ extension PlayerModel {
|
|||||||
currentItem.playbackTime = .zero
|
currentItem.playbackTime = .zero
|
||||||
}
|
}
|
||||||
|
|
||||||
if video != nil {
|
|
||||||
currentItem.video = video!
|
|
||||||
}
|
|
||||||
|
|
||||||
preservedTime = currentItem.playbackTime
|
preservedTime = currentItem.playbackTime
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let video = self?.currentVideo else {
|
guard let video = self?.currentVideo else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self?.videoBeingOpened = nil
|
||||||
|
|
||||||
self?.loadAvailableStreams(video)
|
if video.streams.isEmpty {
|
||||||
|
self?.loadAvailableStreams(video)
|
||||||
|
} else {
|
||||||
|
guard let instance = self?.accounts.current?.instance ?? InstancesModel.forPlayer ?? InstancesModel.all.first else { return }
|
||||||
|
self?.availableStreams = self?.streamsWithInstance(instance: instance, streams: video.streams) ?? video.streams
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func preferredStream(_ streams: [Stream]) -> Stream? {
|
func preferredStream(_ streams: [Stream]) -> Stream? {
|
||||||
let quality = Defaults[.quality]
|
backend.bestPlayable(streams.filter { backend.canPlay($0) }, maxResolution: Defaults[.quality])
|
||||||
var streams = streams
|
|
||||||
|
|
||||||
if let id = Defaults[.playerInstanceID] {
|
|
||||||
streams = streams.filter { $0.instance.id == id }
|
|
||||||
}
|
|
||||||
|
|
||||||
streams = streams.filter { backend.canPlay($0) }
|
|
||||||
|
|
||||||
return backend.bestPlayable(streams, maxResolution: quality)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func advanceToNextItem() {
|
func advanceToNextItem() {
|
||||||
@ -109,7 +94,7 @@ extension PlayerModel {
|
|||||||
currentItem = newItem
|
currentItem = newItem
|
||||||
|
|
||||||
accounts.api.loadDetails(newItem) { newItem in
|
accounts.api.loadDetails(newItem) { newItem in
|
||||||
self.playItem(newItem, video: newItem.video, at: time)
|
self.playItem(newItem, at: time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,23 +125,29 @@ extension PlayerModel {
|
|||||||
play: Bool = false,
|
play: Bool = false,
|
||||||
atTime: CMTime? = nil,
|
atTime: CMTime? = nil,
|
||||||
prepending: Bool = false,
|
prepending: Bool = false,
|
||||||
|
loadDetails: Bool = true,
|
||||||
videoDetailsLoadHandler: @escaping (Video, PlayerQueueItem) -> Void = { _, _ in }
|
videoDetailsLoadHandler: @escaping (Video, PlayerQueueItem) -> Void = { _, _ in }
|
||||||
) -> PlayerQueueItem? {
|
) -> PlayerQueueItem? {
|
||||||
let item = PlayerQueueItem(video, playbackTime: atTime)
|
let item = PlayerQueueItem(video, playbackTime: atTime)
|
||||||
|
|
||||||
if play {
|
if play {
|
||||||
currentItem = item
|
currentItem = item
|
||||||
// pause playing current video as it's going to be replaced with next one
|
videoBeingOpened = video
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
if loadDetails {
|
||||||
|
accounts.api.loadDetails(item) { [weak self] newItem in
|
||||||
|
guard let self = self else { return }
|
||||||
|
videoDetailsLoadHandler(newItem.video, newItem)
|
||||||
|
|
||||||
accounts.api.loadDetails(item) { newItem in
|
if play {
|
||||||
videoDetailsLoadHandler(newItem.video, newItem)
|
self.playItem(newItem)
|
||||||
|
} else {
|
||||||
if play {
|
self.queue.insert(newItem, at: prepending ? 0 : self.queue.endIndex)
|
||||||
self.playItem(newItem, video: video)
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
@ -2,14 +2,16 @@ import AVFAudio
|
|||||||
import CoreMedia
|
import CoreMedia
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
extension PlayerModel {
|
extension PlayerModel {
|
||||||
func handleSegments(at time: CMTime) {
|
func handleSegments(at time: CMTime) {
|
||||||
if let segment = lastSkipped {
|
if let segment = lastSkipped {
|
||||||
if time > .secondsInDefaultTimescale(segment.end + 10) {
|
if time > .secondsInDefaultTimescale(segment.end + 5) {
|
||||||
resetLastSegment()
|
resetLastSegment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let firstSegment = sponsorBlock.segments.first(where: { $0.timeInSegment(time) }) else {
|
guard let firstSegment = sponsorBlock.segments.first(where: { $0.timeInSegment(time) }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -60,7 +62,9 @@ extension PlayerModel {
|
|||||||
backend.seek(to: segment.endTime)
|
backend.seek(to: segment.endTime)
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.lastSkipped = segment
|
withAnimation {
|
||||||
|
self?.lastSkipped = segment
|
||||||
|
}
|
||||||
self?.segmentRestorationTime = time
|
self?.segmentRestorationTime = time
|
||||||
}
|
}
|
||||||
logger.info("SponsorBlock skipping to: \(segment.end)")
|
logger.info("SponsorBlock skipping to: \(segment.end)")
|
||||||
@ -69,8 +73,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.end > 4
|
|
||||||
else {
|
else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -92,7 +95,9 @@ extension PlayerModel {
|
|||||||
|
|
||||||
private func resetLastSegment() {
|
private func resetLastSegment() {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.lastSkipped = nil
|
withAnimation {
|
||||||
|
self?.lastSkipped = nil
|
||||||
|
}
|
||||||
self?.segmentRestorationTime = nil
|
self?.segmentRestorationTime = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,20 +17,14 @@ extension PlayerModel {
|
|||||||
|
|
||||||
func loadAvailableStreams(_ video: Video) {
|
func loadAvailableStreams(_ video: Video) {
|
||||||
availableStreams = []
|
availableStreams = []
|
||||||
let playerInstance = InstancesModel.forPlayer ?? InstancesModel.all.first
|
|
||||||
|
|
||||||
guard !playerInstance.isNil else {
|
guard let playerInstance = InstancesModel.forPlayer ?? InstancesModel.all.first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("loading streams from \(playerInstance!.description)")
|
logger.info("loading streams from \(playerInstance.description)")
|
||||||
|
|
||||||
fetchStreams(playerInstance!.anonymous.video(video.videoID), instance: playerInstance!, video: video) { _ in
|
fetchStreams(playerInstance.anonymous.video(video.videoID), instance: playerInstance, video: video)
|
||||||
InstancesModel.all.filter { $0 != playerInstance }.forEach { instance in
|
|
||||||
self.logger.info("loading streams from \(instance.description)")
|
|
||||||
self.fetchStreams(instance.anonymous.video(video.videoID), instance: instance, video: video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchStreams(
|
private func fetchStreams(
|
||||||
@ -60,8 +54,12 @@ extension PlayerModel {
|
|||||||
stream.instance = instance
|
stream.instance = instance
|
||||||
|
|
||||||
if instance.app == .invidious {
|
if instance.app == .invidious {
|
||||||
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: stream.audioAsset)
|
if let audio = stream.audioAsset {
|
||||||
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: stream.videoAsset)
|
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio)
|
||||||
|
}
|
||||||
|
if let video = stream.videoAsset {
|
||||||
|
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stream
|
return stream
|
||||||
|
50
Model/Player/PlayerTimeModel.swift
Normal file
50
Model/Player/PlayerTimeModel.swift
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import CoreMedia
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class PlayerTimeModel: ObservableObject {
|
||||||
|
static let timePlaceholder = "--:--"
|
||||||
|
|
||||||
|
@Published var currentTime = CMTime.zero
|
||||||
|
@Published var duration = CMTime.zero
|
||||||
|
|
||||||
|
var player: PlayerModel?
|
||||||
|
|
||||||
|
var currentPlaybackTime: String {
|
||||||
|
if player?.currentItem.isNil ?? true || duration.seconds.isZero {
|
||||||
|
return Self.timePlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentTime.seconds.formattedAsPlaybackTime(allowZero: true) ?? Self.timePlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationPlaybackTime: String {
|
||||||
|
if player?.currentItem.isNil ?? true {
|
||||||
|
return Self.timePlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration.seconds.formattedAsPlaybackTime() ?? Self.timePlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
|
var withoutSegmentsPlaybackTime: String {
|
||||||
|
guard let withoutSegmentsDuration = player?.playerItemDurationWithoutSponsorSegments?.seconds else {
|
||||||
|
return Self.timePlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
|
return withoutSegmentsDuration.formattedAsPlaybackTime() ?? Self.timePlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationAndWithoutSegmentsPlaybackTime: String {
|
||||||
|
var durationAndWithoutSegmentsPlaybackTime = "\(durationPlaybackTime)"
|
||||||
|
|
||||||
|
if withoutSegmentsPlaybackTime != durationPlaybackTime {
|
||||||
|
durationAndWithoutSegmentsPlaybackTime += " (\(withoutSegmentsPlaybackTime))"
|
||||||
|
}
|
||||||
|
|
||||||
|
return durationAndWithoutSegmentsPlaybackTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
currentTime = .zero
|
||||||
|
duration = .zero
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@ -16,6 +17,10 @@ final class PlaylistsModel: ObservableObject {
|
|||||||
playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }
|
playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastUsed: Playlist? {
|
||||||
|
find(id: Defaults[.lastUsedPlaylistID])
|
||||||
|
}
|
||||||
|
|
||||||
func find(id: Playlist.ID?) -> Playlist? {
|
func find(id: Playlist.ID?) -> Playlist? {
|
||||||
if id.isNil {
|
if id.isNil {
|
||||||
return nil
|
return nil
|
||||||
@ -57,9 +62,19 @@ final class PlaylistsModel: ObservableObject {
|
|||||||
playlistID: Playlist.ID,
|
playlistID: Playlist.ID,
|
||||||
videoID: Video.ID,
|
videoID: Video.ID,
|
||||||
onSuccess: @escaping () -> Void = {},
|
onSuccess: @escaping () -> Void = {},
|
||||||
onFailure: @escaping (RequestError) -> Void = { _ in }
|
navigation: NavigationModel?,
|
||||||
|
onFailure: ((RequestError) -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
accounts.api.addVideoToPlaylist(videoID, playlistID, onFailure: onFailure) {
|
accounts.api.addVideoToPlaylist(
|
||||||
|
videoID,
|
||||||
|
playlistID,
|
||||||
|
onFailure: onFailure ?? { requestError in
|
||||||
|
navigation?.presentAlert(
|
||||||
|
title: "Error when adding to playlist",
|
||||||
|
message: "(\(requestError.httpStatusCode ?? -1)) \(requestError.userMessage)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
self.load(force: true) {
|
self.load(force: true) {
|
||||||
self.reloadPlaylists.toggle()
|
self.reloadPlaylists.toggle()
|
||||||
onSuccess()
|
onSuccess()
|
||||||
|
@ -21,6 +21,14 @@ class Segment: ObservableObject, Hashable {
|
|||||||
end - start
|
end - start
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var durationText: String {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.minimumFractionDigits = 0
|
||||||
|
formatter.maximumFractionDigits = 1
|
||||||
|
|
||||||
|
return formatter.string(from: NSNumber(value: duration)) ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
var endTime: CMTime {
|
var endTime: CMTime {
|
||||||
.secondsInDefaultTimescale(end)
|
.secondsInDefaultTimescale(end)
|
||||||
}
|
}
|
||||||
|
@ -91,13 +91,13 @@ class Stream: Equatable, Hashable, Identifiable {
|
|||||||
|
|
||||||
private var sortOrder: Int {
|
private var sortOrder: Int {
|
||||||
switch self {
|
switch self {
|
||||||
case .webm:
|
|
||||||
return 0
|
|
||||||
case .mp4:
|
case .mp4:
|
||||||
return 1
|
return 0
|
||||||
case .avc1:
|
case .avc1:
|
||||||
return 2
|
return 1
|
||||||
case .av1:
|
case .av1:
|
||||||
|
return 2
|
||||||
|
case .webm:
|
||||||
return 3
|
return 3
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return 4
|
return 4
|
||||||
@ -160,17 +160,11 @@ class Stream: Equatable, Hashable, Identifiable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var quality: String {
|
var quality: String {
|
||||||
if resolution == .hd2160p30 {
|
kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
|
||||||
return "4K (2160p)"
|
|
||||||
}
|
|
||||||
|
|
||||||
return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var shortQuality: String {
|
var shortQuality: String {
|
||||||
if resolution?.height == 2160 {
|
if kind == .hls {
|
||||||
return "4K"
|
|
||||||
} else if kind == .hls {
|
|
||||||
return "HLS"
|
return "HLS"
|
||||||
} else {
|
} else {
|
||||||
return resolution?.name ?? "?"
|
return resolution?.name ?? "?"
|
||||||
|
@ -16,7 +16,7 @@ final class ThumbnailsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func best(_ video: Video) -> URL? {
|
func best(_ video: Video) -> URL? {
|
||||||
let qualities = [Thumbnail.Quality.maxresdefault, .medium, .default]
|
let qualities = [Thumbnail.Quality.default]
|
||||||
|
|
||||||
for quality in qualities {
|
for quality in qualities {
|
||||||
let url = video.thumbnailURL(quality: quality)
|
let url = video.thumbnailURL(quality: quality)
|
||||||
|
@ -32,6 +32,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
var channel: Channel
|
var channel: Channel
|
||||||
|
|
||||||
var related = [Video]()
|
var related = [Video]()
|
||||||
|
var chapters = [Chapter]()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: String? = nil,
|
id: String? = nil,
|
||||||
@ -53,7 +54,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
dislikes: Int? = nil,
|
dislikes: Int? = nil,
|
||||||
keywords: [String] = [],
|
keywords: [String] = [],
|
||||||
streams: [Stream] = [],
|
streams: [Stream] = [],
|
||||||
related: [Video] = []
|
related: [Video] = [],
|
||||||
|
chapters: [Chapter] = []
|
||||||
) {
|
) {
|
||||||
self.id = id ?? UUID().uuidString
|
self.id = id ?? UUID().uuidString
|
||||||
self.videoID = videoID
|
self.videoID = videoID
|
||||||
@ -75,6 +77,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
self.keywords = keywords
|
self.keywords = keywords
|
||||||
self.streams = streams
|
self.streams = streams
|
||||||
self.related = related
|
self.related = related
|
||||||
|
self.chapters = chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
var publishedDate: String? {
|
var publishedDate: String? {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
@ -11,6 +12,12 @@ extension Defaults.Keys {
|
|||||||
static let defaultForPauseOnHidingPlayer = false
|
static let defaultForPauseOnHidingPlayer = false
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
static let defaultForPlayerDetailsPageButtonLabelStyle = PlayerDetailsPageButtonLabelStyle.iconAndText
|
||||||
|
#else
|
||||||
|
static let defaultForPlayerDetailsPageButtonLabelStyle = UIDevice.current.userInterfaceIdiom == .phone ? PlayerDetailsPageButtonLabelStyle.iconOnly : .iconAndText
|
||||||
|
#endif
|
||||||
|
|
||||||
static let kavinPipedInstanceID = "kavin-piped"
|
static let kavinPipedInstanceID = "kavin-piped"
|
||||||
static let instances = Key<[Instance]>("instances", default: [
|
static let instances = Key<[Instance]>("instances", default: [
|
||||||
.init(
|
.init(
|
||||||
@ -89,6 +96,10 @@ extension Defaults.Keys {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
||||||
|
|
||||||
|
static let playerDetailsPageButtonLabelStyle = Key<PlayerDetailsPageButtonLabelStyle>("playerDetailsPageButtonLabelStyle", default: defaultForPlayerDetailsPageButtonLabelStyle)
|
||||||
|
|
||||||
|
static let controlsBarInPlayer = Key<Bool>("controlsBarInPlayer", default: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
||||||
@ -200,3 +211,11 @@ enum WatchedVideoPlayNowBehavior: String, Defaults.Serializable {
|
|||||||
case info, separate
|
case info, separate
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
enum PlayerDetailsPageButtonLabelStyle: String, CaseIterable, Defaults.Serializable {
|
||||||
|
case iconOnly, iconAndText
|
||||||
|
|
||||||
|
var text: Bool {
|
||||||
|
self == .iconAndText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -70,7 +70,13 @@ struct FavoritesView: View {
|
|||||||
|
|
||||||
struct Favorites_Previews: PreviewProvider {
|
struct Favorites_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
FavoritesView()
|
TabView {
|
||||||
.injectFixtureEnvironmentObjects()
|
FavoritesView()
|
||||||
|
.overlay(VideoPlayerView().injectFixtureEnvironmentObjects())
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
.tabItem {
|
||||||
|
Label("a", systemImage: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ struct AppSidebarNavigation: View {
|
|||||||
@EnvironmentObject<InstancesModel> private var instances
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
|
||||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SearchModel> private var search
|
@EnvironmentObject<SearchModel> private var search
|
||||||
@ -50,15 +49,13 @@ struct AppSidebarNavigation: View {
|
|||||||
.frame(minWidth: sidebarMinWidth)
|
.frame(minWidth: sidebarMinWidth)
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
BrowserPlayerControls {
|
HStack(alignment: .center) {
|
||||||
HStack(alignment: .center) {
|
Spacer()
|
||||||
Spacer()
|
Image(systemName: "4k.tv")
|
||||||
Image(systemName: "4k.tv")
|
.renderingMode(.original)
|
||||||
.renderingMode(.original)
|
.font(.system(size: 60))
|
||||||
.font(.system(size: 60))
|
.foregroundColor(.accentColor)
|
||||||
.foregroundColor(.accentColor)
|
Spacer()
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ struct AppTabNavigation: View {
|
|||||||
@EnvironmentObject<InstancesModel> private var instances
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
|
||||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SearchModel> private var search
|
@EnvironmentObject<SearchModel> private var search
|
||||||
@ -130,20 +129,6 @@ struct AppTabNavigation: View {
|
|||||||
.tag(TabSelection.search)
|
.tag(TabSelection.search)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var videoPlayer: some View {
|
|
||||||
VideoPlayerView()
|
|
||||||
.environmentObject(accounts)
|
|
||||||
.environmentObject(comments)
|
|
||||||
.environmentObject(instances)
|
|
||||||
.environmentObject(navigation)
|
|
||||||
.environmentObject(player)
|
|
||||||
.environmentObject(playerControls)
|
|
||||||
.environmentObject(playlists)
|
|
||||||
.environmentObject(recents)
|
|
||||||
.environmentObject(subscriptions)
|
|
||||||
.environmentObject(thumbnailsModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
var toolbarContent: some ToolbarContent {
|
var toolbarContent: some ToolbarContent {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
Group {
|
Group {
|
||||||
|
@ -12,8 +12,10 @@ struct ContentView: View {
|
|||||||
@EnvironmentObject<CommentsModel> private var comments
|
@EnvironmentObject<CommentsModel> private var comments
|
||||||
@EnvironmentObject<InstancesModel> private var instances
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<NetworkStateModel> private var networkState
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||||
|
@EnvironmentObject<PlayerTimeModel> private var playerTime
|
||||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SearchModel> private var search
|
@EnvironmentObject<SearchModel> private var search
|
||||||
@ -42,7 +44,6 @@ struct ContentView: View {
|
|||||||
TVNavigationView()
|
TVNavigationView()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onAppear(perform: configure)
|
|
||||||
.onChange(of: accounts.signedIn) { _ in
|
.onChange(of: accounts.signedIn) { _ in
|
||||||
subscriptions.load(force: true)
|
subscriptions.load(force: true)
|
||||||
playlists.load(force: true)
|
playlists.load(force: true)
|
||||||
@ -52,7 +53,9 @@ struct ContentView: View {
|
|||||||
.environmentObject(comments)
|
.environmentObject(comments)
|
||||||
.environmentObject(instances)
|
.environmentObject(instances)
|
||||||
.environmentObject(navigation)
|
.environmentObject(navigation)
|
||||||
|
.environmentObject(networkState)
|
||||||
.environmentObject(player)
|
.environmentObject(player)
|
||||||
|
.environmentObject(playerTime)
|
||||||
.environmentObject(playlists)
|
.environmentObject(playlists)
|
||||||
.environmentObject(recents)
|
.environmentObject(recents)
|
||||||
.environmentObject(search)
|
.environmentObject(search)
|
||||||
@ -107,118 +110,9 @@ struct ContentView: View {
|
|||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
.alert(isPresented: $navigation.presentingAlert) {
|
||||||
|
Alert(title: Text(navigation.alertTitle), message: Text(navigation.alertMessage))
|
||||||
func configure() {
|
|
||||||
SiestaLog.Category.enabled = .common
|
|
||||||
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
|
|
||||||
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
|
|
||||||
#if !os(macOS)
|
|
||||||
setupNowPlayingInfoCenter()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
if Defaults[.lockPortraitWhenBrowsing] {
|
|
||||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
if let account = accounts.lastUsed ??
|
|
||||||
instances.lastUsed?.anonymousAccount ??
|
|
||||||
InstancesModel.all.first?.anonymousAccount
|
|
||||||
{
|
|
||||||
accounts.setCurrent(account)
|
|
||||||
}
|
|
||||||
|
|
||||||
if accounts.current.isNil {
|
|
||||||
navigation.presentingWelcomeScreen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
playlists.accounts = accounts
|
|
||||||
search.accounts = accounts
|
|
||||||
subscriptions.accounts = accounts
|
|
||||||
|
|
||||||
comments.player = player
|
|
||||||
|
|
||||||
menu.accounts = accounts
|
|
||||||
menu.navigation = navigation
|
|
||||||
menu.player = player
|
|
||||||
playerControls.player = player
|
|
||||||
|
|
||||||
player.accounts = accounts
|
|
||||||
player.comments = comments
|
|
||||||
player.controls = playerControls
|
|
||||||
|
|
||||||
if !accounts.current.isNil {
|
|
||||||
player.restoreQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !Defaults[.saveRecents] {
|
|
||||||
recents.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
var section = Defaults[.visibleSections].min()?.tabSelection
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
if section == .playlists {
|
|
||||||
section = .search
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
navigation.tabSelection = section ?? .search
|
|
||||||
|
|
||||||
subscriptions.load()
|
|
||||||
playlists.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupNowPlayingInfoCenter() {
|
|
||||||
#if !os(macOS)
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
|
||||||
|
|
||||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().playCommand.addTarget { _ in
|
|
||||||
player.play()
|
|
||||||
return .success
|
|
||||||
}
|
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().pauseCommand.addTarget { _ in
|
|
||||||
player.pause()
|
|
||||||
return .success
|
|
||||||
}
|
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
|
||||||
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = false
|
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.addTarget { remoteEvent in
|
|
||||||
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent
|
|
||||||
else {
|
|
||||||
return .commandFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
player.backend.seek(to: event.positionTime)
|
|
||||||
|
|
||||||
return .success
|
|
||||||
}
|
|
||||||
|
|
||||||
let skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand
|
|
||||||
skipForwardCommand.isEnabled = true
|
|
||||||
skipForwardCommand.preferredIntervals = [10]
|
|
||||||
|
|
||||||
skipForwardCommand.addTarget { _ in
|
|
||||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
|
||||||
return .success
|
|
||||||
}
|
|
||||||
|
|
||||||
let skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand
|
|
||||||
skipBackwardCommand.isEnabled = true
|
|
||||||
skipBackwardCommand.preferredIntervals = [10]
|
|
||||||
|
|
||||||
skipBackwardCommand.addTarget { _ in
|
|
||||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
|
||||||
return .success
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func openWelcomeScreenIfAccountEmpty() {
|
func openWelcomeScreenIfAccountEmpty() {
|
||||||
|
83
Shared/Player/ChaptersView.swift
Normal file
83
Shared/Player/ChaptersView.swift
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import Foundation
|
||||||
|
import SDWebImageSwiftUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ChaptersView: View {
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
if let chapters = player.currentVideo?.chapters, !chapters.isEmpty {
|
||||||
|
Section(header: Text("Chapters")) {
|
||||||
|
ForEach(chapters) { chapter in
|
||||||
|
Button {
|
||||||
|
player.backend.seek(to: chapter.start)
|
||||||
|
} label: {
|
||||||
|
chapterButtonLabel(chapter)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(player.currentVideo?.title ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.id(UUID())
|
||||||
|
#if os(macOS)
|
||||||
|
.listStyle(.inset)
|
||||||
|
#elseif os(iOS)
|
||||||
|
.listStyle(.grouped)
|
||||||
|
#else
|
||||||
|
.listStyle(.plain)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func chapterButtonLabel(_ chapter: Chapter) -> some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
if !chapter.image.isNil {
|
||||||
|
smallImage(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(chapter.title)
|
||||||
|
.font(.headline)
|
||||||
|
Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "")
|
||||||
|
.font(.system(.subheadline).monospacedDigit())
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func smallImage(_ chapter: Chapter) -> some View {
|
||||||
|
WebImage(url: chapter.image)
|
||||||
|
.resizable()
|
||||||
|
.placeholder {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
.indicator(.activity)
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(width: thumbnailWidth, height: 140)
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 12))
|
||||||
|
#else
|
||||||
|
.frame(width: thumbnailWidth, height: 60)
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 6))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var thumbnailWidth: Double {
|
||||||
|
#if os(tvOS)
|
||||||
|
250
|
||||||
|
#else
|
||||||
|
100
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChaptersView_Preview: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ChaptersView()
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,6 @@ struct CommentsView: View {
|
|||||||
NoCommentsView(text: "No comments", systemImage: "0.circle.fill")
|
NoCommentsView(text: "No comments", systemImage: "0.circle.fill")
|
||||||
} else if !comments.loaded {
|
} else if !comments.loaded {
|
||||||
PlaceholderProgressView()
|
PlaceholderProgressView()
|
||||||
.onAppear {
|
|
||||||
comments.load()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let last = comments.all.last
|
let last = comments.all.last
|
||||||
let commentsStack = LazyVStack {
|
let commentsStack = LazyVStack {
|
||||||
|
22
Shared/Player/Controls/ControlBackgroundModifier.swift
Normal file
22
Shared/Player/Controls/ControlBackgroundModifier.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ControlBackgroundModifier: ViewModifier {
|
||||||
|
var enabled = true
|
||||||
|
var edgesIgnoringSafeArea = Edge.Set()
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
if enabled {
|
||||||
|
content
|
||||||
|
#if os(macOS)
|
||||||
|
.background(VisualEffectBlur(material: .hudWindow))
|
||||||
|
#elseif os(iOS)
|
||||||
|
.background(VisualEffectBlur(blurStyle: .systemThinMaterial).edgesIgnoringSafeArea(edgesIgnoringSafeArea))
|
||||||
|
#else
|
||||||
|
.background(.thinMaterial)
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
185
Shared/Player/Controls/ControlsOverlay.swift
Normal file
185
Shared/Player/Controls/ControlsOverlay.swift
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ControlsOverlay: View {
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
@EnvironmentObject<PlayerControlsModel> private var model
|
||||||
|
|
||||||
|
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 6) {
|
||||||
|
HStack {
|
||||||
|
backendButtons
|
||||||
|
}
|
||||||
|
qualityButton
|
||||||
|
HStack {
|
||||||
|
decreaseRateButton
|
||||||
|
rateButton
|
||||||
|
increaseRateButton
|
||||||
|
}
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
if player.activeBackend == .mpv,
|
||||||
|
showMPVPlaybackStats
|
||||||
|
{
|
||||||
|
mpvPlaybackStats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var backendButtons: some View {
|
||||||
|
ForEach(PlayerBackendType.allCases, id: \.self) { backend in
|
||||||
|
backendButton(backend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func backendButton(_ backend: PlayerBackendType) -> some View {
|
||||||
|
Button {
|
||||||
|
player.saveTime {
|
||||||
|
player.changeActiveBackend(from: player.activeBackend, to: backend)
|
||||||
|
model.resetTimer()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text(backend.label)
|
||||||
|
.padding(6)
|
||||||
|
.foregroundColor(player.activeBackend == backend ? .accentColor : .secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var increaseRateButton: some View {
|
||||||
|
let increasedRate = PlayerModel.availableRates.first { $0 > player.currentRate }
|
||||||
|
return Button {
|
||||||
|
if let rate = increasedRate {
|
||||||
|
player.currentRate = rate
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Increase rate", systemImage: "plus")
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
#else
|
||||||
|
.frame(height: 30)
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
#endif
|
||||||
|
.disabled(increasedRate.isNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var decreaseRateButton: some View {
|
||||||
|
let decreasedRate = PlayerModel.availableRates.last { $0 < player.currentRate }
|
||||||
|
|
||||||
|
return Button {
|
||||||
|
if let rate = decreasedRate {
|
||||||
|
player.currentRate = rate
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Decrease rate", systemImage: "minus")
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
#else
|
||||||
|
.frame(height: 30)
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
#endif
|
||||||
|
.disabled(decreasedRate.isNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var qualityButton: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
StreamControl()
|
||||||
|
.labelsHidden()
|
||||||
|
.frame(maxWidth: 300)
|
||||||
|
#elseif os(iOS)
|
||||||
|
Menu {
|
||||||
|
StreamControl()
|
||||||
|
.frame(width: 45, height: 30)
|
||||||
|
#if os(iOS)
|
||||||
|
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||||
|
#endif
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
} label: {
|
||||||
|
Text(player.streamSelection?.shortQuality ?? "loading")
|
||||||
|
.frame(width: 140, height: 30)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(width: 140, height: 30)
|
||||||
|
#if os(macOS)
|
||||||
|
.background(VisualEffectBlur(material: .hudWindow))
|
||||||
|
#elseif os(iOS)
|
||||||
|
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||||
|
#endif
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var rateButton: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
ratePicker
|
||||||
|
.labelsHidden()
|
||||||
|
.frame(maxWidth: 100)
|
||||||
|
#elseif os(iOS)
|
||||||
|
Menu {
|
||||||
|
ratePicker
|
||||||
|
.frame(width: 100, height: 30)
|
||||||
|
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
} label: {
|
||||||
|
Text(player.rateLabel(player.currentRate))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(width: 100, height: 30)
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpvPlaybackStats: some View {
|
||||||
|
Group {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("hw decoder: \(player.mpvBackend.hwDecoder)")
|
||||||
|
Text("dropped: \(player.mpvBackend.frameDropCount)")
|
||||||
|
Text("video: \(String(format: "%.2ffps", player.mpvBackend.outputFps))")
|
||||||
|
Text("buffering: \(String(format: "%.0f%%", player.mpvBackend.bufferingState))")
|
||||||
|
Text("cache: \(String(format: "%.2fs", player.mpvBackend.cacheDuration))")
|
||||||
|
}
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.font(.system(size: 9))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ControlsOverlay_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ControlsOverlay()
|
||||||
|
.environmentObject(PlayerModel())
|
||||||
|
.environmentObject(PlayerControlsModel())
|
||||||
|
}
|
||||||
|
}
|
37
Shared/Player/Controls/OSD/Buffering.swift
Normal file
37
Shared/Player/Controls/OSD/Buffering.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct Buffering: View {
|
||||||
|
var reason = "Buffering stream..."
|
||||||
|
var state: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 2) {
|
||||||
|
ProgressView()
|
||||||
|
#if os(macOS)
|
||||||
|
.scaleEffect(0.4)
|
||||||
|
#else
|
||||||
|
.scaleEffect(0.7)
|
||||||
|
#endif
|
||||||
|
.frame(maxHeight: 14)
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
|
||||||
|
Text(reason)
|
||||||
|
.font(.caption)
|
||||||
|
if let state = state {
|
||||||
|
Text(state)
|
||||||
|
.font(.caption2.monospacedDigit())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Buffering_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Buffering(state: "100% (2.95s)")
|
||||||
|
}
|
||||||
|
}
|
22
Shared/Player/Controls/OSD/NetworkState.swift
Normal file
22
Shared/Player/Controls/OSD/NetworkState.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NetworkState: View {
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
@EnvironmentObject<NetworkStateModel> private var model
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Buffering(state: model.fullStateText)
|
||||||
|
.opacity(model.pausedForCache || player.isSeeking ? 1 : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NetworkState_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let networkState = NetworkStateModel()
|
||||||
|
networkState.bufferingState = 30
|
||||||
|
|
||||||
|
return NetworkState()
|
||||||
|
.environmentObject(networkState)
|
||||||
|
.environmentObject(PlayerModel())
|
||||||
|
}
|
||||||
|
}
|
37
Shared/Player/Controls/OSD/OpeningStream.swift
Normal file
37
Shared/Player/Controls/OSD/OpeningStream.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct OpeningStream: View {
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
@EnvironmentObject<NetworkStateModel> private var model
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Buffering(reason: reason, state: state)
|
||||||
|
.opacity(visible ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var visible: Bool {
|
||||||
|
(!player.currentItem.isNil && !player.videoBeingOpened.isNil) || (player.isLoadingVideo && !model.pausedForCache && !player.isSeeking)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reason: String {
|
||||||
|
player.videoBeingOpened.isNil ? "Opening\(streamQuality)stream..." : "Loading streams..."
|
||||||
|
}
|
||||||
|
|
||||||
|
var state: String? {
|
||||||
|
player.videoBeingOpened.isNil ? model.bufferingStateText : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var streamQuality: String {
|
||||||
|
guard let stream = player.streamSelection else { return " " }
|
||||||
|
guard !player.musicMode else { return " audio " }
|
||||||
|
|
||||||
|
return " \(stream.shortQuality) "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OpeningStream_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
OpeningStream()
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ struct PlayerControls: View {
|
|||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
@Default(.controlsBarInPlayer) private var controlsBarInPlayer
|
||||||
|
|
||||||
init(player: PlayerModel, thumbnails: ThumbnailsModel) {
|
init(player: PlayerModel, thumbnails: ThumbnailsModel) {
|
||||||
self.player = player
|
self.player = player
|
||||||
@ -31,74 +31,107 @@ struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
ZStack(alignment: .topTrailing) {
|
||||||
ZStack(alignment: .bottom) {
|
VStack {
|
||||||
VStack(alignment: .trailing, spacing: 4) {
|
ZStack(alignment: .center) {
|
||||||
#if !os(tvOS)
|
OpeningStream()
|
||||||
buttonsBar
|
NetworkState()
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
qualityButton
|
|
||||||
backendButton
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
Text(player.stream?.description ?? "")
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
mediumButtonsBar
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
if player.activeBackend == .mpv, showMPVPlaybackStats {
|
VStack(spacing: 4) {
|
||||||
mpvPlaybackStats
|
buttonsBar
|
||||||
}
|
|
||||||
|
|
||||||
timeline
|
if let video = player.currentVideo, player.playingFullScreen {
|
||||||
.offset(y: 10)
|
// if let video = Video.fixture {
|
||||||
.zIndex(1)
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(video.title)
|
||||||
|
.font(.title2.bold())
|
||||||
|
|
||||||
|
Text(video.author)
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 3))
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
bottomBar
|
Group {
|
||||||
#if os(macOS)
|
ZStack(alignment: .bottom) {
|
||||||
.background(VisualEffectBlur(material: .hudWindow))
|
floatingControls
|
||||||
#elseif os(iOS)
|
.padding(.top, 20)
|
||||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
.padding(4)
|
||||||
#endif
|
.modifier(ControlBackgroundModifier())
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
|
||||||
|
timeline
|
||||||
|
.padding(4)
|
||||||
|
.offset(y: -25)
|
||||||
|
.zIndex(1)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 500)
|
||||||
|
.padding(.bottom, 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.top, 2)
|
||||||
|
.padding(.horizontal, 2)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.opacity(model.presentingControlsOverlay ? 1 : model.presentingControls ? 1 : 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, 4)
|
#if os(tvOS)
|
||||||
.padding(.horizontal, 4)
|
.onChange(of: model.presentingControls) { _ in
|
||||||
.opacity(model.presentingControls ? 1 : 0)
|
if model.presentingControls {
|
||||||
}
|
focusedField = .play
|
||||||
#if os(tvOS)
|
}
|
||||||
.onChange(of: model.presentingControls) { _ in
|
|
||||||
if model.presentingControls {
|
|
||||||
focusedField = .play
|
|
||||||
}
|
}
|
||||||
|
.onChange(of: focusedField) { _ in
|
||||||
|
model.resetTimer()
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
.background(PlayerGestures())
|
||||||
|
.background(controlsBackground)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ControlsOverlay()
|
||||||
|
.padding()
|
||||||
|
.modifier(ControlBackgroundModifier(enabled: true))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
.offset(x: -2, y: 40)
|
||||||
|
.opacity(model.presentingControlsOverlay ? 1 : 0)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
player.restoreLastSkippedSegment()
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
if let segment = player.lastSkipped {
|
||||||
|
Image(systemName: "arrow.counterclockwise")
|
||||||
|
|
||||||
|
Text("Skipped \(segment.durationText) seconds of \(SponsorBlockAPI.categoryDescription(segment.category)?.lowercased() ?? "segment")")
|
||||||
|
.frame(alignment: .bottomLeading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 5)
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 2))
|
||||||
|
.offset(x: -2, y: -2)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.opacity(model.presentingControls ? 0 : player.lastSkipped.isNil ? 0 : 1)
|
||||||
}
|
}
|
||||||
.onChange(of: focusedField) { _ in
|
|
||||||
model.resetTimer()
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
.background(PlayerGestures())
|
|
||||||
.background(controlsBackground)
|
|
||||||
#endif
|
|
||||||
.environment(\.colorScheme, .dark)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder var controlsBackground: some View {
|
@ViewBuilder var controlsBackground: some View {
|
||||||
if player.musicMode,
|
if player.musicMode,
|
||||||
let item = self.player.currentItem,
|
let item = self.player.currentItem,
|
||||||
let url = thumbnails.best(item.video)
|
let video = item.video,
|
||||||
|
let url = thumbnails.best(video)
|
||||||
{
|
{
|
||||||
WebImage(url: url)
|
WebImage(url: url)
|
||||||
.resizable()
|
.resizable()
|
||||||
@ -110,48 +143,8 @@ struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mpvPlaybackStats: some View {
|
|
||||||
HStack {
|
|
||||||
Group {
|
|
||||||
Text("hw decoder: \(player.mpvBackend.hwDecoder)")
|
|
||||||
Text("dropped: \(player.mpvBackend.frameDropCount)")
|
|
||||||
Text("video: \(String(format: "%.2ffps", player.mpvBackend.outputFps))")
|
|
||||||
Text("buffering: \(String(format: "%.0f%%", player.mpvBackend.bufferingState))")
|
|
||||||
Text("cache: \(String(format: "%.2fs", player.mpvBackend.cacheDuration))")
|
|
||||||
}
|
|
||||||
.padding(4)
|
|
||||||
#if os(macOS)
|
|
||||||
.background(VisualEffectBlur(material: .hudWindow))
|
|
||||||
#elseif os(iOS)
|
|
||||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
|
||||||
#else
|
|
||||||
.background(.thinMaterial)
|
|
||||||
#endif
|
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
#if !os(tvOS)
|
|
||||||
.font(.system(size: 9))
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
var timeline: some View {
|
var timeline: some View {
|
||||||
TimelineView(duration: durationBinding, current: currentTimeBinding, cornerRadius: 0)
|
TimelineView(context: .player).foregroundColor(.primary)
|
||||||
}
|
|
||||||
|
|
||||||
var durationBinding: Binding<Double> {
|
|
||||||
Binding<Double>(
|
|
||||||
get: { model.duration.seconds },
|
|
||||||
set: { value in model.duration = .secondsInDefaultTimescale(value) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentTimeBinding: Binding<Double> {
|
|
||||||
Binding<Double>(
|
|
||||||
get: { model.currentTime.seconds },
|
|
||||||
set: { value in model.currentTime = .secondsInDefaultTimescale(value) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hidePlayerButton: some View {
|
private var hidePlayerButton: some View {
|
||||||
@ -195,20 +188,20 @@ struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buttonsBar: some View {
|
var buttonsBar: some View {
|
||||||
HStack {
|
HStack(spacing: 20) {
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
fullscreenButton
|
fullscreenButton
|
||||||
|
pipButton
|
||||||
#if os(iOS)
|
|
||||||
pipButton
|
|
||||||
.padding(.leading, 5)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
rateButton
|
button("overlay", systemImage: "info.circle") {}
|
||||||
|
|
||||||
musicModeButton
|
button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) {
|
||||||
|
withAnimation(Self.animation) {
|
||||||
|
model.presentingControlsOverlay.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
closeVideoButton
|
closeVideoButton
|
||||||
#endif
|
#endif
|
||||||
@ -227,74 +220,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
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder private var qualityButton: some View {
|
|
||||||
#if os(macOS)
|
|
||||||
StreamControl()
|
|
||||||
.labelsHidden()
|
|
||||||
.frame(maxWidth: 300)
|
|
||||||
#elseif os(iOS)
|
|
||||||
Menu {
|
|
||||||
StreamControl()
|
|
||||||
.frame(width: 45, height: 30)
|
|
||||||
#if os(iOS)
|
|
||||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
|
||||||
#endif
|
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
|
||||||
} label: {
|
|
||||||
Text(player.streamSelection?.shortQuality ?? "loading")
|
|
||||||
.frame(width: 140, height: 30)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.frame(width: 140, height: 30)
|
|
||||||
#if os(macOS)
|
|
||||||
.background(VisualEffectBlur(material: .hudWindow))
|
|
||||||
#elseif os(iOS)
|
|
||||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
|
||||||
#endif
|
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private var backendButton: some View {
|
|
||||||
button(player.activeBackend.label, width: 100) {
|
|
||||||
player.saveTime {
|
|
||||||
player.changeActiveBackend(from: player.activeBackend, to: player.activeBackend.next())
|
|
||||||
model.resetTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var closeVideoButton: some View {
|
private var closeVideoButton: some View {
|
||||||
button("Close", systemImage: "xmark") {
|
button("Close", systemImage: "xmark") {
|
||||||
player.pause()
|
player.pause()
|
||||||
@ -313,116 +238,99 @@ struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var musicModeButton: some View {
|
private var musicModeButton: some View {
|
||||||
button("Music Mode", systemImage: "music.note", active: player.musicMode, action: player.toggleMusicMode)
|
button("Music Mode", systemImage: "music.note", background: false, active: player.musicMode, action: player.toggleMusicMode)
|
||||||
.disabled(player.activeBackend == .appleAVPlayer)
|
.disabled(player.activeBackend == .appleAVPlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private var pipButton: some View {
|
||||||
button("PiP", systemImage: "pip") {
|
button("PiP", systemImage: "pip") {
|
||||||
model.startPiP()
|
model.startPiP()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediumButtonsBar: some View {
|
var floatingControls: some View {
|
||||||
HStack {
|
HStack {
|
||||||
#if !os(tvOS)
|
HStack(spacing: 20) {
|
||||||
restartVideoButton
|
togglePlayButton
|
||||||
.padding(.trailing, 15)
|
seekBackwardButton
|
||||||
|
seekForwardButton
|
||||||
button("Seek Backward", systemImage: "gobackward.10", size: 30, cornerRadius: 5) {
|
|
||||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
|
||||||
}
|
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
.focused($focusedField, equals: .backward)
|
|
||||||
#else
|
|
||||||
.keyboardShortcut("k", modifiers: [])
|
|
||||||
.keyboardShortcut(KeyEquivalent.leftArrow, modifiers: [])
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
button(
|
|
||||||
model.isPlaying ? "Pause" : "Play",
|
|
||||||
systemImage: model.isPlaying ? "pause.fill" : "play.fill",
|
|
||||||
size: 30, cornerRadius: 5
|
|
||||||
) {
|
|
||||||
player.backend.togglePlay()
|
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.focused($focusedField, equals: .play)
|
|
||||||
#else
|
|
||||||
.keyboardShortcut("p")
|
|
||||||
.keyboardShortcut(.space)
|
|
||||||
#endif
|
|
||||||
.disabled(model.isLoadingVideo)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
#if !os(tvOS)
|
HStack(spacing: 20) {
|
||||||
button("Seek Forward", systemImage: "goforward.10", size: 30, cornerRadius: 5) {
|
restartVideoButton
|
||||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
|
||||||
}
|
|
||||||
#if os(tvOS)
|
|
||||||
.focused($focusedField, equals: .forward)
|
|
||||||
#else
|
|
||||||
.keyboardShortcut("l", modifiers: [])
|
|
||||||
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
|
|
||||||
#endif
|
|
||||||
|
|
||||||
advanceToNextItemButton
|
advanceToNextItemButton
|
||||||
.padding(.leading, 15)
|
musicModeButton
|
||||||
#endif
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
}
|
}
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var seekBackwardButton: some View {
|
||||||
|
button("Seek Backward", systemImage: "gobackward.10", size: 25, cornerRadius: 5, background: false) {
|
||||||
|
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .backward)
|
||||||
|
#else
|
||||||
|
.keyboardShortcut("k", modifiers: [])
|
||||||
|
.keyboardShortcut(KeyEquivalent.leftArrow, modifiers: [])
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var seekForwardButton: some View {
|
||||||
|
button("Seek Forward", systemImage: "goforward.10", size: 25, cornerRadius: 5, background: false) {
|
||||||
|
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .forward)
|
||||||
|
#else
|
||||||
|
.keyboardShortcut("l", modifiers: [])
|
||||||
|
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private var restartVideoButton: some View {
|
private var restartVideoButton: some View {
|
||||||
button("Restart video", systemImage: "backward.end.fill", size: 30, cornerRadius: 5) {
|
button("Restart video", systemImage: "backward.end.fill", size: 25, cornerRadius: 5, background: false) {
|
||||||
player.backend.seek(to: 0.0)
|
player.backend.seek(to: 0.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var togglePlayButton: some View {
|
||||||
|
button(
|
||||||
|
model.isPlaying ? "Pause" : "Play",
|
||||||
|
systemImage: model.isPlaying ? "pause.fill" : "play.fill",
|
||||||
|
size: 25, cornerRadius: 5, background: false
|
||||||
|
) {
|
||||||
|
player.backend.togglePlay()
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .play)
|
||||||
|
#else
|
||||||
|
.keyboardShortcut("p")
|
||||||
|
.keyboardShortcut(.space)
|
||||||
|
#endif
|
||||||
|
.disabled(model.isLoadingVideo)
|
||||||
|
}
|
||||||
|
|
||||||
private var advanceToNextItemButton: some View {
|
private var advanceToNextItemButton: some View {
|
||||||
button("Next", systemImage: "forward.fill", size: 30, cornerRadius: 5) {
|
button("Next", systemImage: "forward.fill", size: 25, cornerRadius: 5, background: false) {
|
||||||
player.advanceToNextItem()
|
player.advanceToNextItem()
|
||||||
}
|
}
|
||||||
.disabled(player.queue.isEmpty)
|
.disabled(player.queue.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
var bottomBar: some View {
|
|
||||||
HStack {
|
|
||||||
Text(model.playbackTime)
|
|
||||||
}
|
|
||||||
.font(.system(size: 15))
|
|
||||||
.padding(.horizontal, 5)
|
|
||||||
.padding(.vertical, 3)
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
func button(
|
func button(
|
||||||
_ label: String,
|
_ label: String,
|
||||||
systemImage: String? = nil,
|
systemImage: String? = nil,
|
||||||
size: Double = 30,
|
size: Double = 25,
|
||||||
width: Double? = nil,
|
width: Double? = nil,
|
||||||
height: Double? = nil,
|
height: Double? = nil,
|
||||||
cornerRadius: Double = 3,
|
cornerRadius: Double = 3,
|
||||||
|
background: Bool = true,
|
||||||
active: Bool = false,
|
active: Bool = false,
|
||||||
action: @escaping () -> Void = {}
|
action: @escaping () -> Void = {}
|
||||||
) -> some View {
|
) -> some View {
|
||||||
@ -442,39 +350,30 @@ struct PlayerControls: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
.font(.system(size: 13))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(active ? .accentColor : .primary)
|
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||||
.frame(width: width ?? size, height: height ?? size)
|
.frame(width: width ?? size, height: height ?? size)
|
||||||
#if os(macOS)
|
.modifier(ControlBackgroundModifier(enabled: background))
|
||||||
.background(VisualEffectBlur(material: .hudWindow))
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
#elseif os(iOS)
|
|
||||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
|
||||||
#endif
|
|
||||||
.mask(RoundedRectangle(cornerRadius: cornerRadius))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fullScreenLayout: Bool {
|
var fullScreenLayout: Bool {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
model.playingFullscreen || verticalSizeClass == .compact
|
player.playingFullScreen || verticalSizeClass == .compact
|
||||||
#else
|
#else
|
||||||
model.playingFullscreen
|
player.playingFullScreen
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PlayerControls_Previews: PreviewProvider {
|
struct PlayerControls_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let model = PlayerControlsModel()
|
ZStack {
|
||||||
model.presentingControls = true
|
|
||||||
model.currentTime = .secondsInDefaultTimescale(0)
|
|
||||||
model.duration = .secondsInDefaultTimescale(120)
|
|
||||||
|
|
||||||
return ZStack {
|
|
||||||
Color.gray
|
Color.gray
|
||||||
|
|
||||||
PlayerControls(player: PlayerModel(), thumbnails: ThumbnailsModel())
|
PlayerControls(player: PlayerModel(), thumbnails: ThumbnailsModel())
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
.environmentObject(model)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ final class MPVOGLView: GLKView {
|
|||||||
var needsDrawing = true
|
var needsDrawing = true
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
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")
|
print("Failed to initialize OpenGLES 2.0 context")
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
@ -20,10 +20,12 @@ final class MPVOGLView: GLKView {
|
|||||||
|
|
||||||
super.init(frame: frame, context: context)
|
super.init(frame: frame, context: context)
|
||||||
|
|
||||||
EAGLContext.setCurrent(context)
|
self.context = context
|
||||||
|
bindDrawable()
|
||||||
|
|
||||||
defaultFBO = -1
|
defaultFBO = -1
|
||||||
isOpaque = false
|
isOpaque = true
|
||||||
|
enableSetNeedsDisplay = false
|
||||||
|
|
||||||
fillBlack()
|
fillBlack()
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import UIKit
|
|||||||
|
|
||||||
final class MPVViewController: UIViewController {
|
final class MPVViewController: UIViewController {
|
||||||
var client: MPVClient!
|
var client: MPVClient!
|
||||||
var glView: MPVOGLView!
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
client = MPVClient()
|
client = MPVClient()
|
||||||
@ -17,9 +16,8 @@ final class MPVViewController: UIViewController {
|
|||||||
super.loadView()
|
super.loadView()
|
||||||
|
|
||||||
client.create(frame: view.frame)
|
client.create(frame: view.frame)
|
||||||
glView = client.glView
|
|
||||||
|
|
||||||
view.addSubview(glView)
|
view.addSubview(client.glView)
|
||||||
|
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ struct NoCommentsView: View {
|
|||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: .infinity)
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
#endif
|
#endif
|
||||||
|
@ -6,7 +6,7 @@ import SwiftUI
|
|||||||
struct PlayerQueueRow: View {
|
struct PlayerQueueRow: View {
|
||||||
let item: PlayerQueueItem
|
let item: PlayerQueueItem
|
||||||
var history = false
|
var history = false
|
||||||
@Binding var fullScreen: Bool
|
var fullScreen: Bool
|
||||||
|
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
@ -14,10 +14,10 @@ struct PlayerQueueRow: View {
|
|||||||
|
|
||||||
@FetchRequest private var watchRequest: FetchedResults<Watch>
|
@FetchRequest private var watchRequest: FetchedResults<Watch>
|
||||||
|
|
||||||
init(item: PlayerQueueItem, history: Bool = false, fullScreen: Binding<Bool> = .constant(false)) {
|
init(item: PlayerQueueItem, history: Bool = false, fullScreen: Bool = false) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.history = history
|
self.history = history
|
||||||
_fullScreen = fullScreen
|
self.fullScreen = fullScreen
|
||||||
_watchRequest = FetchRequest<Watch>(
|
_watchRequest = FetchRequest<Watch>(
|
||||||
entity: Watch.entity(),
|
entity: Watch.entity(),
|
||||||
sortDescriptors: [],
|
sortDescriptors: [],
|
||||||
@ -32,6 +32,8 @@ struct PlayerQueueRow: View {
|
|||||||
|
|
||||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||||
|
|
||||||
|
player.videoBeingOpened = item.video
|
||||||
|
|
||||||
if history {
|
if history {
|
||||||
player.playHistory(item, at: watchStoppedAt)
|
player.playHistory(item, at: watchStoppedAt)
|
||||||
} else {
|
} else {
|
||||||
@ -39,9 +41,9 @@ struct PlayerQueueRow: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fullScreen {
|
if fullScreen {
|
||||||
withAnimation {
|
// withAnimation {
|
||||||
fullScreen = false
|
// fullScreen = false
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
if closePiPOnNavigation, player.playingInPictureInPicture {
|
if closePiPOnNavigation, player.playingInPictureInPicture {
|
||||||
|
@ -3,8 +3,8 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PlayerQueueView: View {
|
struct PlayerQueueView: View {
|
||||||
@Binding var sidebarQueue: Bool
|
var sidebarQueue: Bool
|
||||||
@Binding var fullScreen: Bool
|
var fullScreen: Bool
|
||||||
|
|
||||||
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
||||||
var watches: FetchedResults<Watch>
|
var watches: FetchedResults<Watch>
|
||||||
@ -49,7 +49,7 @@ struct PlayerQueueView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ForEach(player.queue) { item in
|
ForEach(player.queue) { item in
|
||||||
PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
PlayerQueueRow(item: item, fullScreen: fullScreen)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
removeButton(item)
|
removeButton(item)
|
||||||
removeAllButton()
|
removeAllButton()
|
||||||
@ -70,7 +70,7 @@ struct PlayerQueueView: View {
|
|||||||
PlayerQueueRow(
|
PlayerQueueRow(
|
||||||
item: PlayerQueueItem.from(watch, video: player.historyVideo(watch.videoID)),
|
item: PlayerQueueItem.from(watch, video: player.historyVideo(watch.videoID)),
|
||||||
history: true,
|
history: true,
|
||||||
fullScreen: $fullScreen
|
fullScreen: fullScreen
|
||||||
)
|
)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
player.loadHistoryVideoDetails(watch.videoID)
|
player.loadHistoryVideoDetails(watch.videoID)
|
||||||
@ -89,7 +89,7 @@ struct PlayerQueueView: View {
|
|||||||
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
|
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
|
||||||
Section(header: Text("Related")) {
|
Section(header: Text("Related")) {
|
||||||
ForEach(player.currentVideo!.related) { video in
|
ForEach(player.currentVideo!.related) { video in
|
||||||
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: $fullScreen)
|
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: fullScreen)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Button {
|
||||||
player.playNext(video)
|
player.playNext(video)
|
||||||
@ -137,7 +137,7 @@ struct PlayerQueueView: View {
|
|||||||
struct PlayerQueueView_Previews: PreviewProvider {
|
struct PlayerQueueView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
VStack {
|
||||||
PlayerQueueView(sidebarQueue: .constant(true), fullScreen: .constant(true))
|
PlayerQueueView(sidebarQueue: true, fullScreen: true)
|
||||||
}
|
}
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,48 @@
|
|||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct RelatedView: View {
|
struct RelatedView: View {
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
|
if let related = player.currentVideo?.related {
|
||||||
Section(header: Text("Related")) {
|
Section(header: Text("Related")) {
|
||||||
ForEach(player.currentVideo!.related) { video in
|
ForEach(related) { video in
|
||||||
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: .constant(false))
|
PlayerQueueRow(item: PlayerQueueItem(video))
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Section {
|
||||||
player.playNext(video)
|
Button {
|
||||||
} label: {
|
player.playNext(video)
|
||||||
Label("Play Next", systemImage: "text.insert")
|
} label: {
|
||||||
|
Label("Play Next", systemImage: "text.insert")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
player.enqueueVideo(video)
|
||||||
|
} label: {
|
||||||
|
Label("Play Last", systemImage: "text.append")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Button {
|
|
||||||
player.enqueueVideo(video)
|
if accounts.app.supportsUserPlaylists && accounts.signedIn {
|
||||||
} label: {
|
Section {
|
||||||
Label("Play Last", systemImage: "text.append")
|
Button {
|
||||||
|
navigation.presentAddToPlaylist(video)
|
||||||
|
} label: {
|
||||||
|
Label("Add to playlist...", systemImage: "text.badge.plus")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let playlist = playlists.lastUsed {
|
||||||
|
Button {
|
||||||
|
playlists.addVideo(playlistID: playlist.id, videoID: video.videoID, navigation: navigation)
|
||||||
|
} label: {
|
||||||
|
Label("Add to \(playlist.title)", systemImage: "text.badge.star")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,34 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TimelineView: View {
|
struct TimelineView: View {
|
||||||
@Binding private var duration: Double
|
enum Context {
|
||||||
@Binding private var current: Double
|
case controls
|
||||||
|
case player
|
||||||
|
}
|
||||||
|
|
||||||
|
private var duration: Double {
|
||||||
|
playerTime.duration.seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
private var current: Double {
|
||||||
|
get {
|
||||||
|
playerTime.currentTime.seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
playerTime.currentTime = .secondsInDefaultTimescale(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@State private var size = CGSize.zero
|
@State private var size = CGSize.zero
|
||||||
@State private var dragging = false
|
@State private var tooltipSize = CGSize.zero
|
||||||
|
@State private var dragging = false { didSet {
|
||||||
|
if dragging {
|
||||||
|
player.backend.stopControlsUpdates()
|
||||||
|
} else {
|
||||||
|
player.backend.startControlsUpdates()
|
||||||
|
}
|
||||||
|
}}
|
||||||
@State private var dragOffset: Double = 0
|
@State private var dragOffset: Double = 0
|
||||||
@State private var draggedFrom: Double = 0
|
@State private var draggedFrom: Double = 0
|
||||||
|
|
||||||
@ -13,147 +36,277 @@ struct TimelineView: View {
|
|||||||
private var height = 8.0
|
private var height = 8.0
|
||||||
|
|
||||||
var cornerRadius: Double
|
var cornerRadius: Double
|
||||||
var thumbTooltipWidth: Double = 100
|
var thumbAreaWidth: Double = 40
|
||||||
|
var context: Context
|
||||||
|
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlayerControlsModel> private var controls
|
@EnvironmentObject<PlayerControlsModel> private var controls
|
||||||
|
@EnvironmentObject<PlayerTimeModel> private var playerTime
|
||||||
|
|
||||||
init(duration: Binding<Double>, current: Binding<Double>, cornerRadius: Double = 10.0) {
|
var chapters: [Chapter] {
|
||||||
_duration = duration
|
player.currentVideo?.chapters ?? []
|
||||||
_current = current
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
cornerRadius: Double = 10.0,
|
||||||
|
context: Context = .controls
|
||||||
|
) {
|
||||||
self.cornerRadius = cornerRadius
|
self.cornerRadius = cornerRadius
|
||||||
|
self.context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .leading) {
|
VStack {
|
||||||
Group {
|
Group {
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
VStack(spacing: 3) {
|
||||||
.foregroundColor(.blue)
|
if dragging {
|
||||||
.frame(maxHeight: height)
|
if let segment = projectedSegment,
|
||||||
|
let description = SponsorBlockAPI.categoryDescription(segment.category)
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
{
|
||||||
.fill(Color.green)
|
Text(description)
|
||||||
.frame(maxHeight: height)
|
.font(.system(size: 8))
|
||||||
.frame(width: current * oneUnitWidth)
|
.fixedSize()
|
||||||
|
.lineLimit(1)
|
||||||
segmentsLayers
|
.foregroundColor(Color("AppRedColor"))
|
||||||
}
|
|
||||||
|
|
||||||
Circle()
|
|
||||||
.strokeBorder(.gray, lineWidth: 1)
|
|
||||||
.background(Circle().fill(dragging ? .gray : .white))
|
|
||||||
.offset(x: thumbOffset)
|
|
||||||
.foregroundColor(.red.opacity(0.6))
|
|
||||||
.frame(maxHeight: height * 4)
|
|
||||||
|
|
||||||
#if !os(tvOS)
|
|
||||||
.gesture(
|
|
||||||
DragGesture(minimumDistance: 0)
|
|
||||||
.onChanged { value in
|
|
||||||
if !dragging {
|
|
||||||
controls.removeTimer()
|
|
||||||
draggedFrom = current
|
|
||||||
}
|
|
||||||
|
|
||||||
dragging = true
|
|
||||||
|
|
||||||
let drag = value.translation.width
|
|
||||||
let change = (drag / size.width) * units
|
|
||||||
let changedCurrent = current + change
|
|
||||||
|
|
||||||
guard changedCurrent >= start, changedCurrent <= duration else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
withAnimation(Animation.linear(duration: 0.2)) {
|
|
||||||
dragOffset = drag
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
if let chapter = projectedChapter {
|
||||||
current = projectedValue
|
Text(chapter.title)
|
||||||
|
.lineLimit(3)
|
||||||
player.backend.seek(to: projectedValue)
|
.font(.system(size: 11).bold())
|
||||||
|
.frame(maxWidth: 250)
|
||||||
dragging = false
|
.fixedSize()
|
||||||
dragOffset = 0.0
|
|
||||||
draggedFrom = 0.0
|
|
||||||
controls.resetTimer()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Text((dragging ? projectedValue : current).formattedAsPlaybackTime(allowZero: true) ?? PlayerTimeModel.timePlaceholder)
|
||||||
|
.font(.system(size: 11).monospacedDigit())
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 3)
|
||||||
|
.foregroundColor(.black)
|
||||||
)
|
)
|
||||||
#endif
|
|
||||||
|
|
||||||
ZStack {
|
.foregroundColor(.white)
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
|
||||||
.frame(maxWidth: thumbTooltipWidth, maxHeight: 30)
|
|
||||||
|
|
||||||
Text(projectedValue.formattedAsPlaybackTime() ?? "--:--")
|
|
||||||
.foregroundColor(.black)
|
|
||||||
}
|
}
|
||||||
.animation(.linear(duration: 0.1))
|
.animation(.easeInOut(duration: 0.2))
|
||||||
|
.frame(maxHeight: 300, alignment: .bottom)
|
||||||
|
.offset(x: thumbTooltipOffset)
|
||||||
|
.overlay(GeometryReader { proxy in
|
||||||
|
Color.clear
|
||||||
|
.onAppear {
|
||||||
|
tooltipSize = proxy.size
|
||||||
|
}
|
||||||
|
.onChange(of: proxy.size) { _ in
|
||||||
|
tooltipSize = proxy.size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.frame(height: 80)
|
||||||
.opacity(dragging ? 1 : 0)
|
.opacity(dragging ? 1 : 0)
|
||||||
.offset(x: thumbTooltipOffset, y: -(height * 2) - 7)
|
.animation(.easeOut, value: thumbTooltipOffset)
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text((dragging ? projectedValue : nil)?.formattedAsPlaybackTime(allowZero: true) ?? playerTime.currentPlaybackTime)
|
||||||
|
.frame(minWidth: 35)
|
||||||
|
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray.opacity(0.1))
|
||||||
|
.frame(maxHeight: height)
|
||||||
|
.zIndex(1)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray.opacity(0.5))
|
||||||
|
.frame(maxHeight: height)
|
||||||
|
.frame(width: current * oneUnitWidth)
|
||||||
|
.zIndex(1)
|
||||||
|
|
||||||
|
segmentsLayers
|
||||||
|
.zIndex(2)
|
||||||
|
}
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
|
|
||||||
|
chaptersLayers
|
||||||
|
.zIndex(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.foregroundColor(.clear)
|
||||||
|
.background(
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(dragging ? .white : .gray)
|
||||||
|
.frame(maxWidth: 8)
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.fill(dragging ? .gray : .white)
|
||||||
|
.frame(maxWidth: 6)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.offset(x: thumbOffset)
|
||||||
|
.frame(maxWidth: thumbAreaWidth, minHeight: thumbAreaWidth)
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { value in
|
||||||
|
if !dragging {
|
||||||
|
controls.removeTimer()
|
||||||
|
draggedFrom = current
|
||||||
|
}
|
||||||
|
|
||||||
|
dragging = true
|
||||||
|
|
||||||
|
let drag = value.translation.width
|
||||||
|
let change = (drag / size.width) * units
|
||||||
|
let changedCurrent = current + change
|
||||||
|
|
||||||
|
guard changedCurrent >= start, changedCurrent <= duration else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
withAnimation(Animation.linear(duration: 0.2)) {
|
||||||
|
dragOffset = drag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
if abs(dragOffset) > 0 {
|
||||||
|
playerTime.currentTime = .secondsInDefaultTimescale(projectedValue)
|
||||||
|
player.backend.seek(to: projectedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
dragging = false
|
||||||
|
dragOffset = 0.0
|
||||||
|
draggedFrom = 0.0
|
||||||
|
controls.resetTimer()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
.background(GeometryReader { proxy in
|
||||||
|
Color.clear
|
||||||
|
.onAppear {
|
||||||
|
self.size = proxy.size
|
||||||
|
}
|
||||||
|
.onChange(of: proxy.size) { size in
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.frame(maxHeight: 20)
|
||||||
|
#if !os(tvOS)
|
||||||
|
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
|
||||||
|
let target = (value.location.x / size.width) * units
|
||||||
|
self.playerTime.currentTime = .secondsInDefaultTimescale(target)
|
||||||
|
player.backend.seek(to: target)
|
||||||
|
})
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Text(dragging ? playerTime.durationPlaybackTime : playerTime.withoutSegmentsPlaybackTime)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 3))
|
||||||
|
.frame(minWidth: 35)
|
||||||
|
}
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 3))
|
||||||
|
.font(.system(size: 9).monospacedDigit())
|
||||||
|
.zIndex(2)
|
||||||
}
|
}
|
||||||
.background(GeometryReader { proxy in
|
}
|
||||||
Color.clear
|
|
||||||
.onAppear {
|
var tooltipVeritcalOffset: Double {
|
||||||
self.size = proxy.size
|
var offset = -20.0
|
||||||
}
|
|
||||||
.onChange(of: proxy.size) { size in
|
if !projectedChapter.isNil {
|
||||||
self.size = size
|
offset -= 8.0
|
||||||
}
|
}
|
||||||
})
|
|
||||||
#if !os(tvOS)
|
if !projectedSegment.isNil {
|
||||||
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
|
offset -= 6.5
|
||||||
let target = (value.location.x / size.width) * units
|
}
|
||||||
current = target
|
|
||||||
player.backend.seek(to: target)
|
return offset
|
||||||
})
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectedValue: Double {
|
var projectedValue: Double {
|
||||||
let change = (dragOffset / size.width) * units
|
let change = (dragOffset / size.width) * units
|
||||||
let projected = draggedFrom + change
|
let projected = draggedFrom + change
|
||||||
return projected.isFinite ? (duration - projected < (0.01 * duration) ? duration : projected) : start
|
|
||||||
|
guard projected.isFinite && projected >= 0 && projected <= duration else {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return projected.clamped(to: 0 ... duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
var thumbOffset: Double {
|
var thumbOffset: Double {
|
||||||
let offset = dragging ? (draggedThumbHorizontalOffset + dragOffset) : thumbHorizontalOffset
|
let offset = dragging ? draggedThumbHorizontalOffset : thumbHorizontalOffset
|
||||||
return offset.isFinite ? offset : thumbLeadingOffset
|
return offset.isFinite ? offset : thumbLeadingOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
var thumbTooltipOffset: Double {
|
var thumbTooltipOffset: Double {
|
||||||
let offset = (dragging ? ((current * oneUnitWidth) + dragOffset) : (current * oneUnitWidth)) - (thumbTooltipWidth / 2)
|
let leadingOffset = size.width / 2 - (tooltipSize.width / 2)
|
||||||
|
let offsetForThumb = thumbOffset - thumbLeadingOffset
|
||||||
|
|
||||||
return offset.clamped(to: minThumbTooltipOffset ... maxThumbTooltipOffset)
|
guard offsetForThumb > tooltipSize.width / 2 else {
|
||||||
|
return -leadingOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbOffset.clamped(to: -leadingOffset ... leadingOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
var minThumbTooltipOffset: Double = -10
|
var minThumbTooltipOffset: Double {
|
||||||
|
60
|
||||||
|
}
|
||||||
|
|
||||||
var maxThumbTooltipOffset: Double {
|
var maxThumbTooltipOffset: Double {
|
||||||
max(minThumbTooltipOffset, (units * oneUnitWidth) - thumbTooltipWidth + 10)
|
max(minThumbTooltipOffset, units * oneUnitWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
var segments: [Segment] {
|
||||||
|
// [.init(category: "outro", segment: [25,30], uuid: UUID().uuidString, videoDuration: 100)] ??
|
||||||
|
player.sponsorBlock.segments
|
||||||
}
|
}
|
||||||
|
|
||||||
var segmentsLayers: some View {
|
var segmentsLayers: some View {
|
||||||
ForEach(player.sponsorBlock.segments, id: \.uuid) { segment in
|
ForEach(segments, id: \.uuid) { segment in
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
Rectangle()
|
||||||
.offset(x: segmentLayerHorizontalOffset(segment))
|
.offset(x: segmentLayerHorizontalOffset(segment))
|
||||||
.foregroundColor(.red)
|
.foregroundColor(Color("AppRedColor"))
|
||||||
.frame(maxHeight: height)
|
.frame(maxHeight: height)
|
||||||
.frame(width: segmentLayerWidth(segment))
|
.frame(width: segmentLayerWidth(segment))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var projectedSegment: Segment? {
|
||||||
|
segments.first { $0.timeInSegment(.secondsInDefaultTimescale(projectedValue)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectedChapter: Chapter? {
|
||||||
|
chapters.last { $0.start <= projectedValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var chaptersLayers: some View {
|
||||||
|
ForEach(chapters) { chapter in
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.fill(Color("AppBlueColor"))
|
||||||
|
.frame(maxWidth: 2, maxHeight: 12)
|
||||||
|
.offset(x: (chapter.start * oneUnitWidth) - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func segmentLayerHorizontalOffset(_ segment: Segment) -> Double {
|
func segmentLayerHorizontalOffset(_ segment: Segment) -> Double {
|
||||||
segment.start * oneUnitWidth
|
segment.start * oneUnitWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
func segmentLayerWidth(_ segment: Segment) -> Double {
|
func segmentLayerWidth(_ segment: Segment) -> Double {
|
||||||
let width = segment.duration * oneUnitWidth
|
let width = segment.duration * oneUnitWidth
|
||||||
return width.isFinite ? width : thumbLeadingOffset
|
return width.isFinite ? width : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var draggedThumbHorizontalOffset: Double {
|
var draggedThumbHorizontalOffset: Double {
|
||||||
thumbLeadingOffset + (draggedFrom * oneUnitWidth)
|
thumbLeadingOffset + (draggedFrom * oneUnitWidth) + dragOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
var thumbHorizontalOffset: Double {
|
var thumbHorizontalOffset: Double {
|
||||||
@ -161,7 +314,7 @@ struct TimelineView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var thumbLeadingOffset: Double {
|
var thumbLeadingOffset: Double {
|
||||||
-(size.width / 2)
|
-size.width / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
var oneUnitWidth: Double {
|
var oneUnitWidth: Double {
|
||||||
@ -172,26 +325,33 @@ struct TimelineView: View {
|
|||||||
var units: Double {
|
var units: Double {
|
||||||
duration - start
|
duration - start
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCurrent(_ current: Double) {
|
|
||||||
withAnimation {
|
|
||||||
self.current = current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TimelineView_Previews: PreviewProvider {
|
struct TimelineView_Previews: PreviewProvider {
|
||||||
|
static var duration = 100.0
|
||||||
|
static var current = 0.0
|
||||||
|
static var durationBinding: Binding<Double> = .init(
|
||||||
|
get: { duration },
|
||||||
|
set: { value in duration = value }
|
||||||
|
)
|
||||||
|
static var currentBinding = Binding<Double>(
|
||||||
|
get: { current },
|
||||||
|
set: { value in current = value }
|
||||||
|
)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: 40) {
|
let playerModel = PlayerModel()
|
||||||
TimelineView(duration: .constant(100), current: .constant(0))
|
playerModel.currentItem = .init(Video.fixture)
|
||||||
TimelineView(duration: .constant(100), current: .constant(1))
|
let playerTimeModel = PlayerTimeModel()
|
||||||
TimelineView(duration: .constant(100), current: .constant(30))
|
playerTimeModel.player = playerModel
|
||||||
TimelineView(duration: .constant(100), current: .constant(50))
|
playerTimeModel.currentTime = .secondsInDefaultTimescale(33)
|
||||||
TimelineView(duration: .constant(100), current: .constant(66))
|
playerTimeModel.duration = .secondsInDefaultTimescale(100)
|
||||||
TimelineView(duration: .constant(100), current: .constant(90))
|
return VStack(spacing: 40) {
|
||||||
TimelineView(duration: .constant(100), current: .constant(100))
|
TimelineView()
|
||||||
}
|
}
|
||||||
.environmentObject(PlayerModel())
|
.environmentObject(playerModel)
|
||||||
|
.environmentObject(playerTimeModel)
|
||||||
|
.environmentObject(PlayerControlsModel())
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,30 @@ import Defaults
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SDWebImageSwiftUI
|
import SDWebImageSwiftUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftUIPager
|
||||||
|
|
||||||
struct VideoDetails: View {
|
struct VideoDetails: View {
|
||||||
enum Page {
|
enum DetailsPage: CaseIterable {
|
||||||
case info, comments, related, queue
|
case info, chapters, comments, related, queue
|
||||||
|
|
||||||
|
var index: Int {
|
||||||
|
switch self {
|
||||||
|
case .info:
|
||||||
|
return 0
|
||||||
|
case .chapters:
|
||||||
|
return 1
|
||||||
|
case .comments:
|
||||||
|
return 2
|
||||||
|
case .related:
|
||||||
|
return 3
|
||||||
|
case .queue:
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binding var sidebarQueue: Bool
|
var sidebarQueue: Bool
|
||||||
@Binding var fullScreen: Bool
|
var fullScreen: Bool
|
||||||
|
|
||||||
@State private var subscribed = false
|
@State private var subscribed = false
|
||||||
@State private var subscriptionToggleButtonDisabled = false
|
@State private var subscriptionToggleButtonDisabled = false
|
||||||
@ -18,89 +34,82 @@ struct VideoDetails: View {
|
|||||||
@State private var presentingShareSheet = false
|
@State private var presentingShareSheet = false
|
||||||
@State private var shareURL: URL?
|
@State private var shareURL: URL?
|
||||||
|
|
||||||
@State private var currentPage = Page.info
|
@StateObject private var page: Page = .first()
|
||||||
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
|
||||||
@Environment(\.navigationStyle) private var navigationStyle
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@EnvironmentObject<CommentsModel> private var comments
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
@Default(.showKeywords) private var showKeywords
|
@Default(.showKeywords) private var showKeywords
|
||||||
|
@Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle
|
||||||
|
@Default(.controlsBarInPlayer) private var controlsBarInPlayer
|
||||||
|
|
||||||
init(
|
var currentPage: DetailsPage {
|
||||||
sidebarQueue: Binding<Bool>? = nil,
|
DetailsPage.allCases.first { $0.index == page.index } ?? .info
|
||||||
fullScreen: Binding<Bool>? = nil
|
|
||||||
) {
|
|
||||||
_sidebarQueue = sidebarQueue ?? .constant(true)
|
|
||||||
_fullScreen = fullScreen ?? .constant(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var video: Video? {
|
var video: Video? {
|
||||||
player.currentVideo
|
player.currentVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
func pageButton(
|
||||||
VStack(alignment: .leading) {
|
_ label: String,
|
||||||
Group {
|
_ symbolName: String,
|
||||||
Group {
|
_ destination: DetailsPage,
|
||||||
HStack(spacing: 0) {
|
pageChangeAction: (() -> Void)? = nil
|
||||||
title
|
) -> some View {
|
||||||
|
Button(action: {
|
||||||
|
page.update(.new(index: destination.index))
|
||||||
|
pageChangeAction?()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
toggleFullScreenDetailsButton
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: symbolName)
|
||||||
|
|
||||||
|
if playerDetailsPageButtonLabelStyle.text {
|
||||||
|
Text(label)
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
|
||||||
.padding(.top, 10)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if !video.isNil {
|
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptionsSection
|
|
||||||
.onChange(of: video) { video in
|
|
||||||
if let video = video {
|
|
||||||
subscribed = subscriptions.isSubscribing(video.channel.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.frame(minHeight: 15)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.foregroundColor(currentPage == destination ? .white : .accentColor)
|
||||||
|
|
||||||
if !sidebarQueue ||
|
Spacer()
|
||||||
(CommentsModel.enabled && CommentsModel.placement == .separate)
|
|
||||||
{
|
|
||||||
pagePicker
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onSwipeGesture(
|
}
|
||||||
up: {
|
.background(currentPage == destination ? Color.accentColor : .clear)
|
||||||
withAnimation {
|
.buttonStyle(.plain)
|
||||||
fullScreen = true
|
.font(.system(size: 10).bold())
|
||||||
}
|
.overlay(
|
||||||
},
|
RoundedRectangle(cornerRadius: 2)
|
||||||
down: {
|
.stroke(Color.accentColor, lineWidth: 2)
|
||||||
withAnimation {
|
.foregroundColor(.clear)
|
||||||
if fullScreen {
|
)
|
||||||
fullScreen = false
|
.frame(maxWidth: .infinity)
|
||||||
} else {
|
}
|
||||||
self.player.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
switch currentPage {
|
@ViewBuilder func detailsByPage(_ page: DetailsPage) -> some View {
|
||||||
|
Group {
|
||||||
|
switch page {
|
||||||
case .info:
|
case .info:
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
detailsPage
|
detailsPage
|
||||||
}
|
}
|
||||||
|
case .chapters:
|
||||||
|
ChaptersView()
|
||||||
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
|
|
||||||
case .queue:
|
case .queue:
|
||||||
PlayerQueueView(sidebarQueue: $sidebarQueue, fullScreen: $fullScreen)
|
PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: fullScreen)
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
|
|
||||||
case .related:
|
case .related:
|
||||||
@ -111,9 +120,54 @@ struct VideoDetails: View {
|
|||||||
.edgesIgnoringSafeArea(.horizontal)
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Group {
|
||||||
|
// Group {
|
||||||
|
// subscriptionsSection
|
||||||
|
// .border(.red, width: 4)
|
||||||
|
//
|
||||||
|
// .onChange(of: video) { video in
|
||||||
|
// if let video = video {
|
||||||
|
// subscribed = subscriptions.isSubscribing(video.channel.id)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .padding(.top, 4)
|
||||||
|
// .padding(.horizontal)
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
pageButton("Info", "info.circle", .info)
|
||||||
|
pageButton("Chapters", "bookmark", .chapters)
|
||||||
|
pageButton("Comments", "text.bubble", .comments) { comments.load() }
|
||||||
|
pageButton("Related", "rectangle.stack.fill", .related)
|
||||||
|
pageButton("Queue", "list.number", .queue)
|
||||||
|
}
|
||||||
|
.onChange(of: player.currentItem) { _ in
|
||||||
|
page.update(.moveToFirst)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
|
||||||
|
Pager(page: page, data: DetailsPage.allCases, id: \.self) {
|
||||||
|
detailsByPage($0)
|
||||||
|
}
|
||||||
|
.onPageWillChange { pageIndex in
|
||||||
|
if pageIndex == DetailsPage.comments.index {
|
||||||
|
comments.load()
|
||||||
|
} else {
|
||||||
|
print("comments not loading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if video.isNil && !sidebarQueue {
|
if video.isNil && !sidebarQueue {
|
||||||
currentPage = .queue
|
page.update(.new(index: DetailsPage.queue.index))
|
||||||
}
|
}
|
||||||
|
|
||||||
guard video != nil, accounts.app.supportsSubscriptions else {
|
guard video != nil, accounts.app.supportsSubscriptions else {
|
||||||
@ -124,91 +178,56 @@ struct VideoDetails: View {
|
|||||||
.onChange(of: sidebarQueue) { queue in
|
.onChange(of: sidebarQueue) { queue in
|
||||||
if queue {
|
if queue {
|
||||||
if currentPage == .related || currentPage == .queue {
|
if currentPage == .related || currentPage == .queue {
|
||||||
currentPage = .info
|
page.update(.moveToFirst)
|
||||||
}
|
}
|
||||||
} else if video.isNil {
|
} else if video.isNil {
|
||||||
currentPage = .queue
|
page.update(.moveToLast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
|
|
||||||
var title: some View {
|
var showAddToPlaylistButton: Bool {
|
||||||
Group {
|
accounts.app.supportsUserPlaylists && accounts.signedIn
|
||||||
if video != nil {
|
|
||||||
Text(video!.title)
|
|
||||||
.onAppear {
|
|
||||||
currentPage = .info
|
|
||||||
}
|
|
||||||
.contextMenu {
|
|
||||||
Button {
|
|
||||||
player.closeCurrentItem()
|
|
||||||
if !sidebarQueue {
|
|
||||||
currentPage = .queue
|
|
||||||
} else {
|
|
||||||
currentPage = .info
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Label("Close Video", systemImage: "xmark.circle")
|
|
||||||
}
|
|
||||||
.disabled(player.currentItem.isNil)
|
|
||||||
}
|
|
||||||
|
|
||||||
.font(.title2.bold())
|
|
||||||
} else {
|
|
||||||
Text("Not playing")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var toggleFullScreenDetailsButton: some View {
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
fullScreen.toggle()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Label("Resize", systemImage: fullScreen ? "chevron.down" : "chevron.up")
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
}
|
|
||||||
.help("Toggle fullscreen details")
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.keyboardShortcut("t")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptionsSection: some View {
|
var subscriptionsSection: some View {
|
||||||
Group {
|
Group {
|
||||||
if video != nil {
|
if let video = video {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Group {
|
Group {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
// ZStack(alignment: .bottomTrailing) {
|
||||||
authorAvatar
|
// authorAvatar
|
||||||
|
//
|
||||||
|
// if subscribed {
|
||||||
|
// Image(systemName: "star.circle.fill")
|
||||||
|
// .background(Color.background)
|
||||||
|
// .clipShape(Circle())
|
||||||
|
// .foregroundColor(.secondary)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if subscribed {
|
// VStack(alignment: .leading, spacing: 4) {
|
||||||
Image(systemName: "star.circle.fill")
|
// Text(video.title)
|
||||||
.background(Color.background)
|
// .font(.system(size: 11))
|
||||||
.clipShape(Circle())
|
// .fontWeight(.bold)
|
||||||
.foregroundColor(.secondary)
|
//
|
||||||
}
|
// HStack(spacing: 4) {
|
||||||
}
|
// Text(video.channel.name)
|
||||||
|
//
|
||||||
VStack(alignment: .leading) {
|
// if let subscribers = video.channel.subscriptionsString {
|
||||||
Text(video!.channel.name)
|
// Text("•")
|
||||||
.font(.system(size: 14))
|
// .foregroundColor(.secondary)
|
||||||
.bold()
|
// .opacity(0.3)
|
||||||
|
//
|
||||||
Group {
|
// Text("\(subscribers) subscribers")
|
||||||
if let subscribers = video!.channel.subscriptionsString {
|
// }
|
||||||
Text("\(subscribers) subscribers")
|
// }
|
||||||
}
|
// .foregroundColor(.secondary)
|
||||||
}
|
// .font(.caption2)
|
||||||
.foregroundColor(.secondary)
|
// }
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 12))
|
.contentShape(RoundedRectangle(cornerRadius: 12))
|
||||||
@ -227,83 +246,11 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if accounts.app.supportsSubscriptions, accounts.signedIn {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Section {
|
|
||||||
if subscribed {
|
|
||||||
Button("Unsubscribe") {
|
|
||||||
presentingUnsubscribeAlert = true
|
|
||||||
}
|
|
||||||
#if os(iOS)
|
|
||||||
.backport
|
|
||||||
.tint(.gray)
|
|
||||||
#endif
|
|
||||||
.alert(isPresented: $presentingUnsubscribeAlert) {
|
|
||||||
Alert(
|
|
||||||
title: Text(
|
|
||||||
"Are you sure you want to unsubscribe from \(video!.channel.name)?"
|
|
||||||
),
|
|
||||||
primaryButton: .destructive(Text("Unsubscribe")) {
|
|
||||||
subscriptionToggleButtonDisabled = true
|
|
||||||
|
|
||||||
subscriptions.unsubscribe(video!.channel.id) {
|
|
||||||
withAnimation {
|
|
||||||
subscriptionToggleButtonDisabled = false
|
|
||||||
subscribed.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondaryButton: .cancel()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button("Subscribe") {
|
|
||||||
subscriptionToggleButtonDisabled = true
|
|
||||||
|
|
||||||
subscriptions.subscribe(video!.channel.id) {
|
|
||||||
withAnimation {
|
|
||||||
subscriptionToggleButtonDisabled = false
|
|
||||||
subscribed.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.backport
|
|
||||||
.tint(subscriptionToggleButtonDisabled ? .gray : .blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled(subscriptionToggleButtonDisabled)
|
|
||||||
.font(.system(size: 13))
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pagePicker: some View {
|
|
||||||
Picker("Page", selection: $currentPage) {
|
|
||||||
if !video.isNil {
|
|
||||||
Text("Info").tag(Page.info)
|
|
||||||
if CommentsModel.enabled, CommentsModel.placement == .separate {
|
|
||||||
Text("Comments").tag(Page.comments)
|
|
||||||
}
|
|
||||||
if !sidebarQueue {
|
|
||||||
Text("Related").tag(Page.related)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !sidebarQueue {
|
|
||||||
Text("Queue").tag(Page.queue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.onDisappear {
|
|
||||||
currentPage = .info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var publishedDateSection: some View {
|
var publishedDateSection: some View {
|
||||||
Group {
|
Group {
|
||||||
if let video = player.currentVideo {
|
if let video = player.currentVideo {
|
||||||
@ -311,32 +258,11 @@ struct VideoDetails: View {
|
|||||||
if let published = video.publishedDate {
|
if let published = video.publishedDate {
|
||||||
Text(published)
|
Text(published)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let date = video.publishedAt {
|
|
||||||
if video.publishedDate != nil {
|
|
||||||
Text("•")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.opacity(0.3)
|
|
||||||
}
|
|
||||||
Text(formattedPublishedAt(date))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.font(.system(size: 12))
|
|
||||||
.padding(.bottom, -1)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formattedPublishedAt(_ date: Date) -> String {
|
|
||||||
let dateFormatter = DateFormatter()
|
|
||||||
|
|
||||||
dateFormatter.dateStyle = .short
|
|
||||||
dateFormatter.timeStyle = .none
|
|
||||||
|
|
||||||
return dateFormatter.string(from: date)
|
|
||||||
}
|
|
||||||
|
|
||||||
var countsSection: some View {
|
var countsSection: some View {
|
||||||
Group {
|
Group {
|
||||||
if let video = player.currentVideo {
|
if let video = player.currentVideo {
|
||||||
@ -386,13 +312,6 @@ struct VideoDetails: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(
|
|
||||||
EmptyView().sheet(isPresented: $presentingAddToPlaylist) {
|
|
||||||
if let video = video {
|
|
||||||
AddToPlaylistView(video: video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.background(
|
.background(
|
||||||
EmptyView().sheet(isPresented: $presentingShareSheet) {
|
EmptyView().sheet(isPresented: $presentingShareSheet) {
|
||||||
@ -419,31 +338,60 @@ struct VideoDetails: View {
|
|||||||
.retryOnAppear(true)
|
.retryOnAppear(true)
|
||||||
.indicator(.activity)
|
.indicator(.activity)
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.frame(width: 45, height: 45, alignment: .leading)
|
.frame(width: 35, height: 35, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var videoProperties: some View {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
publishedDateSection
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
if let views = video?.viewsCount {
|
||||||
|
Image(systemName: "eye")
|
||||||
|
|
||||||
|
Text(views)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let likes = video?.likesCount {
|
||||||
|
Image(systemName: "hand.thumbsup")
|
||||||
|
|
||||||
|
Text(likes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let likes = video?.dislikesCount {
|
||||||
|
Image(systemName: "hand.thumbsdown")
|
||||||
|
|
||||||
|
Text(likes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
var detailsPage: some View {
|
var detailsPage: some View {
|
||||||
Group {
|
Group {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
if let video = player.currentVideo {
|
if let video = player.currentVideo {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
HStack {
|
videoProperties
|
||||||
publishedDateSection
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
countsSection
|
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
.padding(.bottom, 6)
|
.padding(.bottom, 6)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
if let description = video.description {
|
if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
ForEach(1 ... Int.random(in: 3 ... 5), id: \.self) { _ in
|
||||||
|
Text(String(repeating: Video.fixture.description!, count: Int.random(in: 1 ... 4)))
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let description = video.description {
|
||||||
Group {
|
Group {
|
||||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||||
Text(description)
|
Text(description)
|
||||||
@ -531,7 +479,7 @@ struct VideoDetails: View {
|
|||||||
|
|
||||||
struct VideoDetails_Previews: PreviewProvider {
|
struct VideoDetails_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VideoDetails(sidebarQueue: .constant(true))
|
VideoDetails(sidebarQueue: true, fullScreen: false)
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct VideoPlayerView: View {
|
struct VideoPlayerView: View {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
static let hiddenOffset = max(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + 100
|
static let hiddenOffset = YatteeApp.isForPreviews ? 0 : max(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + 100
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static let defaultAspectRatio = 16 / 9.0
|
static let defaultAspectRatio = 16 / 9.0
|
||||||
@ -20,20 +20,22 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var playerSize: CGSize = .zero
|
@State private var playerSize: CGSize = .zero { didSet {
|
||||||
|
if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits {
|
||||||
|
sidebarQueue = true
|
||||||
|
} else {
|
||||||
|
sidebarQueue = false
|
||||||
|
}
|
||||||
|
}}
|
||||||
@State private var hoveringPlayer = false
|
@State private var hoveringPlayer = false
|
||||||
@State private var fullScreenDetails = false
|
@State private var fullScreenDetails = false
|
||||||
|
@State private var sidebarQueue = false
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
|
|
||||||
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
|
||||||
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
|
||||||
@Default(.lockOrientationInFullScreen) private var lockOrientationInFullScreen
|
|
||||||
|
|
||||||
@State private var motionManager: CMMotionManager!
|
@State private var motionManager: CMMotionManager!
|
||||||
@State private var orientation = UIInterfaceOrientation.portrait
|
@State private var orientation = UIInterfaceOrientation.portrait
|
||||||
@State private var lastOrientation: UIInterfaceOrientation?
|
@State private var lastOrientation: UIInterfaceOrientation?
|
||||||
@ -46,19 +48,29 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<ThumbnailsModel> private var thumbnails
|
@EnvironmentObject<ThumbnailsModel> private var thumbnails
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if Defaults[.playerSidebar] == .always {
|
||||||
|
sidebarQueue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
// TODO: remove
|
||||||
|
if #available(iOS 15.0, macOS 12.0, *) {
|
||||||
|
_ = Self._printChanges()
|
||||||
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
HSplitView {
|
return HSplitView {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
.onOpenURL { OpenURLHandler(accounts: accounts, player: player).handle($0) }
|
.onOpenURL { OpenURLHandler(accounts: accounts, player: player).handle($0) }
|
||||||
.frame(minWidth: 950, minHeight: 700)
|
.frame(minWidth: 950, minHeight: 700)
|
||||||
#else
|
#else
|
||||||
GeometryReader { geometry in
|
return GeometryReader { geometry in
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
content
|
content
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@ -79,6 +91,11 @@ struct VideoPlayerView: View {
|
|||||||
if newValue {
|
if newValue {
|
||||||
viewVerticalOffset = 0
|
viewVerticalOffset = 0
|
||||||
configureOrientationUpdatesBasedOnAccelerometer()
|
configureOrientationUpdatesBasedOnAccelerometer()
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak player] in
|
||||||
|
player?.onPresentPlayer?()
|
||||||
|
player?.onPresentPlayer = nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if Defaults[.lockPortraitWhenBrowsing] {
|
if Defaults[.lockPortraitWhenBrowsing] {
|
||||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||||
@ -95,7 +112,7 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.offset(y: viewVerticalOffset)
|
.offset(y: viewVerticalOffset)
|
||||||
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
|
.animation(.easeOut(duration: 0.3), value: viewVerticalOffset)
|
||||||
.backport
|
.backport
|
||||||
.persistentSystemOverlays(!fullScreenLayout)
|
.persistentSystemOverlays(!fullScreenLayout)
|
||||||
#endif
|
#endif
|
||||||
@ -104,7 +121,7 @@ struct VideoPlayerView: View {
|
|||||||
|
|
||||||
var content: some View {
|
var content: some View {
|
||||||
Group {
|
Group {
|
||||||
Group {
|
ZStack(alignment: .bottomLeading) {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
playerView
|
playerView
|
||||||
.ignoresSafeArea(.all, edges: .all)
|
.ignoresSafeArea(.all, edges: .all)
|
||||||
@ -138,17 +155,17 @@ struct VideoPlayerView: View {
|
|||||||
VideoPlayerSizeModifier(
|
VideoPlayerSizeModifier(
|
||||||
geometry: geometry,
|
geometry: geometry,
|
||||||
aspectRatio: player.avPlayerBackend.controller?.aspectRatio,
|
aspectRatio: player.avPlayerBackend.controller?.aspectRatio,
|
||||||
fullScreen: playerControls.playingFullscreen
|
fullScreen: player.playingFullScreen
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.overlay(playerPlaceholder(geometry: geometry))
|
// .overlay(playerPlaceholder(geometry: geometry))
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil)
|
.frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil)
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
hoveringPlayer = hovering
|
hoveringPlayer = hovering
|
||||||
hovering ? playerControls.show() : playerControls.hide()
|
// hovering ? playerControls.show() : playerControls.hide()
|
||||||
}
|
}
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
.gesture(
|
.gesture(
|
||||||
@ -169,9 +186,7 @@ struct VideoPlayerView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
viewVerticalOffset = drag
|
||||||
viewVerticalOffset = drag
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
.onEnded { _ in
|
||||||
if viewVerticalOffset > 100 {
|
if viewVerticalOffset > 100 {
|
||||||
@ -185,29 +200,30 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
#else
|
#else
|
||||||
.onAppear(perform: {
|
// .onAppear(perform: {
|
||||||
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
|
// NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
|
||||||
if hoveringPlayer {
|
// if hoveringPlayer {
|
||||||
playerControls.resetTimer()
|
// playerControls.resetTimer()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return $0
|
// return $0
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
if !playerControls.playingFullscreen {
|
if !player.playingFullScreen {
|
||||||
Group {
|
VStack(spacing: 0) {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if verticalSizeClass == .regular {
|
if verticalSizeClass == .regular {
|
||||||
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||||
@ -220,28 +236,35 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
if !fullScreenLayout {
|
||||||
|
ControlsBar()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 650)
|
.frame(minWidth: 650)
|
||||||
#endif
|
#endif
|
||||||
if !playerControls.playingFullscreen {
|
if !player.playingFullScreen {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if sidebarQueue {
|
if sidebarQueue {
|
||||||
PlayerQueueView(sidebarQueue: .constant(true), fullScreen: $fullScreenDetails)
|
PlayerQueueView(sidebarQueue: true, fullScreen: fullScreenDetails)
|
||||||
.frame(maxWidth: 350)
|
.frame(maxWidth: 350)
|
||||||
}
|
}
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
if Defaults[.playerSidebar] != .never {
|
if Defaults[.playerSidebar] != .never {
|
||||||
PlayerQueueView(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
PlayerQueueView(sidebarQueue: true, fullScreen: fullScreenDetails)
|
||||||
.frame(minWidth: 300)
|
.frame(minWidth: 300)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.transition(.asymmetric(insertion: .slide, removal: .identity))
|
||||||
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.statusBar(hidden: playerControls.playingFullscreen)
|
.statusBar(hidden: player.playingFullScreen)
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -285,9 +308,9 @@ struct VideoPlayerView: View {
|
|||||||
|
|
||||||
var fullScreenLayout: Bool {
|
var fullScreenLayout: Bool {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
playerControls.playingFullscreen || verticalSizeClass == .compact
|
player.playingFullScreen || verticalSizeClass == .compact
|
||||||
#else
|
#else
|
||||||
playerControls.playingFullscreen
|
player.playingFullScreen
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,29 +380,11 @@ struct VideoPlayerView: View {
|
|||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sidebarQueue: Bool {
|
|
||||||
switch Defaults[.playerSidebar] {
|
|
||||||
case .never:
|
|
||||||
return false
|
|
||||||
case .always:
|
|
||||||
return true
|
|
||||||
case .whenFits:
|
|
||||||
return playerSize.width > 900
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sidebarQueueBinding: Binding<Bool> {
|
|
||||||
Binding(
|
|
||||||
get: { sidebarQueue },
|
|
||||||
set: { _ in }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
private func configureOrientationUpdatesBasedOnAccelerometer() {
|
private func configureOrientationUpdatesBasedOnAccelerometer() {
|
||||||
if UIDevice.current.orientation.isLandscape,
|
if UIDevice.current.orientation.isLandscape,
|
||||||
enterFullscreenInLandscape,
|
Defaults[.enterFullscreenInLandscape],
|
||||||
!playerControls.playingFullscreen,
|
!player.playingFullScreen,
|
||||||
!player.playingInPictureInPicture
|
!player.playingInPictureInPicture
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@ -387,7 +392,7 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !honorSystemOrientationLock, motionManager.isNil else {
|
guard !Defaults[.honorSystemOrientationLock], motionManager.isNil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,7 +427,7 @@ struct VideoPlayerView: View {
|
|||||||
|
|
||||||
if orientation.isLandscape {
|
if orientation.isLandscape {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||||
guard enterFullscreenInLandscape else {
|
guard Defaults[.enterFullscreenInLandscape] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,7 +438,7 @@ struct VideoPlayerView: View {
|
|||||||
|
|
||||||
Orientation.lockOrientation(orientationLockMask, andRotateTo: orientation)
|
Orientation.lockOrientation(orientationLockMask, andRotateTo: orientation)
|
||||||
|
|
||||||
guard lockOrientationInFullScreen else {
|
guard Defaults[.lockOrientationInFullScreen] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,8 +447,8 @@ struct VideoPlayerView: View {
|
|||||||
} else {
|
} else {
|
||||||
guard abs(acceleration.z) <= 0.74,
|
guard abs(acceleration.z) <= 0.74,
|
||||||
player.lockedOrientation.isNil,
|
player.lockedOrientation.isNil,
|
||||||
enterFullscreenInLandscape,
|
Defaults[.enterFullscreenInLandscape],
|
||||||
!lockOrientationInFullScreen
|
!Defaults[.lockOrientationInFullScreen]
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -462,14 +467,14 @@ struct VideoPlayerView: View {
|
|||||||
let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
|
let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
|
||||||
if newOrientation?.isLandscape ?? false,
|
if newOrientation?.isLandscape ?? false,
|
||||||
player.presentingPlayer,
|
player.presentingPlayer,
|
||||||
lockOrientationInFullScreen,
|
Defaults[.lockOrientationInFullScreen],
|
||||||
!player.lockedOrientation.isNil
|
!player.lockedOrientation.isNil
|
||||||
{
|
{
|
||||||
Orientation.lockOrientation(.landscape, andRotateTo: newOrientation)
|
Orientation.lockOrientation(.landscape, andRotateTo: newOrientation)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard player.presentingPlayer, enterFullscreenInLandscape, honorSystemOrientationLock else {
|
guard player.presentingPlayer, Defaults[.enterFullscreenInLandscape], Defaults[.honorSystemOrientationLock] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,11 +9,11 @@ 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
|
||||||
|
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlaylistsModel> private var model
|
@EnvironmentObject<PlaylistsModel> private var model
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -123,14 +123,8 @@ 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(
|
|
||||||
title: Text("Error when accessing playlist"),
|
|
||||||
message: Text(error)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
#endif
|
#endif
|
||||||
@ -166,20 +160,9 @@ struct AddToPlaylistView: View {
|
|||||||
|
|
||||||
Defaults[.lastUsedPlaylistID] = id
|
Defaults[.lastUsedPlaylistID] = id
|
||||||
|
|
||||||
submitButtonDisabled = true
|
model.addVideo(playlistID: id, videoID: video.videoID, navigation: navigation)
|
||||||
|
|
||||||
model.addVideo(
|
presentationMode.wrappedValue.dismiss()
|
||||||
playlistID: id,
|
|
||||||
videoID: video.videoID,
|
|
||||||
onSuccess: {
|
|
||||||
presentationMode.wrappedValue.dismiss()
|
|
||||||
},
|
|
||||||
onFailure: { requestError in
|
|
||||||
error = "(\(requestError.httpStatusCode ?? -1)) \(requestError.userMessage)"
|
|
||||||
presentingErrorAlert = true
|
|
||||||
submitButtonDisabled = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectedPlaylist: Playlist? {
|
private var selectedPlaylist: Playlist? {
|
||||||
|
@ -66,8 +66,9 @@ struct SearchSuggestions: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.id(UUID())
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.buttonStyle(.link)
|
.buttonStyle(.link)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,6 +297,7 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
.redrawOn(change: recentsChanged)
|
.redrawOn(change: recentsChanged)
|
||||||
}
|
}
|
||||||
|
.id(UUID())
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
|
@ -1,146 +1,230 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import SDWebImageSwiftUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
||||||
let content: Content
|
enum Context {
|
||||||
let toolbar: Toolbar?
|
case browser, player
|
||||||
|
|
||||||
@Environment(\.navigationStyle) private var navigationStyle
|
|
||||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
|
||||||
@EnvironmentObject<PlayerModel> private var model
|
|
||||||
|
|
||||||
init(@ViewBuilder toolbar: @escaping () -> Toolbar? = { nil }, @ViewBuilder content: @escaping () -> Content) {
|
|
||||||
self.content = content()
|
|
||||||
self.toolbar = toolbar()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(@ViewBuilder content: @escaping () -> Content) where Toolbar == EmptyView {
|
let content: Content
|
||||||
self.init(toolbar: { EmptyView() }, content: content)
|
|
||||||
|
init(
|
||||||
|
context _: Context? = nil,
|
||||||
|
@ViewBuilder toolbar: @escaping () -> Toolbar? = { nil },
|
||||||
|
@ViewBuilder content: @escaping () -> Content
|
||||||
|
) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: Context? = nil,
|
||||||
|
@ViewBuilder content: @escaping () -> Content
|
||||||
|
) where Toolbar == EmptyView {
|
||||||
|
self.init(context: context, toolbar: { EmptyView() }, content: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottomLeading) {
|
if #available(iOS 15.0, macOS 12.0, *) {
|
||||||
|
_ = Self._printChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
return VStack(spacing: 0) {
|
||||||
content
|
content
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.frame(minHeight: 0, maxHeight: .infinity)
|
ControlsBar()
|
||||||
#endif
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
|
||||||
Group {
|
|
||||||
#if !os(tvOS)
|
|
||||||
#if !os(macOS)
|
|
||||||
toolbar
|
|
||||||
.frame(height: 100)
|
|
||||||
.offset(x: 0, y: -28)
|
|
||||||
#endif
|
|
||||||
controls
|
|
||||||
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
.borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
|
||||||
#if os(macOS)
|
|
||||||
.background(VisualEffectBlur(material: .sidebar))
|
|
||||||
#elseif os(iOS)
|
|
||||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial).edgesIgnoringSafeArea(.all))
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var controls: some View {
|
|
||||||
HStack {
|
|
||||||
Button(action: {
|
|
||||||
model.togglePlayer()
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
|
||||||
Text(model.currentVideo?.title ?? "Not playing")
|
|
||||||
.font(.system(size: 14).bold())
|
|
||||||
.foregroundColor(model.currentItem.isNil ? .secondary : .accentColor)
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
if let video = model.currentVideo {
|
|
||||||
Text(video.author)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.font(.system(size: 10))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.vertical)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
.padding(.vertical, 20)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Group {
|
|
||||||
if !model.currentItem.isNil {
|
|
||||||
Button {
|
|
||||||
model.closeCurrentItem()
|
|
||||||
model.closePiP()
|
|
||||||
} label: {
|
|
||||||
Label("Close Video", systemImage: "xmark")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if playerControls.isPlaying {
|
|
||||||
Button(action: {
|
|
||||||
model.pause()
|
|
||||||
}) {
|
|
||||||
Label("Pause", systemImage: "pause.fill")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button(action: {
|
|
||||||
model.play()
|
|
||||||
}) {
|
|
||||||
Label("Play", systemImage: "play.fill")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
|
|
||||||
.font(.system(size: 30))
|
|
||||||
.frame(minWidth: 30)
|
|
||||||
|
|
||||||
Button(action: { model.advanceToNextItem() }) {
|
|
||||||
Label("Next", systemImage: "forward.fill")
|
|
||||||
.padding(.vertical)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
.disabled(model.queue.isEmpty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.padding(.horizontal)
|
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 55)
|
|
||||||
.padding(.vertical, 0)
|
|
||||||
.borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
|
||||||
.borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor"))
|
|
||||||
#if !os(tvOS)
|
|
||||||
.onSwipeGesture(up: {
|
|
||||||
model.show()
|
|
||||||
})
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private var progressViewValue: Double {
|
|
||||||
[model.time?.seconds, model.videoDuration].compactMap { $0 }.min() ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private var progressViewTotal: Double {
|
|
||||||
model.videoDuration ?? 100
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
||||||
|
// enum Context {
|
||||||
|
// case browser, player
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let context: Context
|
||||||
|
// let content: Content
|
||||||
|
// let toolbar: Toolbar?
|
||||||
|
//
|
||||||
|
// @Environment(\.navigationStyle) private var navigationStyle
|
||||||
|
// @EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||||
|
// @EnvironmentObject<PlayerModel> private var model
|
||||||
|
//
|
||||||
|
// var barHeight: Double {
|
||||||
|
// 75
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// init(
|
||||||
|
// context: Context? = nil,
|
||||||
|
// @ViewBuilder toolbar: @escaping () -> Toolbar? = { nil },
|
||||||
|
// @ViewBuilder content: @escaping () -> Content
|
||||||
|
// ) {
|
||||||
|
// self.context = context ?? .browser
|
||||||
|
// self.content = content()
|
||||||
|
// self.toolbar = toolbar()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// init(
|
||||||
|
// context: Context? = nil,
|
||||||
|
// @ViewBuilder content: @escaping () -> Content
|
||||||
|
// ) where Toolbar == EmptyView {
|
||||||
|
// self.init(context: context, toolbar: { EmptyView() }, content: content)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var body: some View {
|
||||||
|
// ZStack(alignment: .bottomLeading) {
|
||||||
|
// VStack(spacing: 0) {
|
||||||
|
// content
|
||||||
|
//
|
||||||
|
// Color.clear.frame(height: barHeight)
|
||||||
|
// }
|
||||||
|
// #if !os(tvOS)
|
||||||
|
// .frame(minHeight: 0, maxHeight: .infinity)
|
||||||
|
// #endif
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// VStack {
|
||||||
|
// #if !os(tvOS)
|
||||||
|
// #if !os(macOS)
|
||||||
|
// toolbar
|
||||||
|
// .frame(height: 100)
|
||||||
|
// .offset(x: 0, y: -28)
|
||||||
|
// #endif
|
||||||
|
//
|
||||||
|
// if context != .player || !playerControls.playingFullscreen {
|
||||||
|
// controls
|
||||||
|
// }
|
||||||
|
// #endif
|
||||||
|
// }
|
||||||
|
// .borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
||||||
|
// #if os(macOS)
|
||||||
|
// .background(VisualEffectBlur(material: .sidebar))
|
||||||
|
// #elseif os(iOS)
|
||||||
|
// .background(VisualEffectBlur(blurStyle: .systemThinMaterial).edgesIgnoringSafeArea(.all))
|
||||||
|
// #endif
|
||||||
|
// }
|
||||||
|
// .background(Color.debug)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private var controls: some View {
|
||||||
|
// VStack(spacing: 0) {
|
||||||
|
// TimelineView(duration: playerControls.durationBinding, current: playerControls.currentTimeBinding)
|
||||||
|
// .foregroundColor(.secondary)
|
||||||
|
//
|
||||||
|
// Button(action: {
|
||||||
|
// model.togglePlayer()
|
||||||
|
// }) {
|
||||||
|
// HStack(spacing: 8) {
|
||||||
|
// authorAvatar
|
||||||
|
//
|
||||||
|
// VStack(alignment: .leading, spacing: 5) {
|
||||||
|
// Text(model.currentVideo?.title ?? "Not playing")
|
||||||
|
// .font(.headline)
|
||||||
|
// .foregroundColor(model.currentVideo.isNil ? .secondary : .accentColor)
|
||||||
|
// .lineLimit(1)
|
||||||
|
//
|
||||||
|
// Text(model.currentVideo?.author ?? "")
|
||||||
|
// .font(.subheadline)
|
||||||
|
// .foregroundColor(.secondary)
|
||||||
|
// .lineLimit(1)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Spacer()
|
||||||
|
//
|
||||||
|
// HStack {
|
||||||
|
// Group {
|
||||||
|
// if !model.currentItem.isNil {
|
||||||
|
// Button {
|
||||||
|
// model.closeCurrentItem()
|
||||||
|
// model.closePiP()
|
||||||
|
// } label: {
|
||||||
|
// Label("Close Video", systemImage: "xmark")
|
||||||
|
// .padding(.horizontal, 4)
|
||||||
|
// .contentShape(Rectangle())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if playerControls.isPlaying {
|
||||||
|
// Button(action: {
|
||||||
|
// model.pause()
|
||||||
|
// }) {
|
||||||
|
// Label("Pause", systemImage: "pause.fill")
|
||||||
|
// .padding(.horizontal, 4)
|
||||||
|
// .contentShape(Rectangle())
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// Button(action: {
|
||||||
|
// model.play()
|
||||||
|
// }) {
|
||||||
|
// Label("Play", systemImage: "play.fill")
|
||||||
|
// .padding(.horizontal, 4)
|
||||||
|
// .contentShape(Rectangle())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
|
||||||
|
// .font(.system(size: 30))
|
||||||
|
// .frame(minWidth: 30)
|
||||||
|
//
|
||||||
|
// Button(action: { model.advanceToNextItem() }) {
|
||||||
|
// Label("Next", systemImage: "forward.fill")
|
||||||
|
// .padding(.vertical)
|
||||||
|
// .contentShape(Rectangle())
|
||||||
|
// }
|
||||||
|
// .disabled(model.queue.isEmpty)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .buttonStyle(.plain)
|
||||||
|
// .contentShape(Rectangle())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .buttonStyle(.plain)
|
||||||
|
// .labelStyle(.iconOnly)
|
||||||
|
// .padding(.horizontal)
|
||||||
|
// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight)
|
||||||
|
// .borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
||||||
|
// .borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor"))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private var authorAvatar: some View {
|
||||||
|
// Group {
|
||||||
|
// if let video = model.currentItem?.video, let url = video.channel.thumbnailURL {
|
||||||
|
// WebImage(url: url)
|
||||||
|
// .resizable()
|
||||||
|
// .placeholder {
|
||||||
|
// Rectangle().fill(Color("PlaceholderColor"))
|
||||||
|
// }
|
||||||
|
// .retryOnAppear(true)
|
||||||
|
// .indicator(.activity)
|
||||||
|
// .clipShape(Circle())
|
||||||
|
// .frame(width: 44, height: 44, alignment: .leading)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private var progressViewValue: Double {
|
||||||
|
// [model.time?.seconds, model.videoDuration].compactMap { $0 }.min() ?? 0
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private var progressViewTotal: Double {
|
||||||
|
// model.videoDuration ?? 100
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
struct PlayerControlsView_Previews: PreviewProvider {
|
struct PlayerControlsView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
BrowserPlayerControls {
|
BrowserPlayerControls(context: .player) {
|
||||||
VStack {
|
BrowserPlayerControls {
|
||||||
Spacer()
|
VStack {
|
||||||
Text("Hello")
|
Spacer()
|
||||||
Spacer()
|
Text("Hello")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.offset(y: -100)
|
||||||
}
|
}
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
}
|
}
|
||||||
|
221
Shared/Views/ControlsBar.swift
Normal file
221
Shared/Views/ControlsBar.swift
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import Defaults
|
||||||
|
import SDWebImageSwiftUI
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftUIPager
|
||||||
|
|
||||||
|
struct ControlsBar: View {
|
||||||
|
enum Pages: CaseIterable {
|
||||||
|
case details, controls
|
||||||
|
}
|
||||||
|
|
||||||
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
|
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||||
|
@EnvironmentObject<PlayerModel> private var model
|
||||||
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
|
|
||||||
|
@StateObject private var controlsPage = Page.first()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Pager(page: controlsPage, data: Pages.allCases, id: \.self) { index in
|
||||||
|
switch index {
|
||||||
|
case .details:
|
||||||
|
details
|
||||||
|
default:
|
||||||
|
controls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pagingPriority(.simultaneous)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight)
|
||||||
|
.borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
||||||
|
.borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor"))
|
||||||
|
.modifier(ControlBackgroundModifier(edgesIgnoringSafeArea: .bottom))
|
||||||
|
}
|
||||||
|
|
||||||
|
var controls: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Group {
|
||||||
|
Button {
|
||||||
|
model.closeCurrentItem()
|
||||||
|
model.closePiP()
|
||||||
|
} label: {
|
||||||
|
Label("Close Video", systemImage: "xmark")
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: { model.backend.seek(to: 0) }) {
|
||||||
|
Label("Restart", systemImage: "backward.end.fill")
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
model.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||||
|
} label: {
|
||||||
|
Label("Backward", systemImage: "gobackward.10")
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if playerControls.isPlaying {
|
||||||
|
Button(action: {
|
||||||
|
model.pause()
|
||||||
|
}) {
|
||||||
|
Label("Pause", systemImage: "pause.fill")
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(action: {
|
||||||
|
model.play()
|
||||||
|
}) {
|
||||||
|
Label("Play", systemImage: "play.fill")
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
model.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||||
|
} label: {
|
||||||
|
Label("Forward", systemImage: "goforward.10")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
|
||||||
|
|
||||||
|
Button(action: { model.advanceToNextItem() }) {
|
||||||
|
Label("Next", systemImage: "forward.fill")
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.disabled(model.queue.isEmpty)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
.font(.system(size: 24))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
var barHeight: Double {
|
||||||
|
75
|
||||||
|
}
|
||||||
|
|
||||||
|
var details: some View {
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
authorAvatar
|
||||||
|
.contextMenu {
|
||||||
|
if let video = model.currentVideo {
|
||||||
|
Group {
|
||||||
|
Section {
|
||||||
|
Text(video.title)
|
||||||
|
|
||||||
|
if accounts.app.supportsUserPlaylists && accounts.signedIn {
|
||||||
|
Section {
|
||||||
|
Button {
|
||||||
|
navigation.presentAddToPlaylist(video)
|
||||||
|
} label: {
|
||||||
|
Label("Add to Playlist...", systemImage: "text.badge.plus")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let playlist = playlists.lastUsed, let video = model.currentVideo {
|
||||||
|
Button {
|
||||||
|
playlists.addVideo(playlistID: playlist.id, videoID: video.videoID, navigation: navigation)
|
||||||
|
} label: {
|
||||||
|
Label("Add to \(playlist.title)", systemImage: "text.badge.star")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {} label: {
|
||||||
|
Label("Share", systemImage: "square.and.arrow.up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button {
|
||||||
|
NavigationModel.openChannel(
|
||||||
|
video.channel,
|
||||||
|
player: model,
|
||||||
|
recents: recents,
|
||||||
|
navigation: navigation,
|
||||||
|
navigationStyle: navigationStyle
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {} label: {
|
||||||
|
Label("Unsubscribe", systemImage: "xmark.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelStyle(.automatic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text(model.currentVideo?.title ?? "Not playing")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(model.currentVideo.isNil ? .secondary : .accentColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(model.currentVideo?.author ?? "")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var authorAvatar: some View {
|
||||||
|
Button {
|
||||||
|
model.togglePlayer()
|
||||||
|
} label: {
|
||||||
|
if let video = model.currentItem?.video, let url = video.channel.thumbnailURL {
|
||||||
|
WebImage(url: url)
|
||||||
|
.resizable()
|
||||||
|
.placeholder {
|
||||||
|
Rectangle().fill(Color("PlaceholderColor"))
|
||||||
|
}
|
||||||
|
.retryOnAppear(true)
|
||||||
|
.indicator(.activity)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "play.rectangle")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.font(.system(size: 30))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 44, height: 44, alignment: .leading)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ControlsBar_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ControlsBar()
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
}
|
||||||
|
}
|
@ -77,9 +77,10 @@ struct VideoContextMenuView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if accounts.app.supportsUserPlaylists {
|
if accounts.app.supportsUserPlaylists, accounts.signedIn {
|
||||||
Section {
|
Section {
|
||||||
addToPlaylistButton
|
addToPlaylistButton
|
||||||
|
addToLastPlaylistButton
|
||||||
|
|
||||||
if let id = navigation.tabSelection?.playlistID ?? playlistID {
|
if let id = navigation.tabSelection?.playlistID ?? playlistID {
|
||||||
removeFromPlaylistButton(playlistID: id)
|
removeFromPlaylistButton(playlistID: id)
|
||||||
@ -116,7 +117,7 @@ struct VideoContextMenuView: View {
|
|||||||
Button {
|
Button {
|
||||||
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt))
|
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt))
|
||||||
} label: {
|
} label: {
|
||||||
Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause")
|
Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime(allowZero: true) ?? "where I left off")", systemImage: "playpause")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,6 +231,16 @@ struct VideoContextMenuView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var addToLastPlaylistButton: some View {
|
||||||
|
if let playlist = playlists.lastUsed {
|
||||||
|
Button {
|
||||||
|
playlists.addVideo(playlistID: playlist.id, videoID: video.videoID, navigation: navigation)
|
||||||
|
} label: {
|
||||||
|
Label("Add to \(playlist.title)", systemImage: "text.badge.star")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func removeFromPlaylistButton(playlistID: String) -> some View {
|
func removeFromPlaylistButton(playlistID: String) -> some View {
|
||||||
Button {
|
Button {
|
||||||
playlists.removeVideo(index: video.indexID!, playlistID: playlistID)
|
playlists.removeVideo(index: video.indexID!, playlistID: playlistID)
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import Defaults
|
import Defaults
|
||||||
|
import MediaPlayer
|
||||||
|
import PINCache
|
||||||
|
import SDWebImage
|
||||||
|
import SDWebImageWebPCoder
|
||||||
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@ -11,19 +16,27 @@ struct YatteeApp: App {
|
|||||||
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
|
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var isForPreviews: Bool {
|
||||||
|
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||||
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@State private var configured = false
|
||||||
|
|
||||||
@StateObject private var accounts = AccountsModel()
|
@StateObject private var accounts = AccountsModel()
|
||||||
@StateObject private var comments = CommentsModel()
|
@StateObject private var comments = CommentsModel()
|
||||||
@StateObject private var instances = InstancesModel()
|
@StateObject private var instances = InstancesModel()
|
||||||
@StateObject private var menu = MenuModel()
|
@StateObject private var menu = MenuModel()
|
||||||
@StateObject private var navigation = NavigationModel()
|
@StateObject private var navigation = NavigationModel()
|
||||||
|
@StateObject private var networkState = NetworkStateModel()
|
||||||
@StateObject private var player = PlayerModel()
|
@StateObject private var player = PlayerModel()
|
||||||
@StateObject private var playerControls = PlayerControlsModel()
|
@StateObject private var playerControls = PlayerControlsModel()
|
||||||
|
@StateObject private var playerTime = PlayerTimeModel()
|
||||||
@StateObject private var playlists = PlaylistsModel()
|
@StateObject private var playlists = PlaylistsModel()
|
||||||
@StateObject private var recents = RecentsModel()
|
@StateObject private var recents = RecentsModel()
|
||||||
@StateObject private var search = SearchModel()
|
@StateObject private var search = SearchModel()
|
||||||
@ -35,13 +48,16 @@ struct YatteeApp: App {
|
|||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.onAppear(perform: configure)
|
||||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.environmentObject(accounts)
|
.environmentObject(accounts)
|
||||||
.environmentObject(comments)
|
.environmentObject(comments)
|
||||||
.environmentObject(instances)
|
.environmentObject(instances)
|
||||||
.environmentObject(navigation)
|
.environmentObject(navigation)
|
||||||
|
.environmentObject(networkState)
|
||||||
.environmentObject(player)
|
.environmentObject(player)
|
||||||
.environmentObject(playerControls)
|
.environmentObject(playerControls)
|
||||||
|
.environmentObject(playerTime)
|
||||||
.environmentObject(playlists)
|
.environmentObject(playlists)
|
||||||
.environmentObject(recents)
|
.environmentObject(recents)
|
||||||
.environmentObject(subscriptions)
|
.environmentObject(subscriptions)
|
||||||
@ -86,6 +102,7 @@ struct YatteeApp: App {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
WindowGroup(player.windowTitle) {
|
WindowGroup(player.windowTitle) {
|
||||||
VideoPlayerView()
|
VideoPlayerView()
|
||||||
|
.onAppear(perform: configure)
|
||||||
.background(
|
.background(
|
||||||
HostingWindowFinder { window in
|
HostingWindowFinder { window in
|
||||||
Windows.playerWindow = window
|
Windows.playerWindow = window
|
||||||
@ -96,7 +113,7 @@ struct YatteeApp: App {
|
|||||||
queue: OperationQueue.main
|
queue: OperationQueue.main
|
||||||
) { _ in
|
) { _ in
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
self.player.controls.playingFullscreen = false
|
self.player.playingFullScreen = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,8 +126,10 @@ struct YatteeApp: App {
|
|||||||
.environmentObject(comments)
|
.environmentObject(comments)
|
||||||
.environmentObject(instances)
|
.environmentObject(instances)
|
||||||
.environmentObject(navigation)
|
.environmentObject(navigation)
|
||||||
|
.environmentObject(networkState)
|
||||||
.environmentObject(player)
|
.environmentObject(player)
|
||||||
.environmentObject(playerControls)
|
.environmentObject(playerControls)
|
||||||
|
.environmentObject(playerTime)
|
||||||
.environmentObject(playlists)
|
.environmentObject(playlists)
|
||||||
.environmentObject(recents)
|
.environmentObject(recents)
|
||||||
.environmentObject(subscriptions)
|
.environmentObject(subscriptions)
|
||||||
@ -129,4 +148,132 @@ struct YatteeApp: App {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configure() {
|
||||||
|
guard !Self.isForPreviews, !configured else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configured = true
|
||||||
|
|
||||||
|
SiestaLog.Category.enabled = .common
|
||||||
|
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
|
||||||
|
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
configureNowPlayingInfoCenter()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
if Defaults[.lockPortraitWhenBrowsing] {
|
||||||
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if let account = accounts.lastUsed ??
|
||||||
|
instances.lastUsed?.anonymousAccount ??
|
||||||
|
InstancesModel.all.first?.anonymousAccount
|
||||||
|
{
|
||||||
|
accounts.setCurrent(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
if accounts.current.isNil {
|
||||||
|
navigation.presentingWelcomeScreen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
playlists.accounts = accounts
|
||||||
|
search.accounts = accounts
|
||||||
|
subscriptions.accounts = accounts
|
||||||
|
|
||||||
|
comments.player = player
|
||||||
|
|
||||||
|
menu.accounts = accounts
|
||||||
|
menu.navigation = navigation
|
||||||
|
menu.player = player
|
||||||
|
|
||||||
|
playerControls.player = player
|
||||||
|
|
||||||
|
player.accounts = accounts
|
||||||
|
player.comments = comments
|
||||||
|
player.controls = playerControls
|
||||||
|
player.networkState = networkState
|
||||||
|
player.playerTime = playerTime
|
||||||
|
|
||||||
|
if !accounts.current.isNil {
|
||||||
|
player.restoreQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Defaults[.saveRecents] {
|
||||||
|
recents.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
var section = Defaults[.visibleSections].min()?.tabSelection
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
if section == .playlists {
|
||||||
|
section = .search
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
navigation.tabSelection = section ?? .search
|
||||||
|
|
||||||
|
subscriptions.load()
|
||||||
|
playlists.load()
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
Windows.player.open()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
Windows.main.focus()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureNowPlayingInfoCenter() {
|
||||||
|
#if !os(macOS)
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||||
|
|
||||||
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
MPRemoteCommandCenter.shared().playCommand.addTarget { _ in
|
||||||
|
player.play()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
MPRemoteCommandCenter.shared().pauseCommand.addTarget { _ in
|
||||||
|
player.pause()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
||||||
|
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = false
|
||||||
|
|
||||||
|
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.addTarget { remoteEvent in
|
||||||
|
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent
|
||||||
|
else {
|
||||||
|
return .commandFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
player.backend.seek(to: event.positionTime)
|
||||||
|
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
let skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand
|
||||||
|
skipForwardCommand.isEnabled = true
|
||||||
|
skipForwardCommand.preferredIntervals = [10]
|
||||||
|
|
||||||
|
skipForwardCommand.addTarget { _ in
|
||||||
|
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
let skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand
|
||||||
|
skipBackwardCommand.isEnabled = true
|
||||||
|
skipBackwardCommand.preferredIntervals = [10]
|
||||||
|
|
||||||
|
skipBackwardCommand.addTarget { _ in
|
||||||
|
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +141,7 @@
|
|||||||
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
3711404026B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
3711404026B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
3711404126B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
3711404126B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
|
371264432865FFD700D77974 /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; };
|
||||||
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
||||||
37130A5C277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
37130A5C277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
||||||
37130A5D277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
37130A5D277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
||||||
@ -190,6 +191,8 @@
|
|||||||
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 */; };
|
||||||
|
372CFD15285F2E2A00B0B54B /* ControlsBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372CFD14285F2E2A00B0B54B /* ControlsBar.swift */; };
|
||||||
|
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372CFD14285F2E2A00B0B54B /* ControlsBar.swift */; };
|
||||||
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; };
|
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; };
|
||||||
372D85DF283842EC00FF3C7D /* 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 */; };
|
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F22838388A000CFD59 /* PlayerLayerView.swift */; };
|
||||||
@ -288,6 +291,18 @@
|
|||||||
3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */; };
|
3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */; };
|
||||||
3751BA8427E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */; };
|
3751BA8427E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */; };
|
||||||
3751BA8527E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */; };
|
3751BA8527E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */; };
|
||||||
|
37520699285E8DD300CA655F /* Chapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37520698285E8DD300CA655F /* Chapter.swift */; };
|
||||||
|
3752069A285E8DD300CA655F /* Chapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37520698285E8DD300CA655F /* Chapter.swift */; };
|
||||||
|
3752069B285E8DD300CA655F /* Chapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37520698285E8DD300CA655F /* Chapter.swift */; };
|
||||||
|
3752069D285E910600CA655F /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChaptersView.swift */; };
|
||||||
|
3752069E285E910600CA655F /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChaptersView.swift */; };
|
||||||
|
3752069F285E910600CA655F /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChaptersView.swift */; };
|
||||||
|
3756C2A62861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; };
|
||||||
|
3756C2A72861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; };
|
||||||
|
3756C2A82861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; };
|
||||||
|
3756C2AA2861151C00E4B059 /* NetworkStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A92861151C00E4B059 /* NetworkStateModel.swift */; };
|
||||||
|
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A92861151C00E4B059 /* NetworkStateModel.swift */; };
|
||||||
|
3756C2AC2861151C00E4B059 /* NetworkStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A92861151C00E4B059 /* NetworkStateModel.swift */; };
|
||||||
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37579D5C27864F5F00FD0B98 /* Help.swift */; };
|
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37579D5C27864F5F00FD0B98 /* Help.swift */; };
|
||||||
37579D5E27864F5F00FD0B98 /* Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37579D5C27864F5F00FD0B98 /* Help.swift */; };
|
37579D5E27864F5F00FD0B98 /* Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37579D5C27864F5F00FD0B98 /* Help.swift */; };
|
||||||
37579D5F27864F5F00FD0B98 /* Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37579D5C27864F5F00FD0B98 /* Help.swift */; };
|
37579D5F27864F5F00FD0B98 /* Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37579D5C27864F5F00FD0B98 /* Help.swift */; };
|
||||||
@ -313,6 +328,9 @@
|
|||||||
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
||||||
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; };
|
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; };
|
||||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; };
|
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; };
|
||||||
|
376527BB285F60F700102284 /* PlayerTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376527BA285F60F700102284 /* PlayerTimeModel.swift */; };
|
||||||
|
376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376527BA285F60F700102284 /* PlayerTimeModel.swift */; };
|
||||||
|
376527BD285F60F700102284 /* PlayerTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376527BA285F60F700102284 /* PlayerTimeModel.swift */; };
|
||||||
376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
|
376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
|
||||||
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
|
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
|
||||||
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
|
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
|
||||||
@ -370,8 +388,6 @@
|
|||||||
37732FF42703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
37732FF42703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
||||||
37732FF52703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
37732FF52703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
||||||
37737786276F9858000521C1 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37737785276F9858000521C1 /* Windows.swift */; };
|
37737786276F9858000521C1 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37737785276F9858000521C1 /* Windows.swift */; };
|
||||||
3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3774122927387B6C00423605 /* InstancesModelTests.swift */; };
|
|
||||||
3774122F27387C7600423605 /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
|
||||||
3774123327387CB000423605 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
3774123327387CB000423605 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||||
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||||
3774123527387CC700423605 /* PipedAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3700155A271B0D4D0049C794 /* PipedAPI.swift */; };
|
3774123527387CC700423605 /* PipedAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3700155A271B0D4D0049C794 /* PipedAPI.swift */; };
|
||||||
@ -394,14 +410,9 @@
|
|||||||
3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C3A24427235DA70087A57A /* ChannelPlaylist.swift */; };
|
3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C3A24427235DA70087A57A /* ChannelPlaylist.swift */; };
|
||||||
3774125A27387D2300423605 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F33272B44000087F250 /* FavoritesModel.swift */; };
|
3774125A27387D2300423605 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F33272B44000087F250 /* FavoritesModel.swift */; };
|
||||||
3774125B27387D2300423605 /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
3774125B27387D2300423605 /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
||||||
3774125D27387D2D00423605 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; };
|
|
||||||
3774125E27387D2D00423605 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
|
||||||
3774125F27387D2D00423605 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33E32720CB35000C1D6B /* Account.swift */; };
|
|
||||||
3774126027387D2D00423605 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; };
|
3774126027387D2D00423605 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; };
|
||||||
3774126127387D2D00423605 /* AccountsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37001562271B1F250049C794 /* AccountsModel.swift */; };
|
3774126127387D2D00423605 /* AccountsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37001562271B1F250049C794 /* AccountsModel.swift */; };
|
||||||
3774126227387D2D00423605 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
3774126227387D2D00423605 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||||
3774126327387D2D00423605 /* InstancesBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA12729D98A0011DE61 /* InstancesBridge.swift */; };
|
|
||||||
3774126427387D4A00423605 /* VideosAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D526DD2720AC4400ED2F5E /* VideosAPI.swift */; };
|
|
||||||
3774126527387D6D00423605 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; };
|
3774126527387D6D00423605 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; };
|
||||||
3774126627387D6D00423605 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
|
3774126627387D6D00423605 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
|
||||||
3774126727387D6D00423605 /* View+Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA51270F284F00E4D32B /* View+Borders.swift */; };
|
3774126727387D6D00423605 /* View+Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA51270F284F00E4D32B /* View+Borders.swift */; };
|
||||||
@ -462,6 +473,11 @@
|
|||||||
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; };
|
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; };
|
||||||
378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */; };
|
378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */; };
|
||||||
378E510026FE8EEE00F49626 /* AccountsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */; };
|
378E510026FE8EEE00F49626 /* AccountsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */; };
|
||||||
|
378FFBC428660172009E3FBE /* URLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378FFBC328660172009E3FBE /* URLParser.swift */; };
|
||||||
|
378FFBC528660172009E3FBE /* URLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378FFBC328660172009E3FBE /* URLParser.swift */; };
|
||||||
|
378FFBC628660172009E3FBE /* URLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378FFBC328660172009E3FBE /* URLParser.swift */; };
|
||||||
|
378FFBC728660172009E3FBE /* URLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378FFBC328660172009E3FBE /* URLParser.swift */; };
|
||||||
|
378FFBC92866018A009E3FBE /* URLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378FFBC82866018A009E3FBE /* URLParserTests.swift */; };
|
||||||
3795593627B08538007FF8F4 /* StreamControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3795593527B08538007FF8F4 /* StreamControl.swift */; };
|
3795593627B08538007FF8F4 /* StreamControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3795593527B08538007FF8F4 /* StreamControl.swift */; };
|
||||||
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3795593527B08538007FF8F4 /* StreamControl.swift */; };
|
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3795593527B08538007FF8F4 /* StreamControl.swift */; };
|
||||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 3797757C268922D100DD52A8 /* Siesta */; };
|
3797757D268922D100DD52A8 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 3797757C268922D100DD52A8 /* Siesta */; };
|
||||||
@ -482,6 +498,11 @@
|
|||||||
37A3B19627257503000FB5EE /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16427255E7F000FB5EE /* content.js */; };
|
37A3B19627257503000FB5EE /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16427255E7F000FB5EE /* content.js */; };
|
||||||
37A3B1982725750B000FB5EE /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16027255E7F000FB5EE /* manifest.json */; };
|
37A3B1982725750B000FB5EE /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16027255E7F000FB5EE /* manifest.json */; };
|
||||||
37A3B19B2725750F000FB5EE /* images in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B15E27255E7F000FB5EE /* images */; };
|
37A3B19B2725750F000FB5EE /* images in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B15E27255E7F000FB5EE /* images */; };
|
||||||
|
37A5DBC4285DFF5400CA4DD1 /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = 37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */; };
|
||||||
|
37A5DBC6285E06B100CA4DD1 /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = 37A5DBC5285E06B100CA4DD1 /* SwiftUIPager */; };
|
||||||
|
37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; };
|
||||||
|
37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; };
|
||||||
|
37A5DBCA285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; };
|
||||||
37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; };
|
37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; };
|
||||||
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; };
|
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; };
|
||||||
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; };
|
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; };
|
||||||
@ -575,6 +596,7 @@
|
|||||||
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
||||||
37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
||||||
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
||||||
|
37C0C0FF28665EAC007F6F78 /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
||||||
37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
||||||
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
||||||
37C2211D27ADA33300305B41 /* MPVViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C2211C27ADA33300305B41 /* MPVViewController.swift */; };
|
37C2211D27ADA33300305B41 /* MPVViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C2211C27ADA33300305B41 /* MPVViewController.swift */; };
|
||||||
@ -604,10 +626,6 @@
|
|||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
|
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
|
||||||
37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; };
|
|
||||||
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; };
|
|
||||||
37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */; };
|
|
||||||
37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; };
|
|
||||||
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; };
|
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; };
|
||||||
37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; };
|
37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; };
|
||||||
37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; };
|
37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */; };
|
||||||
@ -700,10 +718,22 @@
|
|||||||
37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
||||||
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
||||||
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
|
||||||
|
37F13B62285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
||||||
|
37F13B63285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
||||||
|
37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */; };
|
||||||
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
|
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
|
||||||
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
|
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
|
||||||
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
|
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
|
||||||
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */; };
|
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */; };
|
||||||
|
37F4AD1B28612B23004D0F66 /* OpeningStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD1A28612B23004D0F66 /* OpeningStream.swift */; };
|
||||||
|
37F4AD1C28612B23004D0F66 /* OpeningStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD1A28612B23004D0F66 /* OpeningStream.swift */; };
|
||||||
|
37F4AD1D28612B23004D0F66 /* OpeningStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD1A28612B23004D0F66 /* OpeningStream.swift */; };
|
||||||
|
37F4AD1F28612DFD004D0F66 /* Buffering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD1E28612DFD004D0F66 /* Buffering.swift */; };
|
||||||
|
37F4AD2028612DFD004D0F66 /* Buffering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD1E28612DFD004D0F66 /* Buffering.swift */; };
|
||||||
|
37F4AD2128612DFD004D0F66 /* Buffering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD1E28612DFD004D0F66 /* Buffering.swift */; };
|
||||||
|
37F4AD2628613B81004D0F66 /* Color+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD2528613B81004D0F66 /* Color+Debug.swift */; };
|
||||||
|
37F4AD2728613B81004D0F66 /* Color+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD2528613B81004D0F66 /* Color+Debug.swift */; };
|
||||||
|
37F4AD2828613B81004D0F66 /* Color+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AD2528613B81004D0F66 /* Color+Debug.swift */; };
|
||||||
37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
||||||
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
||||||
37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
||||||
@ -847,6 +877,7 @@
|
|||||||
370F500A27CC176F001B35DC /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
|
370F500A27CC176F001B35DC /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
|
||||||
371114EA27B94C8800C2EF7B /* RepeatingTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer.swift; sourceTree = "<group>"; };
|
371114EA27B94C8800C2EF7B /* RepeatingTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer.swift; sourceTree = "<group>"; };
|
||||||
3711403E26B206A6005B3555 /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
|
3711403E26B206A6005B3555 /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
|
||||||
|
3712643B2865FF4500D77974 /* Shared Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Shared Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
37130A5A277657090033018A /* Yattee.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Yattee.xcdatamodel; sourceTree = "<group>"; };
|
37130A5A277657090033018A /* Yattee.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Yattee.xcdatamodel; sourceTree = "<group>"; };
|
||||||
37130A5E277657300033018A /* PersistenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
37130A5E277657300033018A /* PersistenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||||
37136CAB286273060095C0CF /* PersistentSystemOverlays+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistentSystemOverlays+Backport.swift"; sourceTree = "<group>"; };
|
37136CAB286273060095C0CF /* PersistentSystemOverlays+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistentSystemOverlays+Backport.swift"; sourceTree = "<group>"; };
|
||||||
@ -867,6 +898,7 @@
|
|||||||
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>"; };
|
||||||
|
372CFD14285F2E2A00B0B54B /* ControlsBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsBar.swift; sourceTree = "<group>"; };
|
||||||
373031F22838388A000CFD59 /* PlayerLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLayerView.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>"; };
|
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>"; };
|
||||||
@ -919,6 +951,10 @@
|
|||||||
3751BA7D27E63F1D007B1A60 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = "<group>"; };
|
3751BA7D27E63F1D007B1A60 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = "<group>"; };
|
||||||
3751BA7F27E64244007B1A60 /* VideoLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoLayer.swift; sourceTree = "<group>"; };
|
3751BA7F27E64244007B1A60 /* VideoLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoLayer.swift; sourceTree = "<group>"; };
|
||||||
3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnYouTubeDislikeAPI.swift; sourceTree = "<group>"; };
|
3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnYouTubeDislikeAPI.swift; sourceTree = "<group>"; };
|
||||||
|
37520698285E8DD300CA655F /* Chapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chapter.swift; sourceTree = "<group>"; };
|
||||||
|
3752069C285E910600CA655F /* ChaptersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChaptersView.swift; sourceTree = "<group>"; };
|
||||||
|
3756C2A52861131100E4B059 /* NetworkState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkState.swift; sourceTree = "<group>"; };
|
||||||
|
3756C2A92861151C00E4B059 /* NetworkStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStateModel.swift; sourceTree = "<group>"; };
|
||||||
37579D5C27864F5F00FD0B98 /* Help.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Help.swift; sourceTree = "<group>"; };
|
37579D5C27864F5F00FD0B98 /* Help.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Help.swift; sourceTree = "<group>"; };
|
||||||
37599F2F272B42810087F250 /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = "<group>"; };
|
37599F2F272B42810087F250 /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = "<group>"; };
|
||||||
37599F33272B44000087F250 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = "<group>"; };
|
37599F33272B44000087F250 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = "<group>"; };
|
||||||
@ -928,6 +964,7 @@
|
|||||||
375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = "<group>"; };
|
375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = "<group>"; };
|
||||||
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
||||||
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = "<group>"; };
|
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = "<group>"; };
|
||||||
|
376527BA285F60F700102284 /* PlayerTimeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeModel.swift; sourceTree = "<group>"; };
|
||||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
|
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
|
||||||
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
|
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
|
||||||
376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; };
|
376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; };
|
||||||
@ -960,7 +997,6 @@
|
|||||||
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidationStatus.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
37737785276F9858000521C1 /* Windows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
|
||||||
3774122927387B6C00423605 /* InstancesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModelTests.swift; sourceTree = "<group>"; };
|
|
||||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
||||||
3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = "<group>"; };
|
3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = "<group>"; };
|
||||||
3782B9512755667600990149 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = "<group>"; };
|
3782B9512755667600990149 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = "<group>"; };
|
||||||
@ -972,6 +1008,8 @@
|
|||||||
378AE942274EF00A006A4EE1 /* Color+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Background.swift"; sourceTree = "<group>"; };
|
378AE942274EF00A006A4EE1 /* Color+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Background.swift"; sourceTree = "<group>"; };
|
||||||
378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
|
378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
|
||||||
378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsMenuView.swift; sourceTree = "<group>"; };
|
378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsMenuView.swift; sourceTree = "<group>"; };
|
||||||
|
378FFBC328660172009E3FBE /* URLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLParser.swift; sourceTree = "<group>"; };
|
||||||
|
378FFBC82866018A009E3FBE /* URLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLParserTests.swift; sourceTree = "<group>"; };
|
||||||
3795593527B08538007FF8F4 /* StreamControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamControl.swift; sourceTree = "<group>"; };
|
3795593527B08538007FF8F4 /* StreamControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamControl.swift; sourceTree = "<group>"; };
|
||||||
37977582268922F600DD52A8 /* InvidiousAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvidiousAPI.swift; sourceTree = "<group>"; };
|
37977582268922F600DD52A8 /* InvidiousAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvidiousAPI.swift; sourceTree = "<group>"; };
|
||||||
3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
||||||
@ -985,6 +1023,7 @@
|
|||||||
37A3B16C27255E7F000FB5EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
37A3B16C27255E7F000FB5EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
37A3B16D27255E7F000FB5EE /* Open in Yattee.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Open in Yattee.entitlements"; sourceTree = "<group>"; };
|
37A3B16D27255E7F000FB5EE /* Open in Yattee.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Open in Yattee.entitlements"; sourceTree = "<group>"; };
|
||||||
37A3B1792725735F000FB5EE /* Open in Yattee (iOS).appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Open in Yattee (iOS).appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
37A3B1792725735F000FB5EE /* Open in Yattee (iOS).appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Open in Yattee (iOS).appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlBackgroundModifier.swift; sourceTree = "<group>"; };
|
||||||
37A9965926D6F8CA006E3224 /* HorizontalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCells.swift; sourceTree = "<group>"; };
|
37A9965926D6F8CA006E3224 /* HorizontalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCells.swift; sourceTree = "<group>"; };
|
||||||
37A9965D26D6F9B9006E3224 /* FavoritesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesView.swift; sourceTree = "<group>"; };
|
37A9965D26D6F9B9006E3224 /* FavoritesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesView.swift; sourceTree = "<group>"; };
|
||||||
37AAF27D26737323007FC770 /* PopularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularView.swift; sourceTree = "<group>"; };
|
37AAF27D26737323007FC770 /* PopularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularView.swift; sourceTree = "<group>"; };
|
||||||
@ -1042,8 +1081,6 @@
|
|||||||
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = "<group>"; };
|
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = "<group>"; };
|
37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = "<group>"; };
|
||||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
||||||
37CB12782724C76D00213B45 /* VideoURLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParser.swift; sourceTree = "<group>"; };
|
|
||||||
37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParserTests.swift; sourceTree = "<group>"; };
|
|
||||||
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = "<group>"; };
|
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = "<group>"; };
|
||||||
37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueView.swift; sourceTree = "<group>"; };
|
37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueView.swift; sourceTree = "<group>"; };
|
||||||
37CC3F4F270D010D00608308 /* VideoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBanner.swift; sourceTree = "<group>"; };
|
37CC3F4F270D010D00608308 /* VideoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBanner.swift; sourceTree = "<group>"; };
|
||||||
@ -1091,7 +1128,11 @@
|
|||||||
37EBD8C927AF26C200F1C24B /* MPVBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVBackend.swift; sourceTree = "<group>"; };
|
37EBD8C927AF26C200F1C24B /* MPVBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVBackend.swift; sourceTree = "<group>"; };
|
||||||
37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; };
|
37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; };
|
||||||
37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = "<group>"; };
|
37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = "<group>"; };
|
||||||
|
37F13B61285E43C000B137E4 /* ControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsOverlay.swift; sourceTree = "<group>"; };
|
||||||
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
|
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
|
37F4AD1A28612B23004D0F66 /* OpeningStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpeningStream.swift; sourceTree = "<group>"; };
|
||||||
|
37F4AD1E28612DFD004D0F66 /* Buffering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buffering.swift; sourceTree = "<group>"; };
|
||||||
|
37F4AD2528613B81004D0F66 /* Color+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Debug.swift"; sourceTree = "<group>"; };
|
||||||
37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; };
|
37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; };
|
||||||
37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; };
|
37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; };
|
||||||
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapRecognizerViewModifier.swift; sourceTree = "<group>"; };
|
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapRecognizerViewModifier.swift; sourceTree = "<group>"; };
|
||||||
@ -1106,6 +1147,13 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
371264382865FF4500D77974 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
37A3B15427255E7F000FB5EE /* Frameworks */ = {
|
37A3B15427255E7F000FB5EE /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -1152,6 +1200,7 @@
|
|||||||
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */,
|
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */,
|
||||||
37BD07C72698B27B003EBB87 /* Introspect in Frameworks */,
|
37BD07C72698B27B003EBB87 /* Introspect in Frameworks */,
|
||||||
37FB284D2722099E00A57617 /* SDWebImageWebPCoder in Frameworks */,
|
37FB284D2722099E00A57617 /* SDWebImageWebPCoder in Frameworks */,
|
||||||
|
37A5DBC4285DFF5400CA4DD1 /* SwiftUIPager in Frameworks */,
|
||||||
3749BF8A27ADA135000480FF /* libavformat.a in Frameworks */,
|
3749BF8A27ADA135000480FF /* libavformat.a in Frameworks */,
|
||||||
377FC7DB267A080300A6BBAF /* Logging in Frameworks */,
|
377FC7DB267A080300A6BBAF /* Logging in Frameworks */,
|
||||||
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */,
|
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */,
|
||||||
@ -1190,6 +1239,7 @@
|
|||||||
370F4FD927CC16CB001B35DC /* libxcb-shm.0.0.0.dylib in Frameworks */,
|
370F4FD927CC16CB001B35DC /* libxcb-shm.0.0.0.dylib in Frameworks */,
|
||||||
370F4FCA27CC16CB001B35DC /* libXau.6.dylib in Frameworks */,
|
370F4FCA27CC16CB001B35DC /* libXau.6.dylib in Frameworks */,
|
||||||
370F4FD527CC16CB001B35DC /* libfontconfig.1.dylib in Frameworks */,
|
370F4FD527CC16CB001B35DC /* libfontconfig.1.dylib in Frameworks */,
|
||||||
|
37A5DBC6285E06B100CA4DD1 /* SwiftUIPager in Frameworks */,
|
||||||
370F4FCE27CC16CB001B35DC /* libswresample.4.3.100.dylib in Frameworks */,
|
370F4FCE27CC16CB001B35DC /* libswresample.4.3.100.dylib in Frameworks */,
|
||||||
370F4FDA27CC16CB001B35DC /* libmpv.dylib in Frameworks */,
|
370F4FDA27CC16CB001B35DC /* libmpv.dylib in Frameworks */,
|
||||||
370F4FD827CC16CB001B35DC /* libcrypto.3.dylib in Frameworks */,
|
370F4FD827CC16CB001B35DC /* libcrypto.3.dylib in Frameworks */,
|
||||||
@ -1312,11 +1362,22 @@
|
|||||||
371114F227B9552400C2EF7B /* Controls */ = {
|
371114F227B9552400C2EF7B /* Controls */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
3756C2A428610F6D00E4B059 /* OSD */,
|
||||||
37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */,
|
37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */,
|
||||||
|
37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */,
|
||||||
|
37F13B61285E43C000B137E4 /* ControlsOverlay.swift */,
|
||||||
);
|
);
|
||||||
path = Controls;
|
path = Controls;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
3712643C2865FF4500D77974 /* Shared Tests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
378FFBC82866018A009E3FBE /* URLParserTests.swift */,
|
||||||
|
);
|
||||||
|
path = "Shared Tests";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
371AAE2326CEB9E800901972 /* Navigation */ = {
|
371AAE2326CEB9E800901972 /* Navigation */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -1339,6 +1400,7 @@
|
|||||||
375E45F327B1973400BA7902 /* MPV */,
|
375E45F327B1973400BA7902 /* MPV */,
|
||||||
37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */,
|
37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */,
|
||||||
37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */,
|
37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */,
|
||||||
|
3752069C285E910600CA655F /* ChaptersView.swift */,
|
||||||
371B7E602759706A00D21217 /* CommentsView.swift */,
|
371B7E602759706A00D21217 /* CommentsView.swift */,
|
||||||
37EF9A75275BEB8E0043B585 /* CommentView.swift */,
|
37EF9A75275BEB8E0043B585 /* CommentView.swift */,
|
||||||
37DD9DA22785BBC900539416 /* NoCommentsView.swift */,
|
37DD9DA22785BBC900539416 /* NoCommentsView.swift */,
|
||||||
@ -1397,6 +1459,7 @@
|
|||||||
37C3A250272366440087A57A /* ChannelPlaylistView.swift */,
|
37C3A250272366440087A57A /* ChannelPlaylistView.swift */,
|
||||||
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */,
|
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */,
|
||||||
37FB285D272225E800A57617 /* ContentItemView.swift */,
|
37FB285D272225E800A57617 /* ContentItemView.swift */,
|
||||||
|
372CFD14285F2E2A00B0B54B /* ControlsBar.swift */,
|
||||||
3748186D26A769D60084E870 /* DetailBadge.swift */,
|
3748186D26A769D60084E870 /* DetailBadge.swift */,
|
||||||
37599F37272B4D740087F250 /* FavoriteButton.swift */,
|
37599F37272B4D740087F250 /* FavoriteButton.swift */,
|
||||||
37152EE926EFEB95004FB96D /* LazyView.swift */,
|
37152EE926EFEB95004FB96D /* LazyView.swift */,
|
||||||
@ -1453,6 +1516,7 @@
|
|||||||
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */,
|
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */,
|
||||||
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */,
|
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */,
|
||||||
375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */,
|
375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */,
|
||||||
|
376527BA285F60F700102284 /* PlayerTimeModel.swift */,
|
||||||
);
|
);
|
||||||
path = Player;
|
path = Player;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1574,6 +1638,16 @@
|
|||||||
path = ReturnYouTubeDislike;
|
path = ReturnYouTubeDislike;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
3756C2A428610F6D00E4B059 /* OSD */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3756C2A52861131100E4B059 /* NetworkState.swift */,
|
||||||
|
37F4AD1A28612B23004D0F66 /* OpeningStream.swift */,
|
||||||
|
37F4AD1E28612DFD004D0F66 /* Buffering.swift */,
|
||||||
|
);
|
||||||
|
path = OSD;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
375E45F327B1973400BA7902 /* MPV */ = {
|
375E45F327B1973400BA7902 /* MPV */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -1717,6 +1791,7 @@
|
|||||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
||||||
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */,
|
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */,
|
||||||
378AE942274EF00A006A4EE1 /* Color+Background.swift */,
|
378AE942274EF00A006A4EE1 /* Color+Background.swift */,
|
||||||
|
37F4AD2528613B81004D0F66 /* Color+Debug.swift */,
|
||||||
37E8B0EF27B326F30024006F /* Comparable+Clamped.swift */,
|
37E8B0EF27B326F30024006F /* Comparable+Clamped.swift */,
|
||||||
37C3A240272359900087A57A /* Double+Format.swift */,
|
37C3A240272359900087A57A /* Double+Format.swift */,
|
||||||
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
||||||
@ -1743,6 +1818,7 @@
|
|||||||
37DD9DCC2785EE6F00539416 /* Vendor */,
|
37DD9DCC2785EE6F00539416 /* Vendor */,
|
||||||
3748186426A762300084E870 /* Fixtures */,
|
3748186426A762300084E870 /* Fixtures */,
|
||||||
37A3B15827255E7F000FB5EE /* Open in Yattee */,
|
37A3B15827255E7F000FB5EE /* Open in Yattee */,
|
||||||
|
3712643C2865FF4500D77974 /* Shared Tests */,
|
||||||
377FC7D1267A080300A6BBAF /* Frameworks */,
|
377FC7D1267A080300A6BBAF /* Frameworks */,
|
||||||
37D4B0CA2671614900C925CA /* Products */,
|
37D4B0CA2671614900C925CA /* Products */,
|
||||||
37D4B174267164B000C925CA /* Tests Apple TV */,
|
37D4B174267164B000C925CA /* Tests Apple TV */,
|
||||||
@ -1774,7 +1850,7 @@
|
|||||||
371114EA27B94C8800C2EF7B /* RepeatingTimer.swift */,
|
371114EA27B94C8800C2EF7B /* RepeatingTimer.swift */,
|
||||||
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */,
|
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */,
|
||||||
37FFC43F272734C3009FFD26 /* Throttle.swift */,
|
37FFC43F272734C3009FFD26 /* Throttle.swift */,
|
||||||
37CB12782724C76D00213B45 /* VideoURLParser.swift */,
|
378FFBC328660172009E3FBE /* URLParser.swift */,
|
||||||
37D4B0C22671614700C925CA /* YatteeApp.swift */,
|
37D4B0C22671614700C925CA /* YatteeApp.swift */,
|
||||||
37D4B0C42671614800C925CA /* Assets.xcassets */,
|
37D4B0C42671614800C925CA /* Assets.xcassets */,
|
||||||
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
|
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
|
||||||
@ -1793,6 +1869,7 @@
|
|||||||
37D4B171267164B000C925CA /* Tests (tvOS).xctest */,
|
37D4B171267164B000C925CA /* Tests (tvOS).xctest */,
|
||||||
37A3B15727255E7F000FB5EE /* Open in Yattee (macOS).appex */,
|
37A3B15727255E7F000FB5EE /* Open in Yattee (macOS).appex */,
|
||||||
37A3B1792725735F000FB5EE /* Open in Yattee (iOS).appex */,
|
37A3B1792725735F000FB5EE /* Open in Yattee (iOS).appex */,
|
||||||
|
3712643B2865FF4500D77974 /* Shared Tests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1809,9 +1886,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
37BA796C26DC4105002A0235 /* Extensions */,
|
37BA796C26DC4105002A0235 /* Extensions */,
|
||||||
3774122927387B6C00423605 /* InstancesModelTests.swift */,
|
|
||||||
37D4B0E22671614900C925CA /* Tests_macOS.swift */,
|
37D4B0E22671614900C925CA /* Tests_macOS.swift */,
|
||||||
37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */,
|
|
||||||
);
|
);
|
||||||
path = "Tests macOS";
|
path = "Tests macOS";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1849,6 +1924,7 @@
|
|||||||
374C0539272436DA009BDDBE /* SponsorBlock */,
|
374C0539272436DA009BDDBE /* SponsorBlock */,
|
||||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||||
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
||||||
|
37520698285E8DD300CA655F /* Chapter.swift */,
|
||||||
371B7E5B27596B8400D21217 /* Comment.swift */,
|
371B7E5B27596B8400D21217 /* Comment.swift */,
|
||||||
371B7E692759791900D21217 /* CommentsModel.swift */,
|
371B7E692759791900D21217 /* CommentsModel.swift */,
|
||||||
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
|
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
|
||||||
@ -1859,6 +1935,7 @@
|
|||||||
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
||||||
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
||||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||||
|
3756C2A92861151C00E4B059 /* NetworkStateModel.swift */,
|
||||||
37130A5E277657300033018A /* PersistenceController.swift */,
|
37130A5E277657300033018A /* PersistenceController.swift */,
|
||||||
376578882685471400D4EA09 /* Playlist.swift */,
|
376578882685471400D4EA09 /* Playlist.swift */,
|
||||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||||
@ -1987,6 +2064,23 @@
|
|||||||
/* End PBXHeadersBuildPhase section */
|
/* End PBXHeadersBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
3712643A2865FF4500D77974 /* Shared Tests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 371264412865FF4500D77974 /* Build configuration list for PBXNativeTarget "Shared Tests" */;
|
||||||
|
buildPhases = (
|
||||||
|
371264372865FF4500D77974 /* Sources */,
|
||||||
|
371264382865FF4500D77974 /* Frameworks */,
|
||||||
|
371264392865FF4500D77974 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "Shared Tests";
|
||||||
|
productName = "Shared Tests";
|
||||||
|
productReference = 3712643B2865FF4500D77974 /* Shared Tests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
37A3B15627255E7F000FB5EE /* Open in Yattee (macOS) */ = {
|
37A3B15627255E7F000FB5EE /* Open in Yattee (macOS) */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 37A3B17427255E7F000FB5EE /* Build configuration list for PBXNativeTarget "Open in Yattee (macOS)" */;
|
buildConfigurationList = 37A3B17427255E7F000FB5EE /* Build configuration list for PBXNativeTarget "Open in Yattee (macOS)" */;
|
||||||
@ -2050,6 +2144,7 @@
|
|||||||
37FB285527220D9000A57617 /* SDWebImagePINPlugin */,
|
37FB285527220D9000A57617 /* SDWebImagePINPlugin */,
|
||||||
3765917B27237D21009F956E /* PINCache */,
|
3765917B27237D21009F956E /* PINCache */,
|
||||||
37CF8B8328535E4F00B71E37 /* SDWebImage */,
|
37CF8B8328535E4F00B71E37 /* SDWebImage */,
|
||||||
|
37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */,
|
||||||
);
|
);
|
||||||
productName = "Yattee (iOS)";
|
productName = "Yattee (iOS)";
|
||||||
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
||||||
@ -2082,6 +2177,7 @@
|
|||||||
3703206727D2BB45007A0CB8 /* Defaults */,
|
3703206727D2BB45007A0CB8 /* Defaults */,
|
||||||
3703206927D2BB49007A0CB8 /* Alamofire */,
|
3703206927D2BB49007A0CB8 /* Alamofire */,
|
||||||
37CF8B8528535E5A00B71E37 /* SDWebImage */,
|
37CF8B8528535E5A00B71E37 /* SDWebImage */,
|
||||||
|
37A5DBC5285E06B100CA4DD1 /* SwiftUIPager */,
|
||||||
);
|
);
|
||||||
productName = "Yattee (macOS)";
|
productName = "Yattee (macOS)";
|
||||||
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
||||||
@ -2186,9 +2282,12 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 1310;
|
LastSwiftUpdateCheck = 1400;
|
||||||
LastUpgradeCheck = 1400;
|
LastUpgradeCheck = 1400;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
3712643A2865FF4500D77974 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
};
|
||||||
37A3B15627255E7F000FB5EE = {
|
37A3B15627255E7F000FB5EE = {
|
||||||
CreatedOnToolsVersion = 13.1;
|
CreatedOnToolsVersion = 13.1;
|
||||||
};
|
};
|
||||||
@ -2248,6 +2347,7 @@
|
|||||||
37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */,
|
37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */,
|
||||||
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */,
|
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */,
|
||||||
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */,
|
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */,
|
||||||
|
37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -2264,11 +2364,19 @@
|
|||||||
37D4B0D32671614900C925CA /* Tests (iOS) */,
|
37D4B0D32671614900C925CA /* Tests (iOS) */,
|
||||||
37D4B0DD2671614900C925CA /* Tests (macOS) */,
|
37D4B0DD2671614900C925CA /* Tests (macOS) */,
|
||||||
37D4B170267164B000C925CA /* Tests (tvOS) */,
|
37D4B170267164B000C925CA /* Tests (tvOS) */,
|
||||||
|
3712643A2865FF4500D77974 /* Shared Tests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
371264392865FF4500D77974 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
37A3B15527255E7F000FB5EE /* Resources */ = {
|
37A3B15527255E7F000FB5EE /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -2447,6 +2555,17 @@
|
|||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
371264372865FF4500D77974 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
371264432865FFD700D77974 /* CMTime+DefaultTimescale.swift in Sources */,
|
||||||
|
378FFBC728660172009E3FBE /* URLParser.swift in Sources */,
|
||||||
|
37C0C0FF28665EAC007F6F78 /* VideosApp.swift in Sources */,
|
||||||
|
378FFBC92866018A009E3FBE /* URLParserTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
37A3B15327255E7F000FB5EE /* Sources */ = {
|
37A3B15327255E7F000FB5EE /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -2474,15 +2593,17 @@
|
|||||||
371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */,
|
371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||||
37E8B0F027B326F30024006F /* Comparable+Clamped.swift in Sources */,
|
37E8B0F027B326F30024006F /* Comparable+Clamped.swift in Sources */,
|
||||||
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||||
|
372CFD15285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
||||||
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
||||||
37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||||
37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */,
|
378FFBC428660172009E3FBE /* URLParser.swift in Sources */,
|
||||||
3784B23D2728B85300B09468 /* ShareButton.swift in Sources */,
|
3784B23D2728B85300B09468 /* ShareButton.swift in Sources */,
|
||||||
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */,
|
3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */,
|
||||||
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
|
37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
||||||
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
||||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
@ -2524,6 +2645,7 @@
|
|||||||
37030FF727B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
|
37030FF727B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
|
||||||
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||||
|
3752069D285E910600CA655F /* ChaptersView.swift in Sources */,
|
||||||
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
||||||
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||||
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||||
@ -2531,8 +2653,11 @@
|
|||||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
378AE940274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
378AE940274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||||
376BE50927347B5F009AD608 /* SettingsHeader.swift in Sources */,
|
376BE50927347B5F009AD608 /* SettingsHeader.swift in Sources */,
|
||||||
|
376527BB285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||||
3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */,
|
3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */,
|
||||||
|
37F4AD2628613B81004D0F66 /* Color+Debug.swift in Sources */,
|
||||||
3700155F271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
3700155F271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
||||||
|
37F13B62285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
||||||
375E45F827B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */,
|
375E45F827B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */,
|
||||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */,
|
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */,
|
||||||
@ -2560,6 +2685,7 @@
|
|||||||
37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */,
|
37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */,
|
||||||
371114EB27B94C8800C2EF7B /* RepeatingTimer.swift in Sources */,
|
371114EB27B94C8800C2EF7B /* RepeatingTimer.swift in Sources */,
|
||||||
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
|
3756C2A62861131100E4B059 /* NetworkState.swift in Sources */,
|
||||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||||
@ -2578,9 +2704,11 @@
|
|||||||
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
||||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||||
|
37520699285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||||
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||||
|
37F4AD1F28612DFD004D0F66 /* Buffering.swift in Sources */,
|
||||||
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
371B7E5C27596B8400D21217 /* Comment.swift in Sources */,
|
371B7E5C27596B8400D21217 /* Comment.swift in Sources */,
|
||||||
3703100227B0713600ECDDAA /* PlayerGestures.swift in Sources */,
|
3703100227B0713600ECDDAA /* PlayerGestures.swift in Sources */,
|
||||||
@ -2616,6 +2744,7 @@
|
|||||||
37599F30272B42810087F250 /* FavoriteItem.swift in Sources */,
|
37599F30272B42810087F250 /* FavoriteItem.swift in Sources */,
|
||||||
373197D92732015300EF734F /* RelatedView.swift in Sources */,
|
373197D92732015300EF734F /* RelatedView.swift in Sources */,
|
||||||
37F9619B27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */,
|
37F9619B27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */,
|
||||||
|
37F4AD1B28612B23004D0F66 /* OpeningStream.swift in Sources */,
|
||||||
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
||||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||||
37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */,
|
37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */,
|
||||||
@ -2631,6 +2760,7 @@
|
|||||||
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||||
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */,
|
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */,
|
||||||
37030FFB27B0398000ECDDAA /* MPVClient.swift in Sources */,
|
37030FFB27B0398000ECDDAA /* MPVClient.swift in Sources */,
|
||||||
|
3756C2AA2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||||
3758638A2721B0A9000CB14E /* ChannelCell.swift in Sources */,
|
3758638A2721B0A9000CB14E /* ChannelCell.swift in Sources */,
|
||||||
37001563271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001563271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
3795593627B08538007FF8F4 /* StreamControl.swift in Sources */,
|
3795593627B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||||
@ -2678,7 +2808,9 @@
|
|||||||
3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */,
|
3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */,
|
||||||
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
|
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
|
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||||
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
|
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
|
||||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||||
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
@ -2690,11 +2822,13 @@
|
|||||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
37FB285F272225E800A57617 /* ContentItemView.swift in Sources */,
|
37FB285F272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||||
37FD43DC270470B70073EE42 /* InstancesSettings.swift in Sources */,
|
37FD43DC270470B70073EE42 /* InstancesSettings.swift in Sources */,
|
||||||
|
3756C2A72861131100E4B059 /* NetworkState.swift in Sources */,
|
||||||
3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */,
|
3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */,
|
||||||
37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||||
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||||
|
3752069A285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||||
37484C1A26FC837400287258 /* PlayerSettings.swift in Sources */,
|
37484C1A26FC837400287258 /* PlayerSettings.swift in Sources */,
|
||||||
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
||||||
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */,
|
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||||
@ -2704,8 +2838,10 @@
|
|||||||
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||||
378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */,
|
378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */,
|
||||||
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
|
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||||
|
376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||||
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||||
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||||
|
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
||||||
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||||
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||||
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
@ -2715,10 +2851,12 @@
|
|||||||
376BE50727347B57009AD608 /* SettingsHeader.swift in Sources */,
|
376BE50727347B57009AD608 /* SettingsHeader.swift in Sources */,
|
||||||
378AE93C274EDFB2006A4EE1 /* Backport.swift in Sources */,
|
378AE93C274EDFB2006A4EE1 /* Backport.swift in Sources */,
|
||||||
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
|
37F4AD2028612DFD004D0F66 /* Buffering.swift in Sources */,
|
||||||
377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */,
|
377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */,
|
||||||
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */,
|
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||||
37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */,
|
37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */,
|
||||||
37BC50AD2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
37BC50AD2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||||
|
3752069E285E910600CA655F /* ChaptersView.swift in Sources */,
|
||||||
37030FF827B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
|
37030FF827B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
|
||||||
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||||
@ -2734,6 +2872,7 @@
|
|||||||
374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */,
|
374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */,
|
||||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
||||||
|
37F4AD1C28612B23004D0F66 /* OpeningStream.swift in Sources */,
|
||||||
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||||
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||||
37579D5E27864F5F00FD0B98 /* Help.swift in Sources */,
|
37579D5E27864F5F00FD0B98 /* Help.swift in Sources */,
|
||||||
@ -2758,6 +2897,7 @@
|
|||||||
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 */,
|
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */,
|
||||||
|
37F13B63285E43C000B137E4 /* ControlsOverlay.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 */,
|
||||||
@ -2772,6 +2912,7 @@
|
|||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
|
37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
||||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
@ -2780,6 +2921,7 @@
|
|||||||
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||||
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */,
|
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */,
|
||||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
|
37F4AD2728613B81004D0F66 /* Color+Debug.swift in Sources */,
|
||||||
37732FF12703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
37732FF12703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
||||||
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
|
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
|
||||||
37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||||
@ -2824,7 +2966,6 @@
|
|||||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||||
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */,
|
|
||||||
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -2842,32 +2983,25 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
3774124C27387D2300423605 /* RecentsModel.swift in Sources */,
|
3774124C27387D2300423605 /* RecentsModel.swift in Sources */,
|
||||||
3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */,
|
|
||||||
371B7E642759706A00D21217 /* CommentsView.swift in Sources */,
|
371B7E642759706A00D21217 /* CommentsView.swift in Sources */,
|
||||||
3774124927387D2300423605 /* Channel.swift in Sources */,
|
3774124927387D2300423605 /* Channel.swift in Sources */,
|
||||||
3774125727387D2300423605 /* FavoriteItem.swift in Sources */,
|
3774125727387D2300423605 /* FavoriteItem.swift in Sources */,
|
||||||
3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */,
|
3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */,
|
||||||
3774126027387D2D00423605 /* AccountsBridge.swift in Sources */,
|
3774126027387D2D00423605 /* AccountsBridge.swift in Sources */,
|
||||||
3774125827387D2300423605 /* TrendingCategory.swift in Sources */,
|
3774125827387D2300423605 /* TrendingCategory.swift in Sources */,
|
||||||
3774125F27387D2D00423605 /* Account.swift in Sources */,
|
|
||||||
3774126827387D6D00423605 /* Double+Format.swift in Sources */,
|
3774126827387D6D00423605 /* Double+Format.swift in Sources */,
|
||||||
3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */,
|
3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */,
|
||||||
3774125627387D2300423605 /* Segment.swift in Sources */,
|
3774125627387D2300423605 /* Segment.swift in Sources */,
|
||||||
373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */,
|
373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||||
3774126427387D4A00423605 /* VideosAPI.swift in Sources */,
|
|
||||||
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
|
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
|
||||||
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */,
|
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */,
|
||||||
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */,
|
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */,
|
||||||
37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */,
|
|
||||||
3774125427387D2300423605 /* Store.swift in Sources */,
|
3774125427387D2300423605 /* Store.swift in Sources */,
|
||||||
3774125027387D2300423605 /* Video.swift in Sources */,
|
3774125027387D2300423605 /* Video.swift in Sources */,
|
||||||
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */,
|
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||||
3774125327387D2300423605 /* Country.swift in Sources */,
|
3774125327387D2300423605 /* Country.swift in Sources */,
|
||||||
3774125E27387D2D00423605 /* InstancesModel.swift in Sources */,
|
|
||||||
37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */,
|
|
||||||
3774127227387E0B00423605 /* SiestaConfiguration.swift in Sources */,
|
3774127227387E0B00423605 /* SiestaConfiguration.swift in Sources */,
|
||||||
3774126D27387D8500423605 /* SponsorBlockAPI.swift in Sources */,
|
3774126D27387D8500423605 /* SponsorBlockAPI.swift in Sources */,
|
||||||
3774126327387D2D00423605 /* InstancesBridge.swift in Sources */,
|
|
||||||
3774125127387D2300423605 /* NavigationModel.swift in Sources */,
|
3774125127387D2300423605 /* NavigationModel.swift in Sources */,
|
||||||
3774124A27387D2300423605 /* ContentItem.swift in Sources */,
|
3774124A27387D2300423605 /* ContentItem.swift in Sources */,
|
||||||
3774126227387D2D00423605 /* AccountValidator.swift in Sources */,
|
3774126227387D2D00423605 /* AccountValidator.swift in Sources */,
|
||||||
@ -2876,14 +3010,12 @@
|
|||||||
3774126A27387D6D00423605 /* TypedContentAccessors.swift in Sources */,
|
3774126A27387D6D00423605 /* TypedContentAccessors.swift in Sources */,
|
||||||
3774127027387D9A00423605 /* SponsorBlockSegment.swift in Sources */,
|
3774127027387D9A00423605 /* SponsorBlockSegment.swift in Sources */,
|
||||||
3774125A27387D2300423605 /* FavoritesModel.swift in Sources */,
|
3774125A27387D2300423605 /* FavoritesModel.swift in Sources */,
|
||||||
3774125D27387D2D00423605 /* Instance.swift in Sources */,
|
|
||||||
3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */,
|
3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */,
|
||||||
3774125527387D2300423605 /* Stream.swift in Sources */,
|
3774125527387D2300423605 /* Stream.swift in Sources */,
|
||||||
371B7E5F27596B8400D21217 /* Comment.swift in Sources */,
|
371B7E5F27596B8400D21217 /* Comment.swift in Sources */,
|
||||||
3774126F27387D8D00423605 /* SearchQuery.swift in Sources */,
|
3774126F27387D8D00423605 /* SearchQuery.swift in Sources */,
|
||||||
3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */,
|
3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */,
|
||||||
3774125227387D2300423605 /* Thumbnail.swift in Sources */,
|
3774125227387D2300423605 /* Thumbnail.swift in Sources */,
|
||||||
3774122F27387C7600423605 /* VideosApp.swift in Sources */,
|
|
||||||
37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */,
|
37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */,
|
||||||
3774126527387D6D00423605 /* Int+Format.swift in Sources */,
|
3774126527387D6D00423605 /* Int+Format.swift in Sources */,
|
||||||
3774126627387D6D00423605 /* Array+Next.swift in Sources */,
|
3774126627387D6D00423605 /* Array+Next.swift in Sources */,
|
||||||
@ -2927,7 +3059,9 @@
|
|||||||
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
||||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||||
37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */,
|
37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */,
|
||||||
|
37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
||||||
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
|
378FFBC628660172009E3FBE /* URLParser.swift in Sources */,
|
||||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||||
3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||||
@ -2937,6 +3071,7 @@
|
|||||||
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||||
378AE93E274EDFB4006A4EE1 /* Tint+Backport.swift in Sources */,
|
378AE93E274EDFB4006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||||
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */,
|
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||||
|
37F4AD2828613B81004D0F66 /* Color+Debug.swift in Sources */,
|
||||||
37E8B0F227B326F30024006F /* Comparable+Clamped.swift in Sources */,
|
37E8B0F227B326F30024006F /* Comparable+Clamped.swift in Sources */,
|
||||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
||||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
@ -2946,14 +3081,18 @@
|
|||||||
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
|
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
|
||||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
|
376527BD285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||||
371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
|
371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
|
||||||
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
||||||
|
3756C2AC2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||||
|
37A5DBCA285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||||
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
||||||
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||||
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
|
3752069B285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||||
37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */,
|
37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
3743B86A27216D3600261544 /* ChannelCell.swift in Sources */,
|
3743B86A27216D3600261544 /* ChannelCell.swift in Sources */,
|
||||||
3751BA8527E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
|
3751BA8527E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
|
||||||
@ -2981,6 +3120,7 @@
|
|||||||
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||||
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */,
|
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
|
3756C2A82861131100E4B059 /* NetworkState.swift in Sources */,
|
||||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||||
37001561271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
37001561271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
||||||
@ -3012,9 +3152,12 @@
|
|||||||
37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
||||||
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */,
|
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */,
|
||||||
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
|
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||||
|
3752069F285E910600CA655F /* ChaptersView.swift in Sources */,
|
||||||
|
37F4AD1D28612B23004D0F66 /* OpeningStream.swift in Sources */,
|
||||||
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
|
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
|
||||||
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
37C3A24B27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
37C3A24B27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||||
|
37F4AD2128612DFD004D0F66 /* Buffering.swift in Sources */,
|
||||||
37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||||
37FAE000272ED58000330459 /* EditFavorites.swift in Sources */,
|
37FAE000272ED58000330459 /* EditFavorites.swift in Sources */,
|
||||||
37F961A127BD90BB00058149 /* PlayerBackendType.swift in Sources */,
|
37F961A127BD90BB00058149 /* PlayerBackendType.swift in Sources */,
|
||||||
@ -3081,6 +3224,48 @@
|
|||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
3712643F2865FF4500D77974 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "net.arekf.Shared-Tests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = auto;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
371264402865FF4500D77974 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "net.arekf.Shared-Tests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = auto;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
37A3B17227255E7F000FB5EE /* Debug */ = {
|
37A3B17227255E7F000FB5EE /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@ -3088,7 +3273,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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@ -3123,7 +3308,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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@ -3156,7 +3341,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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
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";
|
||||||
@ -3188,7 +3373,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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
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";
|
||||||
@ -3356,7 +3541,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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
@ -3366,6 +3551,7 @@
|
|||||||
);
|
);
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "Need camera access to take pictures";
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||||
@ -3401,12 +3587,13 @@
|
|||||||
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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "Need camera access to take pictures";
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||||
@ -3442,11 +3629,12 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements;
|
||||||
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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
@ -3482,11 +3670,12 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements;
|
||||||
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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
@ -3626,7 +3815,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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@ -3665,7 +3854,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 = 50;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@ -3793,6 +3982,15 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
371264412865FF4500D77974 /* Build configuration list for PBXNativeTarget "Shared Tests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
3712643F2865FF4500D77974 /* Debug */,
|
||||||
|
371264402865FF4500D77974 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
37A3B17427255E7F000FB5EE /* Build configuration list for PBXNativeTarget "Open in Yattee (macOS)" */ = {
|
37A3B17427255E7F000FB5EE /* Build configuration list for PBXNativeTarget "Open in Yattee (macOS)" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
@ -3928,6 +4126,14 @@
|
|||||||
minimumVersion = 1.5.0;
|
minimumVersion = 1.5.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/fermoya/SwiftUIPager.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
37B767DE2678C5BF0098BAA8 /* XCRemoteSwiftPackageReference "swift-log" */ = {
|
37B767DE2678C5BF0098BAA8 /* XCRemoteSwiftPackageReference "swift-log" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/apple/swift-log.git";
|
repositoryURL = "https://github.com/apple/swift-log.git";
|
||||||
@ -4095,6 +4301,16 @@
|
|||||||
package = 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */;
|
package = 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */;
|
||||||
productName = Siesta;
|
productName = Siesta;
|
||||||
};
|
};
|
||||||
|
37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;
|
||||||
|
productName = SwiftUIPager;
|
||||||
|
};
|
||||||
|
37A5DBC5285E06B100CA4DD1 /* SwiftUIPager */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;
|
||||||
|
productName = SwiftUIPager;
|
||||||
|
};
|
||||||
37B767DF2678C5BF0098BAA8 /* Logging */ = {
|
37B767DF2678C5BF0098BAA8 /* Logging */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 37B767DE2678C5BF0098BAA8 /* XCRemoteSwiftPackageReference "swift-log" */;
|
package = 37B767DE2678C5BF0098BAA8 /* XCRemoteSwiftPackageReference "swift-log" */;
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
{
|
|
||||||
"pins" : [
|
|
||||||
{
|
|
||||||
"identity" : "alamofire",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
|
|
||||||
"version" : "5.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "defaults",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/sindresorhus/Defaults",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "981ccb0a01c54abbe3c12ccb8226108527bbf115",
|
|
||||||
"version" : "6.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "libwebp-xcode",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/SDWebImage/libwebp-Xcode.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "0f3bdb28a1edc5e8e43876d3835d20c601ef331f",
|
|
||||||
"version" : "1.2.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "pincache",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pinterest/PINCache",
|
|
||||||
"state" : {
|
|
||||||
"branch" : "master",
|
|
||||||
"revision" : "9ca06045b5aff12ee8c0ef5880aa8469c4896144"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "pinoperation",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pinterest/PINOperation.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "44d8ca154a4e75a028a5548c31ff3a53b90cef15",
|
|
||||||
"version" : "1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "sdwebimage",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/SDWebImage/SDWebImage.git",
|
|
||||||
"state" : {
|
|
||||||
"branch" : "master",
|
|
||||||
"revision" : "eeb25d6e9c1ecedbcbdc6694a6e40eaa8dcddbb5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "sdwebimagepinplugin",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/SDWebImage/SDWebImagePINPlugin.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "bd73a4fb30352ec311303d811559c9c46df4caa4",
|
|
||||||
"version" : "0.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "sdwebimageswiftui",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "cd8625b7cf11a97698e180d28bb7d5d357196678",
|
|
||||||
"version" : "2.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "sdwebimagewebpcoder",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/SDWebImage/SDWebImageWebPCoder.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "8a0c5e1ae08ed763739262b9dcef64cfb241c14b",
|
|
||||||
"version" : "0.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "siesta",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/bustoutsolutions/siesta",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "43f34046ebb5beb6802200353c473af303bbc31e",
|
|
||||||
"version" : "1.5.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-log",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-log.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
|
|
||||||
"version" : "1.4.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swiftui-introspect",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
|
|
||||||
"version" : "0.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swiftyjson",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
|
|
||||||
"version" : "5.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 2
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user