mirror of
https://github.com/yattee/yattee.git
synced 2024-12-12 21:30:32 +05:30
Player controls UI changes
WIP on controls Chapters working Add previews variable Add lists ids WIP
This commit is contained in:
parent
9c98cf9558
commit
321c265a11
@ -5,9 +5,19 @@ disabled_rules:
|
||||
- opening_brace
|
||||
- number_separator
|
||||
- multiline_arguments
|
||||
|
||||
opt_in_rules:
|
||||
- conditional_returns_on_newline
|
||||
- implicit_return
|
||||
excluded:
|
||||
- Vendor
|
||||
- Tests Apple TV
|
||||
- Tests iOS
|
||||
- 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
|
||||
|
||||
extension Double {
|
||||
func formattedAsPlaybackTime() -> String? {
|
||||
guard !isZero, isFinite else {
|
||||
func formattedAsPlaybackTime(allowZero: Bool = false) -> String? {
|
||||
guard allowZero || !isZero, isFinite else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import UIKit
|
||||
|
||||
extension UIViewController {
|
||||
@objc var swizzle_prefersHomeIndicatorAutoHidden: Bool {
|
||||
return true
|
||||
true
|
||||
}
|
||||
|
||||
public class func swizzleHomeIndicatorProperty() {
|
||||
|
@ -6,6 +6,7 @@ extension Video {
|
||||
|
||||
static var fixture: Video {
|
||||
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(
|
||||
videoID: fixtureID,
|
||||
@ -29,7 +30,12 @@ extension Video {
|
||||
publishedAt: Date(),
|
||||
likes: 37333,
|
||||
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(invidious)
|
||||
.environmentObject(NavigationModel())
|
||||
.environmentObject(NetworkStateModel())
|
||||
.environmentObject(PipedAPI())
|
||||
.environmentObject(player)
|
||||
.environmentObject(PlayerControlsModel())
|
||||
.environmentObject(playerControls)
|
||||
.environmentObject(PlayerTimeModel())
|
||||
.environmentObject(PlaylistsModel())
|
||||
.environmentObject(RecentsModel())
|
||||
.environmentObject(SearchModel())
|
||||
@ -37,6 +39,10 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
||||
return player
|
||||
}
|
||||
|
||||
private var playerControls: PlayerControlsModel {
|
||||
PlayerControlsModel(presentingControls: true, player: player)
|
||||
}
|
||||
|
||||
private var subscriptions: SubscriptionsModel {
|
||||
let subscriptions = SubscriptionsModel()
|
||||
|
||||
|
@ -383,6 +383,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
id = videoID
|
||||
}
|
||||
|
||||
let description = json["description"].stringValue
|
||||
|
||||
return Video(
|
||||
id: id,
|
||||
videoID: videoID,
|
||||
@ -391,7 +393,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
length: json["lengthSeconds"].doubleValue,
|
||||
published: json["publishedText"].stringValue,
|
||||
views: json["viewCount"].intValue,
|
||||
description: json["description"].stringValue,
|
||||
description: description,
|
||||
genre: json["genre"].stringValue,
|
||||
channel: extractChannel(from: json),
|
||||
thumbnails: extractThumbnails(from: json),
|
||||
@ -403,7 +405,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
dislikes: json["dislikeCount"].int,
|
||||
keywords: json["keywords"].arrayValue.compactMap { $0.string },
|
||||
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 description = extractDescription(from: content) ?? ""
|
||||
|
||||
var chapters = extractChapters(from: content)
|
||||
if chapters.isEmpty, !description.isEmpty {
|
||||
chapters = extractChapters(from: description)
|
||||
}
|
||||
|
||||
return Video(
|
||||
videoID: extractID(from: content),
|
||||
title: details["title"]?.string ?? "",
|
||||
@ -416,14 +423,15 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
length: details["duration"]?.double ?? 0,
|
||||
published: published ?? "",
|
||||
views: details["views"]?.int ?? 0,
|
||||
description: extractDescription(from: content),
|
||||
description: description,
|
||||
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
|
||||
thumbnails: thumbnails,
|
||||
live: live,
|
||||
likes: details["likes"]?.int,
|
||||
dislikes: details["dislikes"]?.int,
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
guard Self.enabled else {
|
||||
guard Self.enabled, !loaded else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !Self.instance.isNil,
|
||||
!(player?.currentVideo.isNil ?? true)
|
||||
let video = player.currentVideo
|
||||
else {
|
||||
return
|
||||
}
|
||||
@ -56,7 +56,7 @@ final class CommentsModel: ObservableObject {
|
||||
|
||||
firstPage = page.isNil || page!.isEmpty
|
||||
|
||||
api?.comments(player.currentVideo!.videoID, page: page)?
|
||||
api?.comments(video.videoID, page: page)?
|
||||
.load()
|
||||
.onSuccess { [weak self] response in
|
||||
if let page: CommentsPage = response.typedContent() {
|
||||
|
@ -66,6 +66,10 @@ final class NavigationModel: ObservableObject {
|
||||
@Published var presentingSettings = false
|
||||
@Published var presentingWelcomeScreen = false
|
||||
|
||||
@Published var presentingAlert = false
|
||||
@Published var alertTitle = ""
|
||||
@Published var alertMessage = ""
|
||||
|
||||
static func openChannel(
|
||||
_ channel: Channel,
|
||||
player: PlayerModel,
|
||||
@ -181,6 +185,12 @@ final class NavigationModel: ObservableObject {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
func presentAlert(title: String, message: String) {
|
||||
alertTitle = title
|
||||
alertMessage = message
|
||||
presentingAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
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 controls: PlayerControlsModel!
|
||||
var playerTime: PlayerTimeModel!
|
||||
var networkState: NetworkStateModel!
|
||||
|
||||
var stream: Stream?
|
||||
var video: Video?
|
||||
@ -31,6 +33,11 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
avPlayer.timeControlStatus == .playing
|
||||
}
|
||||
|
||||
var isSeeking: Bool {
|
||||
// TODO: implement this maybe?
|
||||
false
|
||||
}
|
||||
|
||||
var playerItemDuration: CMTime? {
|
||||
avPlayer.currentItem?.asset.duration
|
||||
}
|
||||
@ -52,9 +59,10 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
private var timeObserverThrottle = Throttle(interval: 2)
|
||||
|
||||
init(model: PlayerModel, controls: PlayerControlsModel?) {
|
||||
init(model: PlayerModel, controls: PlayerControlsModel?, playerTime: PlayerTimeModel?) {
|
||||
self.model = model
|
||||
self.controls = controls
|
||||
self.playerTime = playerTime
|
||||
|
||||
addFrequentTimeObserver()
|
||||
addInfrequentTimeObserver()
|
||||
@ -493,8 +501,8 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
return
|
||||
}
|
||||
|
||||
self.controls.duration = self.playerItemDuration ?? .zero
|
||||
self.controls.currentTime = self.currentTime ?? .zero
|
||||
self.playerTime.duration = self.playerItemDuration ?? .zero
|
||||
self.playerTime.currentTime = self.currentTime ?? .zero
|
||||
|
||||
#if !os(tvOS)
|
||||
self.model.updateNowPlayingInfo()
|
||||
@ -581,4 +589,5 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
func stopControlsUpdates() {}
|
||||
func setNeedsDrawing(_: Bool) {}
|
||||
func setSize(_: Double, _: Double) {}
|
||||
func updateNetworkState() {}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
var model: PlayerModel!
|
||||
var controls: PlayerControlsModel!
|
||||
var playerTime: PlayerTimeModel!
|
||||
var networkState: NetworkStateModel!
|
||||
|
||||
var stream: Stream?
|
||||
var video: Video?
|
||||
@ -24,17 +26,22 @@ final class MPVBackend: PlayerBackend {
|
||||
return
|
||||
}
|
||||
|
||||
self.controls.isLoadingVideo = self.isLoadingVideo
|
||||
self.controls?.isLoadingVideo = self.isLoadingVideo
|
||||
self.updateNetworkState()
|
||||
|
||||
if !self.isLoadingVideo {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
||||
self?.handleEOF = true
|
||||
}
|
||||
}
|
||||
|
||||
self.model?.objectWillChange.send()
|
||||
}
|
||||
}}
|
||||
|
||||
var isPlaying = true { didSet {
|
||||
updateNetworkState()
|
||||
|
||||
if isPlaying {
|
||||
startClientUpdates()
|
||||
} else {
|
||||
@ -49,6 +56,15 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
#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?
|
||||
|
||||
#if !os(macOS)
|
||||
@ -88,9 +104,16 @@ final class MPVBackend: PlayerBackend {
|
||||
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.controls = controls
|
||||
self.playerTime = playerTime
|
||||
self.networkState = networkState
|
||||
|
||||
clientTimer = .init(timeInterval: Self.controlsUpdateInterval)
|
||||
clientTimer.eventHandler = getClientUpdates
|
||||
@ -155,7 +178,6 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
if !preservingTime,
|
||||
let segment = self.model.sponsorBlock.segments.first,
|
||||
segment.end > 4,
|
||||
self.model.lastSkipped.isNil
|
||||
{
|
||||
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 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?.pause()
|
||||
}
|
||||
@ -229,7 +251,7 @@ final class MPVBackend: PlayerBackend {
|
||||
isPlaying = true
|
||||
startClientUpdates()
|
||||
|
||||
if controls.presentingControls {
|
||||
if controls?.presentingControls ?? false {
|
||||
startControlsUpdates()
|
||||
}
|
||||
|
||||
@ -254,7 +276,7 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
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?.updateControls()
|
||||
completionHandler?(true)
|
||||
@ -262,7 +284,7 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
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?.updateControls()
|
||||
completionHandler?(true)
|
||||
@ -280,13 +302,7 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func enterFullScreen() {
|
||||
model.toggleFullscreen(controls?.playingFullscreen ?? false)
|
||||
|
||||
#if os(iOS)
|
||||
if Defaults[.lockOrientationInFullScreen] {
|
||||
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
|
||||
}
|
||||
#endif
|
||||
model.toggleFullscreen(model?.playingFullScreen ?? false)
|
||||
}
|
||||
|
||||
func exitFullScreen() {}
|
||||
@ -297,15 +313,13 @@ final class MPVBackend: PlayerBackend {
|
||||
guard model.presentingPlayer else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.logger.info("updating controls")
|
||||
self.controls.currentTime = self.currentTime ?? .zero
|
||||
self.controls.duration = self.playerItemDuration ?? .zero
|
||||
self.playerTime.currentTime = self.currentTime ?? .zero
|
||||
self.playerTime.duration = self.playerItemDuration ?? .zero
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,13 +389,22 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
case MPV_EVENT_PLAYBACK_RESTART:
|
||||
isLoadingVideo = false
|
||||
isSeeking = false
|
||||
|
||||
onFileLoaded?()
|
||||
startClientUpdates()
|
||||
onFileLoaded = nil
|
||||
|
||||
case MPV_EVENT_PAUSE:
|
||||
updateNetworkState()
|
||||
|
||||
case MPV_EVENT_UNPAUSE:
|
||||
isLoadingVideo = false
|
||||
isSeeking = false
|
||||
updateNetworkState()
|
||||
|
||||
case MPV_EVENT_SEEK:
|
||||
isSeeking = true
|
||||
|
||||
case MPV_EVENT_END_FILE:
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
@ -417,18 +440,41 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
func setSize(_ width: Double, _ height: Double) {
|
||||
self.client?.setSize(width, height)
|
||||
client?.setSize(width, height)
|
||||
}
|
||||
|
||||
func addVideoTrack(_ url: URL) {
|
||||
self.client?.addVideoTrack(url)
|
||||
client?.addVideoTrack(url)
|
||||
}
|
||||
|
||||
func setVideoToAuto() {
|
||||
self.client?.setVideoToAuto()
|
||||
client?.setVideoToAuto()
|
||||
}
|
||||
|
||||
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"))
|
||||
#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, "vo", "libmpv"))
|
||||
|
||||
@ -167,6 +170,10 @@ final class MPVClient: ObservableObject {
|
||||
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) {
|
||||
guard !seeking else {
|
||||
logger.warning("ignoring seek, another in progress")
|
||||
@ -262,6 +269,12 @@ final class MPVClient: ObservableObject {
|
||||
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) {
|
||||
var data: Int = flag ? 1 : 0
|
||||
mpv_set_property_async(mpv, 0, name, MPV_FORMAT_FLAG, &data)
|
||||
|
@ -5,6 +5,8 @@ import Foundation
|
||||
protocol PlayerBackend {
|
||||
var model: PlayerModel! { get set }
|
||||
var controls: PlayerControlsModel! { get set }
|
||||
var playerTime: PlayerTimeModel! { get set }
|
||||
var networkState: NetworkStateModel! { get set }
|
||||
|
||||
var stream: Stream? { get set }
|
||||
var video: Video? { get set }
|
||||
@ -14,6 +16,7 @@ protocol PlayerBackend {
|
||||
var isLoadingVideo: Bool { get }
|
||||
|
||||
var isPlaying: Bool { get }
|
||||
var isSeeking: Bool { get }
|
||||
var playerItemDuration: CMTime? { get }
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
|
||||
@ -49,6 +52,8 @@ protocol PlayerBackend {
|
||||
func startControlsUpdates()
|
||||
func stopControlsUpdates()
|
||||
|
||||
func updateNetworkState()
|
||||
|
||||
func setNeedsDrawing(_ needsDrawing: Bool)
|
||||
func setSize(_ width: Double, _ height: Double)
|
||||
}
|
||||
|
@ -13,4 +13,8 @@ enum PlayerBackendType: String, CaseIterable, Defaults.Serializable {
|
||||
return "AVPlayer"
|
||||
}
|
||||
}
|
||||
|
||||
var supportsNetworkStateBufferingDetails: Bool {
|
||||
self == .mpv
|
||||
}
|
||||
}
|
||||
|
@ -5,37 +5,26 @@ import SwiftUI
|
||||
final class PlayerControlsModel: ObservableObject {
|
||||
@Published var isLoadingVideo = false
|
||||
@Published var isPlaying = true
|
||||
@Published var currentTime = CMTime.zero
|
||||
@Published var duration = CMTime.zero
|
||||
@Published var presentingControls = false { didSet { handlePresentationChange() } }
|
||||
@Published var presentingControlsOverlay = false
|
||||
@Published var timer: Timer?
|
||||
@Published var playingFullscreen = false
|
||||
|
||||
var player: PlayerModel!
|
||||
|
||||
var playbackTime: String {
|
||||
guard let current = currentTime.seconds.formattedAsPlaybackTime(),
|
||||
let duration = duration.seconds.formattedAsPlaybackTime()
|
||||
else {
|
||||
return "--:-- / --:--"
|
||||
}
|
||||
|
||||
var withoutSegments = ""
|
||||
if let withoutSegmentsDuration = playerItemDurationWithoutSponsorSegments,
|
||||
self.duration.seconds != withoutSegmentsDuration
|
||||
{
|
||||
withoutSegments = " (\(withoutSegmentsDuration.formattedAsPlaybackTime() ?? "--:--"))"
|
||||
}
|
||||
|
||||
return "\(current) / \(duration)\(withoutSegments)"
|
||||
}
|
||||
|
||||
var playerItemDurationWithoutSponsorSegments: Double? {
|
||||
guard let duration = player.playerItemDurationWithoutSponsorSegments else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return duration.seconds
|
||||
init(
|
||||
isLoadingVideo: Bool = false,
|
||||
isPlaying: Bool = true,
|
||||
presentingControls: Bool = false,
|
||||
presentingControlsOverlay: Bool = false,
|
||||
timer: Timer? = nil,
|
||||
player: PlayerModel? = nil
|
||||
) {
|
||||
self.isLoadingVideo = isLoadingVideo
|
||||
self.isPlaying = isPlaying
|
||||
self.presentingControls = presentingControls
|
||||
self.presentingControlsOverlay = presentingControlsOverlay
|
||||
self.timer = timer
|
||||
self.player = player
|
||||
}
|
||||
|
||||
func handlePresentationChange() {
|
||||
@ -45,7 +34,7 @@ final class PlayerControlsModel: ObservableObject {
|
||||
self?.resetTimer()
|
||||
}
|
||||
} else {
|
||||
player.backend.stopControlsUpdates()
|
||||
player?.backend.stopControlsUpdates()
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
@ -91,11 +80,6 @@ final class PlayerControlsModel: ObservableObject {
|
||||
presentingControls ? hide() : show()
|
||||
}
|
||||
|
||||
func reset() {
|
||||
currentTime = .zero
|
||||
duration = .zero
|
||||
}
|
||||
|
||||
func resetTimer() {
|
||||
#if os(tvOS)
|
||||
if !presentingControls {
|
||||
|
@ -53,6 +53,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
@Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } }
|
||||
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
|
||||
@Published var videoBeingOpened: Video?
|
||||
@Published var historyVideos = [Video]()
|
||||
|
||||
@Published var preservedTime: CMTime?
|
||||
@ -65,6 +66,10 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var musicMode = false
|
||||
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
||||
|
||||
@Published var isSeeking = false { didSet {
|
||||
backend.updateNetworkState()
|
||||
}}
|
||||
|
||||
#if os(iOS)
|
||||
@Published var motionManager: CMMotionManager!
|
||||
@Published var lockedOrientation: UIInterfaceOrientation?
|
||||
@ -79,9 +84,24 @@ final class PlayerModel: ObservableObject {
|
||||
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 backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
||||
|
||||
@Published var playingFullScreen = false
|
||||
@Published var playingInPictureInPicture = false
|
||||
var pipController: AVPictureInPictureController?
|
||||
var pipDelegate = PiPDelegate()
|
||||
@ -108,13 +128,31 @@ final class PlayerModel: ObservableObject {
|
||||
var playerLayerView: PlayerLayerView!
|
||||
#endif
|
||||
|
||||
init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil, controls: PlayerControlsModel? = nil) {
|
||||
self.accounts = accounts ?? AccountsModel()
|
||||
self.comments = comments ?? CommentsModel()
|
||||
self.controls = controls ?? PlayerControlsModel()
|
||||
var onPresentPlayer: (() -> Void)?
|
||||
|
||||
self.avPlayerBackend = AVPlayerBackend(model: self, controls: controls)
|
||||
self.mpvBackend = MPVBackend(model: self)
|
||||
init(
|
||||
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
|
||||
}
|
||||
@ -136,7 +174,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func hide() {
|
||||
controls.playingFullscreen = false
|
||||
playingFullScreen = false
|
||||
presentingPlayer = false
|
||||
|
||||
#if os(iOS)
|
||||
@ -176,11 +214,19 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
var playerItemDuration: CMTime? {
|
||||
backend.playerItemDuration
|
||||
guard !currentItem.isNil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return backend.playerItemDuration
|
||||
}
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
@ -212,18 +258,15 @@ final class PlayerModel: ObservableObject {
|
||||
func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) {
|
||||
pause()
|
||||
|
||||
var delay = 0.0
|
||||
#if os(iOS)
|
||||
delay = 0.5
|
||||
#endif
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
guard let self = self else {
|
||||
if !playingInPictureInPicture, showingPlayer {
|
||||
onPresentPlayer = { [weak self] in self?.playNow(video, at: time) }
|
||||
show()
|
||||
return
|
||||
}
|
||||
#endif
|
||||
|
||||
self.playNow(video, at: time)
|
||||
}
|
||||
playNow(video, at: time)
|
||||
|
||||
guard !playingInPictureInPicture else {
|
||||
return
|
||||
@ -260,7 +303,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
controls.reset()
|
||||
playerTime.reset()
|
||||
|
||||
backend.playStream(
|
||||
stream,
|
||||
@ -468,10 +511,13 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
func handleEnterBackground() {
|
||||
setNeedsDrawing(false)
|
||||
if !playingInPictureInPicture, !musicMode {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
|
||||
func enterFullScreen() {
|
||||
guard !controls.playingFullscreen else {
|
||||
guard !playingFullScreen else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -481,13 +527,13 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func exitFullScreen() {
|
||||
guard controls.playingFullscreen else {
|
||||
guard playingFullScreen else {
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("exiting fullscreen")
|
||||
|
||||
if controls.playingFullscreen {
|
||||
if playingFullScreen {
|
||||
toggleFullscreen(true)
|
||||
}
|
||||
|
||||
@ -559,14 +605,14 @@ final class PlayerModel: ObservableObject {
|
||||
setNeedsDrawing(false)
|
||||
#endif
|
||||
|
||||
controls.playingFullscreen = !isFullScreen
|
||||
playingFullScreen = !isFullScreen
|
||||
|
||||
#if os(iOS)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||
self?.setNeedsDrawing(true)
|
||||
}
|
||||
|
||||
if controls.playingFullscreen {
|
||||
if playingFullScreen {
|
||||
guard !(UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true) else {
|
||||
return
|
||||
}
|
||||
@ -590,12 +636,6 @@ final class PlayerModel: ObservableObject {
|
||||
avPlayerBackend.switchToMPVOnPipClose = false
|
||||
closePiP()
|
||||
}
|
||||
#if os(macOS)
|
||||
// TODO: initialize mpv on startup on mac
|
||||
if mpvBackend.client.isNil {
|
||||
Windows.player.open()
|
||||
}
|
||||
#endif
|
||||
changeActiveBackend(from: .appleAVPlayer, to: .mpv)
|
||||
controls.presentingControls = true
|
||||
controls.removeTimer()
|
||||
|
@ -20,22 +20,14 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
videosToPlay.dropFirst().reversed().forEach { video in
|
||||
enqueueVideo(video, prepending: true) { _, item in
|
||||
if item.video == first {
|
||||
self.advanceToItem(item)
|
||||
}
|
||||
}
|
||||
enqueueVideo(video, prepending: true, loadDetails: false)
|
||||
}
|
||||
|
||||
show()
|
||||
}
|
||||
|
||||
func playNext(_ video: Video) {
|
||||
enqueueVideo(video, prepending: true) { _, item in
|
||||
if self.currentItem.isNil {
|
||||
self.advanceToItem(item)
|
||||
}
|
||||
}
|
||||
enqueueVideo(video, play: currentItem.isNil, prepending: true)
|
||||
}
|
||||
|
||||
func playNow(_ video: Video, at time: CMTime? = nil) {
|
||||
@ -45,12 +37,12 @@ extension PlayerModel {
|
||||
|
||||
prepareCurrentItemForHistory()
|
||||
|
||||
enqueueVideo(video, prepending: true) { _, item in
|
||||
enqueueVideo(video, play: true, atTime: time, prepending: true) { _, item in
|
||||
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 {
|
||||
backend.closeItem()
|
||||
}
|
||||
@ -65,32 +57,25 @@ extension PlayerModel {
|
||||
currentItem.playbackTime = .zero
|
||||
}
|
||||
|
||||
if video != nil {
|
||||
currentItem.video = video!
|
||||
}
|
||||
|
||||
preservedTime = currentItem.playbackTime
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let video = self?.currentVideo else {
|
||||
return
|
||||
}
|
||||
self?.videoBeingOpened = nil
|
||||
|
||||
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? {
|
||||
let quality = 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)
|
||||
backend.bestPlayable(streams.filter { backend.canPlay($0) }, maxResolution: Defaults[.quality])
|
||||
}
|
||||
|
||||
func advanceToNextItem() {
|
||||
@ -109,7 +94,7 @@ extension PlayerModel {
|
||||
currentItem = newItem
|
||||
|
||||
accounts.api.loadDetails(newItem) { newItem in
|
||||
self.playItem(newItem, video: newItem.video, at: time)
|
||||
self.playItem(newItem, at: time)
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,24 +125,30 @@ extension PlayerModel {
|
||||
play: Bool = false,
|
||||
atTime: CMTime? = nil,
|
||||
prepending: Bool = false,
|
||||
loadDetails: Bool = true,
|
||||
videoDetailsLoadHandler: @escaping (Video, PlayerQueueItem) -> Void = { _, _ in }
|
||||
) -> PlayerQueueItem? {
|
||||
let item = PlayerQueueItem(video, playbackTime: atTime)
|
||||
|
||||
if play {
|
||||
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)
|
||||
|
||||
accounts.api.loadDetails(item) { newItem in
|
||||
if loadDetails {
|
||||
accounts.api.loadDetails(item) { [weak self] newItem in
|
||||
guard let self = self else { return }
|
||||
videoDetailsLoadHandler(newItem.video, newItem)
|
||||
|
||||
if play {
|
||||
self.playItem(newItem, video: video)
|
||||
self.playItem(newItem)
|
||||
} else {
|
||||
self.queue.insert(newItem, at: prepending ? 0 : self.queue.endIndex)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
@ -2,14 +2,16 @@ import AVFAudio
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension PlayerModel {
|
||||
func handleSegments(at time: CMTime) {
|
||||
if let segment = lastSkipped {
|
||||
if time > .secondsInDefaultTimescale(segment.end + 10) {
|
||||
if time > .secondsInDefaultTimescale(segment.end + 5) {
|
||||
resetLastSegment()
|
||||
}
|
||||
}
|
||||
|
||||
guard let firstSegment = sponsorBlock.segments.first(where: { $0.timeInSegment(time) }) else {
|
||||
return
|
||||
}
|
||||
@ -60,7 +62,9 @@ extension PlayerModel {
|
||||
backend.seek(to: segment.endTime)
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
withAnimation {
|
||||
self?.lastSkipped = segment
|
||||
}
|
||||
self?.segmentRestorationTime = time
|
||||
}
|
||||
logger.info("SponsorBlock skipping to: \(segment.end)")
|
||||
@ -69,8 +73,7 @@ extension PlayerModel {
|
||||
private func shouldSkip(_ segment: Segment, at time: CMTime) -> Bool {
|
||||
guard isPlaying,
|
||||
!restoredSegments.contains(segment),
|
||||
Defaults[.sponsorBlockCategories].contains(segment.category),
|
||||
segment.end > 4
|
||||
Defaults[.sponsorBlockCategories].contains(segment.category)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@ -92,7 +95,9 @@ extension PlayerModel {
|
||||
|
||||
private func resetLastSegment() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
withAnimation {
|
||||
self?.lastSkipped = nil
|
||||
}
|
||||
self?.segmentRestorationTime = nil
|
||||
}
|
||||
}
|
||||
|
@ -17,20 +17,14 @@ extension PlayerModel {
|
||||
|
||||
func loadAvailableStreams(_ video: Video) {
|
||||
availableStreams = []
|
||||
let playerInstance = InstancesModel.forPlayer ?? InstancesModel.all.first
|
||||
|
||||
guard !playerInstance.isNil else {
|
||||
guard let playerInstance = InstancesModel.forPlayer ?? InstancesModel.all.first else {
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
fetchStreams(playerInstance.anonymous.video(video.videoID), instance: playerInstance, video: video)
|
||||
}
|
||||
|
||||
private func fetchStreams(
|
||||
@ -60,8 +54,12 @@ extension PlayerModel {
|
||||
stream.instance = instance
|
||||
|
||||
if instance.app == .invidious {
|
||||
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: stream.audioAsset)
|
||||
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: stream.videoAsset)
|
||||
if let audio = stream.audioAsset {
|
||||
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio)
|
||||
}
|
||||
if let video = stream.videoAsset {
|
||||
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video)
|
||||
}
|
||||
}
|
||||
|
||||
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 Siesta
|
||||
import SwiftUI
|
||||
@ -16,6 +17,10 @@ final class PlaylistsModel: ObservableObject {
|
||||
playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }
|
||||
}
|
||||
|
||||
var lastUsed: Playlist? {
|
||||
find(id: Defaults[.lastUsedPlaylistID])
|
||||
}
|
||||
|
||||
func find(id: Playlist.ID?) -> Playlist? {
|
||||
if id.isNil {
|
||||
return nil
|
||||
@ -57,9 +62,19 @@ final class PlaylistsModel: ObservableObject {
|
||||
playlistID: Playlist.ID,
|
||||
videoID: Video.ID,
|
||||
onSuccess: @escaping () -> Void = {},
|
||||
onFailure: @escaping (RequestError) -> Void = { _ in }
|
||||
navigation: NavigationModel?,
|
||||
onFailure: ((RequestError) -> Void)? = nil
|
||||
) {
|
||||
accounts.api.addVideoToPlaylist(
|
||||
videoID,
|
||||
playlistID,
|
||||
onFailure: onFailure ?? { requestError in
|
||||
navigation?.presentAlert(
|
||||
title: "Error when adding to playlist",
|
||||
message: "(\(requestError.httpStatusCode ?? -1)) \(requestError.userMessage)"
|
||||
)
|
||||
}
|
||||
) {
|
||||
accounts.api.addVideoToPlaylist(videoID, playlistID, onFailure: onFailure) {
|
||||
self.load(force: true) {
|
||||
self.reloadPlaylists.toggle()
|
||||
onSuccess()
|
||||
|
@ -21,6 +21,14 @@ class Segment: ObservableObject, Hashable {
|
||||
end - start
|
||||
}
|
||||
|
||||
var durationText: String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.minimumFractionDigits = 0
|
||||
formatter.maximumFractionDigits = 1
|
||||
|
||||
return formatter.string(from: NSNumber(value: duration)) ?? ""
|
||||
}
|
||||
|
||||
var endTime: CMTime {
|
||||
.secondsInDefaultTimescale(end)
|
||||
}
|
||||
|
@ -91,13 +91,13 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
|
||||
private var sortOrder: Int {
|
||||
switch self {
|
||||
case .webm:
|
||||
return 0
|
||||
case .mp4:
|
||||
return 1
|
||||
return 0
|
||||
case .avc1:
|
||||
return 2
|
||||
return 1
|
||||
case .av1:
|
||||
return 2
|
||||
case .webm:
|
||||
return 3
|
||||
case .unknown:
|
||||
return 4
|
||||
@ -160,17 +160,11 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
var quality: String {
|
||||
if resolution == .hd2160p30 {
|
||||
return "4K (2160p)"
|
||||
}
|
||||
|
||||
return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
|
||||
kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
|
||||
}
|
||||
|
||||
var shortQuality: String {
|
||||
if resolution?.height == 2160 {
|
||||
return "4K"
|
||||
} else if kind == .hls {
|
||||
if kind == .hls {
|
||||
return "HLS"
|
||||
} else {
|
||||
return resolution?.name ?? "?"
|
||||
|
@ -16,7 +16,7 @@ final class ThumbnailsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func best(_ video: Video) -> URL? {
|
||||
let qualities = [Thumbnail.Quality.maxresdefault, .medium, .default]
|
||||
let qualities = [Thumbnail.Quality.default]
|
||||
|
||||
for quality in qualities {
|
||||
let url = video.thumbnailURL(quality: quality)
|
||||
|
@ -32,6 +32,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
var channel: Channel
|
||||
|
||||
var related = [Video]()
|
||||
var chapters = [Chapter]()
|
||||
|
||||
init(
|
||||
id: String? = nil,
|
||||
@ -53,7 +54,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
dislikes: Int? = nil,
|
||||
keywords: [String] = [],
|
||||
streams: [Stream] = [],
|
||||
related: [Video] = []
|
||||
related: [Video] = [],
|
||||
chapters: [Chapter] = []
|
||||
) {
|
||||
self.id = id ?? UUID().uuidString
|
||||
self.videoID = videoID
|
||||
@ -75,6 +77,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
self.keywords = keywords
|
||||
self.streams = streams
|
||||
self.related = related
|
||||
self.chapters = chapters
|
||||
}
|
||||
|
||||
var publishedDate: String? {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
@ -11,6 +12,12 @@ extension Defaults.Keys {
|
||||
static let defaultForPauseOnHidingPlayer = false
|
||||
#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 instances = Key<[Instance]>("instances", default: [
|
||||
.init(
|
||||
@ -89,6 +96,10 @@ extension Defaults.Keys {
|
||||
#endif
|
||||
|
||||
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 {
|
||||
@ -200,3 +211,11 @@ enum WatchedVideoPlayNowBehavior: String, Defaults.Serializable {
|
||||
case info, separate
|
||||
}
|
||||
#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 {
|
||||
static var previews: some View {
|
||||
TabView {
|
||||
FavoritesView()
|
||||
.overlay(VideoPlayerView().injectFixtureEnvironmentObjects())
|
||||
.injectFixtureEnvironmentObjects()
|
||||
.tabItem {
|
||||
Label("a", systemImage: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ struct AppSidebarNavigation: View {
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
@ -50,7 +49,6 @@ struct AppSidebarNavigation: View {
|
||||
.frame(minWidth: sidebarMinWidth)
|
||||
|
||||
VStack {
|
||||
BrowserPlayerControls {
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
Image(systemName: "4k.tv")
|
||||
@ -61,7 +59,6 @@ struct AppSidebarNavigation: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(\.navigationStyle, .sidebar)
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ struct AppTabNavigation: View {
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
@ -130,20 +129,6 @@ struct AppTabNavigation: View {
|
||||
.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 {
|
||||
#if os(iOS)
|
||||
Group {
|
||||
|
@ -12,8 +12,10 @@ struct ContentView: View {
|
||||
@EnvironmentObject<CommentsModel> private var comments
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<NetworkStateModel> private var networkState
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||
@EnvironmentObject<PlayerTimeModel> private var playerTime
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
@ -42,7 +44,6 @@ struct ContentView: View {
|
||||
TVNavigationView()
|
||||
#endif
|
||||
}
|
||||
.onAppear(perform: configure)
|
||||
.onChange(of: accounts.signedIn) { _ in
|
||||
subscriptions.load(force: true)
|
||||
playlists.load(force: true)
|
||||
@ -52,7 +53,9 @@ struct ContentView: View {
|
||||
.environmentObject(comments)
|
||||
.environmentObject(instances)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(networkState)
|
||||
.environmentObject(player)
|
||||
.environmentObject(playerTime)
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(recents)
|
||||
.environmentObject(search)
|
||||
@ -107,117 +110,8 @@ struct ContentView: View {
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.alert(isPresented: $navigation.presentingAlert) {
|
||||
Alert(title: Text(navigation.alertTitle), message: Text(navigation.alertMessage))
|
||||
}
|
||||
}
|
||||
|
||||
|
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")
|
||||
} else if !comments.loaded {
|
||||
PlaceholderProgressView()
|
||||
.onAppear {
|
||||
comments.load()
|
||||
}
|
||||
} else {
|
||||
let last = comments.all.last
|
||||
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?
|
||||
#endif
|
||||
|
||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||
@Default(.controlsBarInPlayer) private var controlsBarInPlayer
|
||||
|
||||
init(player: PlayerModel, thumbnails: ThumbnailsModel) {
|
||||
self.player = player
|
||||
@ -31,53 +31,56 @@ struct PlayerControls: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
VStack {
|
||||
ZStack(alignment: .bottom) {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
#if !os(tvOS)
|
||||
ZStack(alignment: .center) {
|
||||
OpeningStream()
|
||||
NetworkState()
|
||||
|
||||
Group {
|
||||
VStack(spacing: 4) {
|
||||
buttonsBar
|
||||
|
||||
HStack(spacing: 4) {
|
||||
qualityButton
|
||||
backendButton
|
||||
if let video = player.currentVideo, player.playingFullScreen {
|
||||
// if let video = Video.fixture {
|
||||
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)
|
||||
}
|
||||
#else
|
||||
Text(player.stream?.description ?? "")
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
|
||||
mediumButtonsBar
|
||||
|
||||
Spacer()
|
||||
|
||||
Group {
|
||||
if player.activeBackend == .mpv, showMPVPlaybackStats {
|
||||
mpvPlaybackStats
|
||||
}
|
||||
ZStack(alignment: .bottom) {
|
||||
floatingControls
|
||||
.padding(.top, 20)
|
||||
.padding(4)
|
||||
.modifier(ControlBackgroundModifier())
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
|
||||
timeline
|
||||
.offset(y: 10)
|
||||
.padding(4)
|
||||
.offset(y: -25)
|
||||
.zIndex(1)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
bottomBar
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBlur(material: .hudWindow))
|
||||
#elseif os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||
#endif
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
}
|
||||
.frame(maxWidth: 500)
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 2)
|
||||
.padding(.horizontal, 2)
|
||||
}
|
||||
.opacity(model.presentingControlsOverlay ? 1 : model.presentingControls ? 1 : 0)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.padding(.horizontal, 4)
|
||||
.opacity(model.presentingControls ? 1 : 0)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.onChange(of: model.presentingControls) { _ in
|
||||
@ -92,13 +95,43 @@ struct PlayerControls: View {
|
||||
.background(PlayerGestures())
|
||||
.background(controlsBackground)
|
||||
#endif
|
||||
.environment(\.colorScheme, .dark)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var controlsBackground: some View {
|
||||
if player.musicMode,
|
||||
let item = self.player.currentItem,
|
||||
let url = thumbnails.best(item.video)
|
||||
let video = item.video,
|
||||
let url = thumbnails.best(video)
|
||||
{
|
||||
WebImage(url: url)
|
||||
.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 {
|
||||
TimelineView(duration: durationBinding, current: currentTimeBinding, cornerRadius: 0)
|
||||
}
|
||||
|
||||
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) }
|
||||
)
|
||||
TimelineView(context: .player).foregroundColor(.primary)
|
||||
}
|
||||
|
||||
private var hidePlayerButton: some View {
|
||||
@ -195,20 +188,20 @@ struct PlayerControls: View {
|
||||
}
|
||||
|
||||
var buttonsBar: some View {
|
||||
HStack {
|
||||
HStack(spacing: 20) {
|
||||
#if !os(tvOS)
|
||||
fullscreenButton
|
||||
|
||||
#if os(iOS)
|
||||
pipButton
|
||||
.padding(.leading, 5)
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
|
||||
rateButton
|
||||
button("overlay", systemImage: "info.circle") {}
|
||||
|
||||
musicModeButton
|
||||
button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) {
|
||||
withAnimation(Self.animation) {
|
||||
model.presentingControlsOverlay.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
closeVideoButton
|
||||
#endif
|
||||
@ -227,74 +220,6 @@ struct PlayerControls: View {
|
||||
#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 {
|
||||
button("Close", systemImage: "xmark") {
|
||||
player.pause()
|
||||
@ -313,54 +238,72 @@ struct PlayerControls: 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)
|
||||
}
|
||||
|
||||
var ratePicker: some View {
|
||||
Picker("Rate", selection: rateBinding) {
|
||||
ForEach(PlayerModel.availableRates, id: \.self) { rate in
|
||||
Text(player.rateLabel(rate)).tag(rate)
|
||||
}
|
||||
}
|
||||
.transaction { t in t.animation = .none }
|
||||
}
|
||||
|
||||
private var rateBinding: Binding<Float> {
|
||||
.init(get: { player.currentRate }, set: { rate in player.currentRate = rate })
|
||||
}
|
||||
|
||||
private var pipButton: some View {
|
||||
button("PiP", systemImage: "pip") {
|
||||
model.startPiP()
|
||||
}
|
||||
}
|
||||
|
||||
var mediumButtonsBar: some View {
|
||||
var floatingControls: some View {
|
||||
HStack {
|
||||
#if !os(tvOS)
|
||||
restartVideoButton
|
||||
.padding(.trailing, 15)
|
||||
HStack(spacing: 20) {
|
||||
togglePlayButton
|
||||
seekBackwardButton
|
||||
seekForwardButton
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
button("Seek Backward", systemImage: "gobackward.10", size: 30, cornerRadius: 5) {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 20) {
|
||||
restartVideoButton
|
||||
advanceToNextItemButton
|
||||
musicModeButton
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
Spacer()
|
||||
private var restartVideoButton: some View {
|
||||
button("Restart video", systemImage: "backward.end.fill", size: 25, cornerRadius: 5, background: false) {
|
||||
player.backend.seek(to: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
private var togglePlayButton: some View {
|
||||
button(
|
||||
model.isPlaying ? "Pause" : "Play",
|
||||
systemImage: model.isPlaying ? "pause.fill" : "play.fill",
|
||||
size: 30, cornerRadius: 5
|
||||
size: 25, cornerRadius: 5, background: false
|
||||
) {
|
||||
player.backend.togglePlay()
|
||||
}
|
||||
@ -371,58 +314,23 @@ struct PlayerControls: View {
|
||||
.keyboardShortcut(.space)
|
||||
#endif
|
||||
.disabled(model.isLoadingVideo)
|
||||
|
||||
Spacer()
|
||||
|
||||
#if !os(tvOS)
|
||||
button("Seek Forward", systemImage: "goforward.10", size: 30, cornerRadius: 5) {
|
||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||
}
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .forward)
|
||||
#else
|
||||
.keyboardShortcut("l", modifiers: [])
|
||||
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
|
||||
#endif
|
||||
|
||||
advanceToNextItemButton
|
||||
.padding(.leading, 15)
|
||||
#endif
|
||||
}
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
|
||||
private var restartVideoButton: some View {
|
||||
button("Restart video", systemImage: "backward.end.fill", size: 30, cornerRadius: 5) {
|
||||
player.backend.seek(to: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
.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(
|
||||
_ label: String,
|
||||
systemImage: String? = nil,
|
||||
size: Double = 30,
|
||||
size: Double = 25,
|
||||
width: Double? = nil,
|
||||
height: Double? = nil,
|
||||
cornerRadius: Double = 3,
|
||||
background: Bool = true,
|
||||
active: Bool = false,
|
||||
action: @escaping () -> Void = {}
|
||||
) -> some View {
|
||||
@ -442,39 +350,30 @@ struct PlayerControls: View {
|
||||
.padding()
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.font(.system(size: 13))
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(active ? .accentColor : .primary)
|
||||
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||
.frame(width: width ?? size, height: height ?? size)
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBlur(material: .hudWindow))
|
||||
#elseif os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||
#endif
|
||||
.mask(RoundedRectangle(cornerRadius: cornerRadius))
|
||||
.modifier(ControlBackgroundModifier(enabled: background))
|
||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||
}
|
||||
|
||||
var fullScreenLayout: Bool {
|
||||
#if os(iOS)
|
||||
model.playingFullscreen || verticalSizeClass == .compact
|
||||
player.playingFullScreen || verticalSizeClass == .compact
|
||||
#else
|
||||
model.playingFullscreen
|
||||
player.playingFullScreen
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerControls_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let model = PlayerControlsModel()
|
||||
model.presentingControls = true
|
||||
model.currentTime = .secondsInDefaultTimescale(0)
|
||||
model.duration = .secondsInDefaultTimescale(120)
|
||||
|
||||
return ZStack {
|
||||
ZStack {
|
||||
Color.gray
|
||||
|
||||
PlayerControls(player: PlayerModel(), thumbnails: ThumbnailsModel())
|
||||
.injectFixtureEnvironmentObjects()
|
||||
.environmentObject(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ final class MPVOGLView: GLKView {
|
||||
var needsDrawing = true
|
||||
|
||||
override init(frame: CGRect) {
|
||||
guard let context = EAGLContext(api: .openGLES3) else {
|
||||
guard let context = EAGLContext(api: .openGLES2) else {
|
||||
print("Failed to initialize OpenGLES 2.0 context")
|
||||
exit(1)
|
||||
}
|
||||
@ -20,10 +20,12 @@ final class MPVOGLView: GLKView {
|
||||
|
||||
super.init(frame: frame, context: context)
|
||||
|
||||
EAGLContext.setCurrent(context)
|
||||
self.context = context
|
||||
bindDrawable()
|
||||
|
||||
defaultFBO = -1
|
||||
isOpaque = false
|
||||
isOpaque = true
|
||||
enableSetNeedsDisplay = false
|
||||
|
||||
fillBlack()
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import UIKit
|
||||
|
||||
final class MPVViewController: UIViewController {
|
||||
var client: MPVClient!
|
||||
var glView: MPVOGLView!
|
||||
|
||||
init() {
|
||||
client = MPVClient()
|
||||
@ -17,9 +16,8 @@ final class MPVViewController: UIViewController {
|
||||
super.loadView()
|
||||
|
||||
client.create(frame: view.frame)
|
||||
glView = client.glView
|
||||
|
||||
view.addSubview(glView)
|
||||
view.addSubview(client.glView)
|
||||
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ struct NoCommentsView: View {
|
||||
.font(.system(size: 12))
|
||||
#endif
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: .infinity)
|
||||
#if !os(tvOS)
|
||||
.foregroundColor(.secondary)
|
||||
#endif
|
||||
|
@ -6,7 +6,7 @@ import SwiftUI
|
||||
struct PlayerQueueRow: View {
|
||||
let item: PlayerQueueItem
|
||||
var history = false
|
||||
@Binding var fullScreen: Bool
|
||||
var fullScreen: Bool
|
||||
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
|
||||
@ -14,10 +14,10 @@ struct PlayerQueueRow: View {
|
||||
|
||||
@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.history = history
|
||||
_fullScreen = fullScreen
|
||||
self.fullScreen = fullScreen
|
||||
_watchRequest = FetchRequest<Watch>(
|
||||
entity: Watch.entity(),
|
||||
sortDescriptors: [],
|
||||
@ -32,6 +32,8 @@ struct PlayerQueueRow: View {
|
||||
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||
|
||||
player.videoBeingOpened = item.video
|
||||
|
||||
if history {
|
||||
player.playHistory(item, at: watchStoppedAt)
|
||||
} else {
|
||||
@ -39,9 +41,9 @@ struct PlayerQueueRow: View {
|
||||
}
|
||||
|
||||
if fullScreen {
|
||||
withAnimation {
|
||||
fullScreen = false
|
||||
}
|
||||
// withAnimation {
|
||||
// fullScreen = false
|
||||
// }
|
||||
}
|
||||
|
||||
if closePiPOnNavigation, player.playingInPictureInPicture {
|
||||
|
@ -3,8 +3,8 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct PlayerQueueView: View {
|
||||
@Binding var sidebarQueue: Bool
|
||||
@Binding var fullScreen: Bool
|
||||
var sidebarQueue: Bool
|
||||
var fullScreen: Bool
|
||||
|
||||
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
||||
var watches: FetchedResults<Watch>
|
||||
@ -49,7 +49,7 @@ struct PlayerQueueView: View {
|
||||
}
|
||||
|
||||
ForEach(player.queue) { item in
|
||||
PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
||||
PlayerQueueRow(item: item, fullScreen: fullScreen)
|
||||
.contextMenu {
|
||||
removeButton(item)
|
||||
removeAllButton()
|
||||
@ -70,7 +70,7 @@ struct PlayerQueueView: View {
|
||||
PlayerQueueRow(
|
||||
item: PlayerQueueItem.from(watch, video: player.historyVideo(watch.videoID)),
|
||||
history: true,
|
||||
fullScreen: $fullScreen
|
||||
fullScreen: fullScreen
|
||||
)
|
||||
.onAppear {
|
||||
player.loadHistoryVideoDetails(watch.videoID)
|
||||
@ -89,7 +89,7 @@ struct PlayerQueueView: View {
|
||||
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
|
||||
Section(header: Text("Related")) {
|
||||
ForEach(player.currentVideo!.related) { video in
|
||||
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: $fullScreen)
|
||||
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: fullScreen)
|
||||
.contextMenu {
|
||||
Button {
|
||||
player.playNext(video)
|
||||
@ -137,7 +137,7 @@ struct PlayerQueueView: View {
|
||||
struct PlayerQueueView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
PlayerQueueView(sidebarQueue: .constant(true), fullScreen: .constant(true))
|
||||
PlayerQueueView(sidebarQueue: true, fullScreen: true)
|
||||
}
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
|
@ -1,15 +1,20 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct RelatedView: View {
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
|
||||
if let related = player.currentVideo?.related {
|
||||
Section(header: Text("Related")) {
|
||||
ForEach(player.currentVideo!.related) { video in
|
||||
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: .constant(false))
|
||||
ForEach(related) { video in
|
||||
PlayerQueueRow(item: PlayerQueueItem(video))
|
||||
.contextMenu {
|
||||
Section {
|
||||
Button {
|
||||
player.playNext(video)
|
||||
} label: {
|
||||
@ -21,6 +26,25 @@ struct RelatedView: View {
|
||||
Label("Play Last", systemImage: "text.append")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
struct TimelineView: View {
|
||||
@Binding private var duration: Double
|
||||
@Binding private var current: Double
|
||||
enum Context {
|
||||
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 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 draggedFrom: Double = 0
|
||||
|
||||
@ -13,38 +36,119 @@ struct TimelineView: View {
|
||||
private var height = 8.0
|
||||
|
||||
var cornerRadius: Double
|
||||
var thumbTooltipWidth: Double = 100
|
||||
var thumbAreaWidth: Double = 40
|
||||
var context: Context
|
||||
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlayerControlsModel> private var controls
|
||||
@EnvironmentObject<PlayerTimeModel> private var playerTime
|
||||
|
||||
init(duration: Binding<Double>, current: Binding<Double>, cornerRadius: Double = 10.0) {
|
||||
_duration = duration
|
||||
_current = current
|
||||
var chapters: [Chapter] {
|
||||
player.currentVideo?.chapters ?? []
|
||||
}
|
||||
|
||||
init(
|
||||
cornerRadius: Double = 10.0,
|
||||
context: Context = .controls
|
||||
) {
|
||||
self.cornerRadius = cornerRadius
|
||||
self.context = context
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
VStack {
|
||||
Group {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.foregroundColor(.blue)
|
||||
.frame(maxHeight: height)
|
||||
VStack(spacing: 3) {
|
||||
if dragging {
|
||||
if let segment = projectedSegment,
|
||||
let description = SponsorBlockAPI.categoryDescription(segment.category)
|
||||
{
|
||||
Text(description)
|
||||
.font(.system(size: 8))
|
||||
.fixedSize()
|
||||
.lineLimit(1)
|
||||
.foregroundColor(Color("AppRedColor"))
|
||||
}
|
||||
if let chapter = projectedChapter {
|
||||
Text(chapter.title)
|
||||
.lineLimit(3)
|
||||
.font(.system(size: 11).bold())
|
||||
.frame(maxWidth: 250)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
Text((dragging ? projectedValue : current).formattedAsPlaybackTime(allowZero: true) ?? PlayerTimeModel.timePlaceholder)
|
||||
.font(.system(size: 11).monospacedDigit())
|
||||
}
|
||||
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Color.green)
|
||||
.padding(.vertical, 3)
|
||||
.padding(.horizontal, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.foregroundColor(.black)
|
||||
)
|
||||
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.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)
|
||||
.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()
|
||||
.strokeBorder(.gray, lineWidth: 1)
|
||||
.background(Circle().fill(dragging ? .gray : .white))
|
||||
.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)
|
||||
.foregroundColor(.red.opacity(0.6))
|
||||
.frame(maxHeight: height * 4)
|
||||
.frame(maxWidth: thumbAreaWidth, minHeight: thumbAreaWidth)
|
||||
|
||||
#if !os(tvOS)
|
||||
.gesture(
|
||||
@ -69,9 +173,10 @@ struct TimelineView: View {
|
||||
}
|
||||
}
|
||||
.onEnded { _ in
|
||||
current = projectedValue
|
||||
|
||||
if abs(dragOffset) > 0 {
|
||||
playerTime.currentTime = .secondsInDefaultTimescale(projectedValue)
|
||||
player.backend.seek(to: projectedValue)
|
||||
}
|
||||
|
||||
dragging = false
|
||||
dragOffset = 0.0
|
||||
@ -80,18 +185,8 @@ struct TimelineView: View {
|
||||
}
|
||||
)
|
||||
#endif
|
||||
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.frame(maxWidth: thumbTooltipWidth, maxHeight: 30)
|
||||
|
||||
Text(projectedValue.formattedAsPlaybackTime() ?? "--:--")
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.animation(.linear(duration: 0.1))
|
||||
.opacity(dragging ? 1 : 0)
|
||||
.offset(x: thumbTooltipOffset, y: -(height * 2) - 7)
|
||||
}
|
||||
|
||||
.background(GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
@ -101,59 +196,117 @@ struct TimelineView: View {
|
||||
self.size = size
|
||||
}
|
||||
})
|
||||
.frame(maxHeight: 20)
|
||||
#if !os(tvOS)
|
||||
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
|
||||
let target = (value.location.x / size.width) * units
|
||||
current = target
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
var tooltipVeritcalOffset: Double {
|
||||
var offset = -20.0
|
||||
|
||||
if !projectedChapter.isNil {
|
||||
offset -= 8.0
|
||||
}
|
||||
|
||||
if !projectedSegment.isNil {
|
||||
offset -= 6.5
|
||||
}
|
||||
|
||||
return offset
|
||||
}
|
||||
|
||||
var projectedValue: Double {
|
||||
let change = (dragOffset / size.width) * units
|
||||
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 {
|
||||
let offset = dragging ? (draggedThumbHorizontalOffset + dragOffset) : thumbHorizontalOffset
|
||||
let offset = dragging ? draggedThumbHorizontalOffset : thumbHorizontalOffset
|
||||
return offset.isFinite ? offset : thumbLeadingOffset
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var minThumbTooltipOffset: Double = -10
|
||||
return thumbOffset.clamped(to: -leadingOffset ... leadingOffset)
|
||||
}
|
||||
|
||||
var minThumbTooltipOffset: Double {
|
||||
60
|
||||
}
|
||||
|
||||
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 {
|
||||
ForEach(player.sponsorBlock.segments, id: \.uuid) { segment in
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
ForEach(segments, id: \.uuid) { segment in
|
||||
Rectangle()
|
||||
.offset(x: segmentLayerHorizontalOffset(segment))
|
||||
.foregroundColor(.red)
|
||||
.foregroundColor(Color("AppRedColor"))
|
||||
.frame(maxHeight: height)
|
||||
.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 {
|
||||
segment.start * oneUnitWidth
|
||||
}
|
||||
|
||||
func segmentLayerWidth(_ segment: Segment) -> Double {
|
||||
let width = segment.duration * oneUnitWidth
|
||||
return width.isFinite ? width : thumbLeadingOffset
|
||||
return width.isFinite ? width : 1
|
||||
}
|
||||
|
||||
var draggedThumbHorizontalOffset: Double {
|
||||
thumbLeadingOffset + (draggedFrom * oneUnitWidth)
|
||||
thumbLeadingOffset + (draggedFrom * oneUnitWidth) + dragOffset
|
||||
}
|
||||
|
||||
var thumbHorizontalOffset: Double {
|
||||
@ -161,7 +314,7 @@ struct TimelineView: View {
|
||||
}
|
||||
|
||||
var thumbLeadingOffset: Double {
|
||||
-(size.width / 2)
|
||||
-size.width / 2
|
||||
}
|
||||
|
||||
var oneUnitWidth: Double {
|
||||
@ -172,26 +325,33 @@ struct TimelineView: View {
|
||||
var units: Double {
|
||||
duration - start
|
||||
}
|
||||
|
||||
func setCurrent(_ current: Double) {
|
||||
withAnimation {
|
||||
self.current = current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
VStack(spacing: 40) {
|
||||
TimelineView(duration: .constant(100), current: .constant(0))
|
||||
TimelineView(duration: .constant(100), current: .constant(1))
|
||||
TimelineView(duration: .constant(100), current: .constant(30))
|
||||
TimelineView(duration: .constant(100), current: .constant(50))
|
||||
TimelineView(duration: .constant(100), current: .constant(66))
|
||||
TimelineView(duration: .constant(100), current: .constant(90))
|
||||
TimelineView(duration: .constant(100), current: .constant(100))
|
||||
let playerModel = PlayerModel()
|
||||
playerModel.currentItem = .init(Video.fixture)
|
||||
let playerTimeModel = PlayerTimeModel()
|
||||
playerTimeModel.player = playerModel
|
||||
playerTimeModel.currentTime = .secondsInDefaultTimescale(33)
|
||||
playerTimeModel.duration = .secondsInDefaultTimescale(100)
|
||||
return VStack(spacing: 40) {
|
||||
TimelineView()
|
||||
}
|
||||
.environmentObject(PlayerModel())
|
||||
.environmentObject(playerModel)
|
||||
.environmentObject(playerTimeModel)
|
||||
.environmentObject(PlayerControlsModel())
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,30 @@ import Defaults
|
||||
import Foundation
|
||||
import SDWebImageSwiftUI
|
||||
import SwiftUI
|
||||
import SwiftUIPager
|
||||
|
||||
struct VideoDetails: View {
|
||||
enum Page {
|
||||
case info, comments, related, queue
|
||||
enum DetailsPage: CaseIterable {
|
||||
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
|
||||
@Binding var fullScreen: Bool
|
||||
var sidebarQueue: Bool
|
||||
var fullScreen: Bool
|
||||
|
||||
@State private var subscribed = false
|
||||
@State private var subscriptionToggleButtonDisabled = false
|
||||
@ -18,89 +34,82 @@ struct VideoDetails: View {
|
||||
@State private var presentingShareSheet = false
|
||||
@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
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<CommentsModel> private var comments
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
@Default(.showKeywords) private var showKeywords
|
||||
@Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle
|
||||
@Default(.controlsBarInPlayer) private var controlsBarInPlayer
|
||||
|
||||
init(
|
||||
sidebarQueue: Binding<Bool>? = nil,
|
||||
fullScreen: Binding<Bool>? = nil
|
||||
) {
|
||||
_sidebarQueue = sidebarQueue ?? .constant(true)
|
||||
_fullScreen = fullScreen ?? .constant(false)
|
||||
var currentPage: DetailsPage {
|
||||
DetailsPage.allCases.first { $0.index == page.index } ?? .info
|
||||
}
|
||||
|
||||
var video: Video? {
|
||||
player.currentVideo
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Group {
|
||||
Group {
|
||||
HStack(spacing: 0) {
|
||||
title
|
||||
func pageButton(
|
||||
_ label: String,
|
||||
_ symbolName: String,
|
||||
_ destination: DetailsPage,
|
||||
pageChangeAction: (() -> Void)? = nil
|
||||
) -> some View {
|
||||
Button(action: {
|
||||
page.update(.new(index: destination.index))
|
||||
pageChangeAction?()
|
||||
}) {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
toggleFullScreenDetailsButton
|
||||
}
|
||||
#if os(macOS)
|
||||
.padding(.top, 10)
|
||||
#endif
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: symbolName)
|
||||
|
||||
if !video.isNil {
|
||||
Divider()
|
||||
if playerDetailsPageButtonLabelStyle.text {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 15)
|
||||
.lineLimit(1)
|
||||
.padding(.vertical, 4)
|
||||
.foregroundColor(currentPage == destination ? .white : .accentColor)
|
||||
|
||||
subscriptionsSection
|
||||
.onChange(of: video) { video in
|
||||
if let video = video {
|
||||
subscribed = subscriptions.isSubscribing(video.channel.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
if !sidebarQueue ||
|
||||
(CommentsModel.enabled && CommentsModel.placement == .separate)
|
||||
{
|
||||
pagePicker
|
||||
.padding(.horizontal)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onSwipeGesture(
|
||||
up: {
|
||||
withAnimation {
|
||||
fullScreen = true
|
||||
}
|
||||
},
|
||||
down: {
|
||||
withAnimation {
|
||||
if fullScreen {
|
||||
fullScreen = false
|
||||
} else {
|
||||
self.player.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(currentPage == destination ? Color.accentColor : .clear)
|
||||
.buttonStyle(.plain)
|
||||
.font(.system(size: 10).bold())
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.accentColor, lineWidth: 2)
|
||||
.foregroundColor(.clear)
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
switch currentPage {
|
||||
@ViewBuilder func detailsByPage(_ page: DetailsPage) -> some View {
|
||||
Group {
|
||||
switch page {
|
||||
case .info:
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
detailsPage
|
||||
}
|
||||
case .chapters:
|
||||
ChaptersView()
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
|
||||
case .queue:
|
||||
PlayerQueueView(sidebarQueue: $sidebarQueue, fullScreen: $fullScreen)
|
||||
PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: fullScreen)
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
|
||||
case .related:
|
||||
@ -111,9 +120,54 @@ struct VideoDetails: View {
|
||||
.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 {
|
||||
if video.isNil && !sidebarQueue {
|
||||
currentPage = .queue
|
||||
page.update(.new(index: DetailsPage.queue.index))
|
||||
}
|
||||
|
||||
guard video != nil, accounts.app.supportsSubscriptions else {
|
||||
@ -124,91 +178,56 @@ struct VideoDetails: View {
|
||||
.onChange(of: sidebarQueue) { queue in
|
||||
if queue {
|
||||
if currentPage == .related || currentPage == .queue {
|
||||
currentPage = .info
|
||||
page.update(.moveToFirst)
|
||||
}
|
||||
} else if video.isNil {
|
||||
currentPage = .queue
|
||||
page.update(.moveToLast)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
|
||||
var title: some View {
|
||||
Group {
|
||||
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 showAddToPlaylistButton: Bool {
|
||||
accounts.app.supportsUserPlaylists && accounts.signedIn
|
||||
}
|
||||
|
||||
var subscriptionsSection: some View {
|
||||
Group {
|
||||
if video != nil {
|
||||
if let video = video {
|
||||
HStack(alignment: .center) {
|
||||
HStack(spacing: 10) {
|
||||
Group {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
authorAvatar
|
||||
// ZStack(alignment: .bottomTrailing) {
|
||||
// authorAvatar
|
||||
//
|
||||
// if subscribed {
|
||||
// Image(systemName: "star.circle.fill")
|
||||
// .background(Color.background)
|
||||
// .clipShape(Circle())
|
||||
// .foregroundColor(.secondary)
|
||||
// }
|
||||
// }
|
||||
|
||||
if subscribed {
|
||||
Image(systemName: "star.circle.fill")
|
||||
.background(Color.background)
|
||||
.clipShape(Circle())
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(video!.channel.name)
|
||||
.font(.system(size: 14))
|
||||
.bold()
|
||||
|
||||
Group {
|
||||
if let subscribers = video!.channel.subscriptionsString {
|
||||
Text("\(subscribers) subscribers")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
}
|
||||
// VStack(alignment: .leading, spacing: 4) {
|
||||
// Text(video.title)
|
||||
// .font(.system(size: 11))
|
||||
// .fontWeight(.bold)
|
||||
//
|
||||
// HStack(spacing: 4) {
|
||||
// Text(video.channel.name)
|
||||
//
|
||||
// if let subscribers = video.channel.subscriptionsString {
|
||||
// Text("•")
|
||||
// .foregroundColor(.secondary)
|
||||
// .opacity(0.3)
|
||||
//
|
||||
// Text("\(subscribers) subscribers")
|
||||
// }
|
||||
// }
|
||||
// .foregroundColor(.secondary)
|
||||
// .font(.caption2)
|
||||
// }
|
||||
}
|
||||
}
|
||||
.contentShape(RoundedRectangle(cornerRadius: 12))
|
||||
@ -227,80 +246,8 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,30 +258,9 @@ struct VideoDetails: View {
|
||||
if let published = video.publishedDate {
|
||||
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 {
|
||||
@ -386,13 +312,6 @@ struct VideoDetails: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $presentingAddToPlaylist) {
|
||||
if let video = video {
|
||||
AddToPlaylistView(video: video)
|
||||
}
|
||||
}
|
||||
)
|
||||
#if os(iOS)
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $presentingShareSheet) {
|
||||
@ -419,31 +338,60 @@ struct VideoDetails: View {
|
||||
.retryOnAppear(true)
|
||||
.indicator(.activity)
|
||||
.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 {
|
||||
Group {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let video = player.currentVideo {
|
||||
VStack(spacing: 6) {
|
||||
HStack {
|
||||
publishedDateSection
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
countsSection
|
||||
videoProperties
|
||||
|
||||
Divider()
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
|
||||
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 {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
Text(description)
|
||||
@ -531,7 +479,7 @@ struct VideoDetails: View {
|
||||
|
||||
struct VideoDetails_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VideoDetails(sidebarQueue: .constant(true))
|
||||
VideoDetails(sidebarQueue: true, fullScreen: false)
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import SwiftUI
|
||||
|
||||
struct VideoPlayerView: View {
|
||||
#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
|
||||
|
||||
static let defaultAspectRatio = 16 / 9.0
|
||||
@ -20,20 +20,22 @@ struct VideoPlayerView: View {
|
||||
#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 fullScreenDetails = false
|
||||
@State private var sidebarQueue = false
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@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 orientation = UIInterfaceOrientation.portrait
|
||||
@State private var lastOrientation: UIInterfaceOrientation?
|
||||
@ -46,19 +48,29 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<ThumbnailsModel> private var thumbnails
|
||||
|
||||
init() {
|
||||
if Defaults[.playerSidebar] == .always {
|
||||
sidebarQueue = true
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// TODO: remove
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
_ = Self._printChanges()
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
HSplitView {
|
||||
return HSplitView {
|
||||
content
|
||||
}
|
||||
.onOpenURL { OpenURLHandler(accounts: accounts, player: player).handle($0) }
|
||||
.frame(minWidth: 950, minHeight: 700)
|
||||
#else
|
||||
GeometryReader { geometry in
|
||||
return GeometryReader { geometry in
|
||||
HStack(spacing: 0) {
|
||||
content
|
||||
.onAppear {
|
||||
@ -79,6 +91,11 @@ struct VideoPlayerView: View {
|
||||
if newValue {
|
||||
viewVerticalOffset = 0
|
||||
configureOrientationUpdatesBasedOnAccelerometer()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak player] in
|
||||
player?.onPresentPlayer?()
|
||||
player?.onPresentPlayer = nil
|
||||
}
|
||||
} else {
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
@ -95,7 +112,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#if os(iOS)
|
||||
.offset(y: viewVerticalOffset)
|
||||
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
|
||||
.animation(.easeOut(duration: 0.3), value: viewVerticalOffset)
|
||||
.backport
|
||||
.persistentSystemOverlays(!fullScreenLayout)
|
||||
#endif
|
||||
@ -104,7 +121,7 @@ struct VideoPlayerView: View {
|
||||
|
||||
var content: some View {
|
||||
Group {
|
||||
Group {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
#if os(tvOS)
|
||||
playerView
|
||||
.ignoresSafeArea(.all, edges: .all)
|
||||
@ -138,17 +155,17 @@ struct VideoPlayerView: View {
|
||||
VideoPlayerSizeModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.avPlayerBackend.controller?.aspectRatio,
|
||||
fullScreen: playerControls.playingFullscreen
|
||||
fullScreen: player.playingFullScreen
|
||||
)
|
||||
)
|
||||
.overlay(playerPlaceholder(geometry: geometry))
|
||||
// .overlay(playerPlaceholder(geometry: geometry))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil)
|
||||
.onHover { hovering in
|
||||
hoveringPlayer = hovering
|
||||
hovering ? playerControls.show() : playerControls.hide()
|
||||
// hovering ? playerControls.show() : playerControls.hide()
|
||||
}
|
||||
#if !os(macOS)
|
||||
.gesture(
|
||||
@ -169,10 +186,8 @@ struct VideoPlayerView: View {
|
||||
return
|
||||
}
|
||||
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
viewVerticalOffset = drag
|
||||
}
|
||||
}
|
||||
.onEnded { _ in
|
||||
if viewVerticalOffset > 100 {
|
||||
player.backend.setNeedsDrawing(false)
|
||||
@ -185,29 +200,30 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
)
|
||||
#else
|
||||
.onAppear(perform: {
|
||||
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
|
||||
if hoveringPlayer {
|
||||
playerControls.resetTimer()
|
||||
}
|
||||
|
||||
return $0
|
||||
}
|
||||
})
|
||||
// .onAppear(perform: {
|
||||
// NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
|
||||
// if hoveringPlayer {
|
||||
// playerControls.resetTimer()
|
||||
// }
|
||||
//
|
||||
// return $0
|
||||
// }
|
||||
// })
|
||||
#endif
|
||||
|
||||
.background(Color.black)
|
||||
|
||||
#if !os(tvOS)
|
||||
if !playerControls.playingFullscreen {
|
||||
Group {
|
||||
if !player.playingFullScreen {
|
||||
VStack(spacing: 0) {
|
||||
#if os(iOS)
|
||||
if verticalSizeClass == .regular {
|
||||
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails)
|
||||
}
|
||||
|
||||
#else
|
||||
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails)
|
||||
|
||||
#endif
|
||||
}
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
@ -220,28 +236,35 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
if !fullScreenLayout {
|
||||
ControlsBar()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 650)
|
||||
#endif
|
||||
if !playerControls.playingFullscreen {
|
||||
if !player.playingFullScreen {
|
||||
#if os(iOS)
|
||||
if sidebarQueue {
|
||||
PlayerQueueView(sidebarQueue: .constant(true), fullScreen: $fullScreenDetails)
|
||||
PlayerQueueView(sidebarQueue: true, fullScreen: fullScreenDetails)
|
||||
.frame(maxWidth: 350)
|
||||
}
|
||||
#elseif os(macOS)
|
||||
if Defaults[.playerSidebar] != .never {
|
||||
PlayerQueueView(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreenDetails)
|
||||
PlayerQueueView(sidebarQueue: true, fullScreen: fullScreenDetails)
|
||||
.frame(minWidth: 300)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.transition(.asymmetric(insertion: .slide, removal: .identity))
|
||||
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
||||
#if os(iOS)
|
||||
.statusBar(hidden: playerControls.playingFullscreen)
|
||||
.statusBar(hidden: player.playingFullScreen)
|
||||
.navigationBarHidden(true)
|
||||
#endif
|
||||
}
|
||||
@ -285,9 +308,9 @@ struct VideoPlayerView: View {
|
||||
|
||||
var fullScreenLayout: Bool {
|
||||
#if os(iOS)
|
||||
playerControls.playingFullscreen || verticalSizeClass == .compact
|
||||
player.playingFullScreen || verticalSizeClass == .compact
|
||||
#else
|
||||
playerControls.playingFullscreen
|
||||
player.playingFullScreen
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -357,29 +380,11 @@ struct VideoPlayerView: View {
|
||||
.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)
|
||||
private func configureOrientationUpdatesBasedOnAccelerometer() {
|
||||
if UIDevice.current.orientation.isLandscape,
|
||||
enterFullscreenInLandscape,
|
||||
!playerControls.playingFullscreen,
|
||||
Defaults[.enterFullscreenInLandscape],
|
||||
!player.playingFullScreen,
|
||||
!player.playingInPictureInPicture
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
@ -387,7 +392,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
guard !honorSystemOrientationLock, motionManager.isNil else {
|
||||
guard !Defaults[.honorSystemOrientationLock], motionManager.isNil else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -422,7 +427,7 @@ struct VideoPlayerView: View {
|
||||
|
||||
if orientation.isLandscape {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||
guard enterFullscreenInLandscape else {
|
||||
guard Defaults[.enterFullscreenInLandscape] else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -433,7 +438,7 @@ struct VideoPlayerView: View {
|
||||
|
||||
Orientation.lockOrientation(orientationLockMask, andRotateTo: orientation)
|
||||
|
||||
guard lockOrientationInFullScreen else {
|
||||
guard Defaults[.lockOrientationInFullScreen] else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -442,8 +447,8 @@ struct VideoPlayerView: View {
|
||||
} else {
|
||||
guard abs(acceleration.z) <= 0.74,
|
||||
player.lockedOrientation.isNil,
|
||||
enterFullscreenInLandscape,
|
||||
!lockOrientationInFullScreen
|
||||
Defaults[.enterFullscreenInLandscape],
|
||||
!Defaults[.lockOrientationInFullScreen]
|
||||
else {
|
||||
return
|
||||
}
|
||||
@ -462,14 +467,14 @@ struct VideoPlayerView: View {
|
||||
let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
|
||||
if newOrientation?.isLandscape ?? false,
|
||||
player.presentingPlayer,
|
||||
lockOrientationInFullScreen,
|
||||
Defaults[.lockOrientationInFullScreen],
|
||||
!player.lockedOrientation.isNil
|
||||
{
|
||||
Orientation.lockOrientation(.landscape, andRotateTo: newOrientation)
|
||||
return
|
||||
}
|
||||
|
||||
guard player.presentingPlayer, enterFullscreenInLandscape, honorSystemOrientationLock else {
|
||||
guard player.presentingPlayer, Defaults[.enterFullscreenInLandscape], Defaults[.honorSystemOrientationLock] else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -9,11 +9,11 @@ struct AddToPlaylistView: View {
|
||||
|
||||
@State private var error = ""
|
||||
@State private var presentingErrorAlert = false
|
||||
@State private var submitButtonDisabled = false
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlaylistsModel> private var model
|
||||
|
||||
var body: some View {
|
||||
@ -123,14 +123,8 @@ struct AddToPlaylistView: View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Add to Playlist", action: addToPlaylist)
|
||||
.disabled(submitButtonDisabled || selectedPlaylist.isNil)
|
||||
.disabled(selectedPlaylist.isNil)
|
||||
.padding(.top, 30)
|
||||
.alert(isPresented: $presentingErrorAlert) {
|
||||
Alert(
|
||||
title: Text("Error when accessing playlist"),
|
||||
message: Text(error)
|
||||
)
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
@ -166,20 +160,9 @@ struct AddToPlaylistView: View {
|
||||
|
||||
Defaults[.lastUsedPlaylistID] = id
|
||||
|
||||
submitButtonDisabled = true
|
||||
model.addVideo(playlistID: id, videoID: video.videoID, navigation: navigation)
|
||||
|
||||
model.addVideo(
|
||||
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? {
|
||||
|
@ -66,6 +66,7 @@ struct SearchSuggestions: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.id(UUID())
|
||||
#if os(macOS)
|
||||
.buttonStyle(.link)
|
||||
#endif
|
||||
|
@ -297,6 +297,7 @@ struct SearchView: View {
|
||||
}
|
||||
.redrawOn(change: recentsChanged)
|
||||
}
|
||||
.id(UUID())
|
||||
}
|
||||
#if os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
|
@ -1,140 +1,222 @@
|
||||
import Foundation
|
||||
import SDWebImageSwiftUI
|
||||
import SwiftUI
|
||||
|
||||
struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
||||
let content: Content
|
||||
let toolbar: Toolbar?
|
||||
|
||||
@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()
|
||||
enum Context {
|
||||
case browser, player
|
||||
}
|
||||
|
||||
init(@ViewBuilder content: @escaping () -> Content) where Toolbar == EmptyView {
|
||||
self.init(toolbar: { EmptyView() }, content: content)
|
||||
let 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 {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
_ = Self._printChanges()
|
||||
}
|
||||
|
||||
return VStack(spacing: 0) {
|
||||
content
|
||||
|
||||
#if !os(tvOS)
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
#endif
|
||||
|
||||
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))
|
||||
ControlsBar()
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
#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 {
|
||||
static var previews: some View {
|
||||
BrowserPlayerControls(context: .player) {
|
||||
BrowserPlayerControls {
|
||||
VStack {
|
||||
Spacer()
|
||||
@ -142,6 +224,8 @@ struct PlayerControlsView_Previews: PreviewProvider {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.offset(y: -100)
|
||||
}
|
||||
.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 {
|
||||
addToPlaylistButton
|
||||
addToLastPlaylistButton
|
||||
|
||||
if let id = navigation.tabSelection?.playlistID ?? playlistID {
|
||||
removeFromPlaylistButton(playlistID: id)
|
||||
@ -116,7 +117,7 @@ struct VideoContextMenuView: View {
|
||||
Button {
|
||||
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt))
|
||||
} 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 {
|
||||
Button {
|
||||
playlists.removeVideo(index: video.indexID!, playlistID: playlistID)
|
||||
|
@ -1,4 +1,9 @@
|
||||
import Defaults
|
||||
import MediaPlayer
|
||||
import PINCache
|
||||
import SDWebImage
|
||||
import SDWebImageWebPCoder
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
@ -11,19 +16,27 @@ struct YatteeApp: App {
|
||||
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
|
||||
}
|
||||
|
||||
static var isForPreviews: Bool {
|
||||
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
#elseif os(iOS)
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
#endif
|
||||
|
||||
@State private var configured = false
|
||||
|
||||
@StateObject private var accounts = AccountsModel()
|
||||
@StateObject private var comments = CommentsModel()
|
||||
@StateObject private var instances = InstancesModel()
|
||||
@StateObject private var menu = MenuModel()
|
||||
@StateObject private var navigation = NavigationModel()
|
||||
@StateObject private var networkState = NetworkStateModel()
|
||||
@StateObject private var player = PlayerModel()
|
||||
@StateObject private var playerControls = PlayerControlsModel()
|
||||
@StateObject private var playerTime = PlayerTimeModel()
|
||||
@StateObject private var playlists = PlaylistsModel()
|
||||
@StateObject private var recents = RecentsModel()
|
||||
@StateObject private var search = SearchModel()
|
||||
@ -35,13 +48,16 @@ struct YatteeApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.onAppear(perform: configure)
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(comments)
|
||||
.environmentObject(instances)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(networkState)
|
||||
.environmentObject(player)
|
||||
.environmentObject(playerControls)
|
||||
.environmentObject(playerTime)
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(recents)
|
||||
.environmentObject(subscriptions)
|
||||
@ -86,6 +102,7 @@ struct YatteeApp: App {
|
||||
#if os(macOS)
|
||||
WindowGroup(player.windowTitle) {
|
||||
VideoPlayerView()
|
||||
.onAppear(perform: configure)
|
||||
.background(
|
||||
HostingWindowFinder { window in
|
||||
Windows.playerWindow = window
|
||||
@ -96,7 +113,7 @@ struct YatteeApp: App {
|
||||
queue: OperationQueue.main
|
||||
) { _ in
|
||||
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(instances)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(networkState)
|
||||
.environmentObject(player)
|
||||
.environmentObject(playerControls)
|
||||
.environmentObject(playerTime)
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(recents)
|
||||
.environmentObject(subscriptions)
|
||||
@ -129,4 +148,132 @@ struct YatteeApp: App {
|
||||
}
|
||||
#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 */; };
|
||||
3711404026B206A6005B3555 /* 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 */; };
|
||||
37130A5C277657090033018A /* 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 */; };
|
||||
372915E72687E3B900F5A35B /* 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 */; };
|
||||
372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.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 */; };
|
||||
3751BA8427E6914F007B1A60 /* 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 */; };
|
||||
37579D5E27864F5F00FD0B98 /* 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 */; };
|
||||
3763495126DFF59D00B9A393 /* 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 */; };
|
||||
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 */; };
|
||||
@ -370,8 +388,6 @@
|
||||
37732FF42703D32400F04329 /* 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 */; };
|
||||
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 */; };
|
||||
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.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 */; };
|
||||
3774125A27387D2300423605 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F33272B44000087F250 /* FavoritesModel.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 */; };
|
||||
3774126127387D2D00423605 /* AccountsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37001562271B1F250049C794 /* AccountsModel.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 */; };
|
||||
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 */; };
|
||||
@ -462,6 +473,11 @@
|
||||
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; };
|
||||
378E50FF26FE8EEE00F49626 /* 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 */; };
|
||||
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3795593527B08538007FF8F4 /* StreamControl.swift */; };
|
||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 3797757C268922D100DD52A8 /* Siesta */; };
|
||||
@ -482,6 +498,11 @@
|
||||
37A3B19627257503000FB5EE /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16427255E7F000FB5EE /* content.js */; };
|
||||
37A3B1982725750B000FB5EE /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16027255E7F000FB5EE /* manifest.json */; };
|
||||
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 */; };
|
||||
37A9965B26D6F8CA006E3224 /* 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 */; };
|
||||
37C0698327260B2100F7F6CB /* 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 */; };
|
||||
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.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 */; };
|
||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.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 */; };
|
||||
37CC3F46270CE30600608308 /* 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 */; };
|
||||
37EF9A78275BEB8E0043B585 /* 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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
37F4AE7326828F0900BD60EA /* 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -867,6 +898,7 @@
|
||||
3727B74927872A920021C15E /* VisualEffectBlur-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisualEffectBlur-iOS.swift"; sourceTree = "<group>"; };
|
||||
3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = "<group>"; };
|
||||
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -919,6 +951,10 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -928,6 +964,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -960,7 +997,6 @@
|
||||
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidationStatus.swift; sourceTree = "<group>"; };
|
||||
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
||||
37737785276F9858000521C1 /* Windows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -972,6 +1008,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -985,6 +1023,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1042,8 +1081,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1091,7 +1128,11 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1106,6 +1147,13 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
371264382865FF4500D77974 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
37A3B15427255E7F000FB5EE /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -1152,6 +1200,7 @@
|
||||
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */,
|
||||
37BD07C72698B27B003EBB87 /* Introspect in Frameworks */,
|
||||
37FB284D2722099E00A57617 /* SDWebImageWebPCoder in Frameworks */,
|
||||
37A5DBC4285DFF5400CA4DD1 /* SwiftUIPager in Frameworks */,
|
||||
3749BF8A27ADA135000480FF /* libavformat.a in Frameworks */,
|
||||
377FC7DB267A080300A6BBAF /* Logging in Frameworks */,
|
||||
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */,
|
||||
@ -1190,6 +1239,7 @@
|
||||
370F4FD927CC16CB001B35DC /* libxcb-shm.0.0.0.dylib in Frameworks */,
|
||||
370F4FCA27CC16CB001B35DC /* libXau.6.dylib in Frameworks */,
|
||||
370F4FD527CC16CB001B35DC /* libfontconfig.1.dylib in Frameworks */,
|
||||
37A5DBC6285E06B100CA4DD1 /* SwiftUIPager in Frameworks */,
|
||||
370F4FCE27CC16CB001B35DC /* libswresample.4.3.100.dylib in Frameworks */,
|
||||
370F4FDA27CC16CB001B35DC /* libmpv.dylib in Frameworks */,
|
||||
370F4FD827CC16CB001B35DC /* libcrypto.3.dylib in Frameworks */,
|
||||
@ -1312,11 +1362,22 @@
|
||||
371114F227B9552400C2EF7B /* Controls */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3756C2A428610F6D00E4B059 /* OSD */,
|
||||
37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */,
|
||||
37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */,
|
||||
37F13B61285E43C000B137E4 /* ControlsOverlay.swift */,
|
||||
);
|
||||
path = Controls;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3712643C2865FF4500D77974 /* Shared Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
378FFBC82866018A009E3FBE /* URLParserTests.swift */,
|
||||
);
|
||||
path = "Shared Tests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
371AAE2326CEB9E800901972 /* Navigation */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1339,6 +1400,7 @@
|
||||
375E45F327B1973400BA7902 /* MPV */,
|
||||
37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */,
|
||||
37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */,
|
||||
3752069C285E910600CA655F /* ChaptersView.swift */,
|
||||
371B7E602759706A00D21217 /* CommentsView.swift */,
|
||||
37EF9A75275BEB8E0043B585 /* CommentView.swift */,
|
||||
37DD9DA22785BBC900539416 /* NoCommentsView.swift */,
|
||||
@ -1397,6 +1459,7 @@
|
||||
37C3A250272366440087A57A /* ChannelPlaylistView.swift */,
|
||||
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */,
|
||||
37FB285D272225E800A57617 /* ContentItemView.swift */,
|
||||
372CFD14285F2E2A00B0B54B /* ControlsBar.swift */,
|
||||
3748186D26A769D60084E870 /* DetailBadge.swift */,
|
||||
37599F37272B4D740087F250 /* FavoriteButton.swift */,
|
||||
37152EE926EFEB95004FB96D /* LazyView.swift */,
|
||||
@ -1453,6 +1516,7 @@
|
||||
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */,
|
||||
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */,
|
||||
375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */,
|
||||
376527BA285F60F700102284 /* PlayerTimeModel.swift */,
|
||||
);
|
||||
path = Player;
|
||||
sourceTree = "<group>";
|
||||
@ -1574,6 +1638,16 @@
|
||||
path = ReturnYouTubeDislike;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3756C2A428610F6D00E4B059 /* OSD */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3756C2A52861131100E4B059 /* NetworkState.swift */,
|
||||
37F4AD1A28612B23004D0F66 /* OpeningStream.swift */,
|
||||
37F4AD1E28612DFD004D0F66 /* Buffering.swift */,
|
||||
);
|
||||
path = OSD;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
375E45F327B1973400BA7902 /* MPV */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1717,6 +1791,7 @@
|
||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
||||
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */,
|
||||
378AE942274EF00A006A4EE1 /* Color+Background.swift */,
|
||||
37F4AD2528613B81004D0F66 /* Color+Debug.swift */,
|
||||
37E8B0EF27B326F30024006F /* Comparable+Clamped.swift */,
|
||||
37C3A240272359900087A57A /* Double+Format.swift */,
|
||||
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
||||
@ -1743,6 +1818,7 @@
|
||||
37DD9DCC2785EE6F00539416 /* Vendor */,
|
||||
3748186426A762300084E870 /* Fixtures */,
|
||||
37A3B15827255E7F000FB5EE /* Open in Yattee */,
|
||||
3712643C2865FF4500D77974 /* Shared Tests */,
|
||||
377FC7D1267A080300A6BBAF /* Frameworks */,
|
||||
37D4B0CA2671614900C925CA /* Products */,
|
||||
37D4B174267164B000C925CA /* Tests Apple TV */,
|
||||
@ -1774,7 +1850,7 @@
|
||||
371114EA27B94C8800C2EF7B /* RepeatingTimer.swift */,
|
||||
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */,
|
||||
37FFC43F272734C3009FFD26 /* Throttle.swift */,
|
||||
37CB12782724C76D00213B45 /* VideoURLParser.swift */,
|
||||
378FFBC328660172009E3FBE /* URLParser.swift */,
|
||||
37D4B0C22671614700C925CA /* YatteeApp.swift */,
|
||||
37D4B0C42671614800C925CA /* Assets.xcassets */,
|
||||
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
|
||||
@ -1793,6 +1869,7 @@
|
||||
37D4B171267164B000C925CA /* Tests (tvOS).xctest */,
|
||||
37A3B15727255E7F000FB5EE /* Open in Yattee (macOS).appex */,
|
||||
37A3B1792725735F000FB5EE /* Open in Yattee (iOS).appex */,
|
||||
3712643B2865FF4500D77974 /* Shared Tests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -1809,9 +1886,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37BA796C26DC4105002A0235 /* Extensions */,
|
||||
3774122927387B6C00423605 /* InstancesModelTests.swift */,
|
||||
37D4B0E22671614900C925CA /* Tests_macOS.swift */,
|
||||
37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */,
|
||||
);
|
||||
path = "Tests macOS";
|
||||
sourceTree = "<group>";
|
||||
@ -1849,6 +1924,7 @@
|
||||
374C0539272436DA009BDDBE /* SponsorBlock */,
|
||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
||||
37520698285E8DD300CA655F /* Chapter.swift */,
|
||||
371B7E5B27596B8400D21217 /* Comment.swift */,
|
||||
371B7E692759791900D21217 /* CommentsModel.swift */,
|
||||
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
|
||||
@ -1859,6 +1935,7 @@
|
||||
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
||||
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||
3756C2A92861151C00E4B059 /* NetworkStateModel.swift */,
|
||||
37130A5E277657300033018A /* PersistenceController.swift */,
|
||||
376578882685471400D4EA09 /* Playlist.swift */,
|
||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||
@ -1987,6 +2064,23 @@
|
||||
/* End PBXHeadersBuildPhase 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) */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 37A3B17427255E7F000FB5EE /* Build configuration list for PBXNativeTarget "Open in Yattee (macOS)" */;
|
||||
@ -2050,6 +2144,7 @@
|
||||
37FB285527220D9000A57617 /* SDWebImagePINPlugin */,
|
||||
3765917B27237D21009F956E /* PINCache */,
|
||||
37CF8B8328535E4F00B71E37 /* SDWebImage */,
|
||||
37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */,
|
||||
);
|
||||
productName = "Yattee (iOS)";
|
||||
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
||||
@ -2082,6 +2177,7 @@
|
||||
3703206727D2BB45007A0CB8 /* Defaults */,
|
||||
3703206927D2BB49007A0CB8 /* Alamofire */,
|
||||
37CF8B8528535E5A00B71E37 /* SDWebImage */,
|
||||
37A5DBC5285E06B100CA4DD1 /* SwiftUIPager */,
|
||||
);
|
||||
productName = "Yattee (macOS)";
|
||||
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
||||
@ -2186,9 +2282,12 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1310;
|
||||
LastSwiftUpdateCheck = 1400;
|
||||
LastUpgradeCheck = 1400;
|
||||
TargetAttributes = {
|
||||
3712643A2865FF4500D77974 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
};
|
||||
37A3B15627255E7F000FB5EE = {
|
||||
CreatedOnToolsVersion = 13.1;
|
||||
};
|
||||
@ -2248,6 +2347,7 @@
|
||||
37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */,
|
||||
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */,
|
||||
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */,
|
||||
37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
|
||||
);
|
||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -2264,11 +2364,19 @@
|
||||
37D4B0D32671614900C925CA /* Tests (iOS) */,
|
||||
37D4B0DD2671614900C925CA /* Tests (macOS) */,
|
||||
37D4B170267164B000C925CA /* Tests (tvOS) */,
|
||||
3712643A2865FF4500D77974 /* Shared Tests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
371264392865FF4500D77974 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
37A3B15527255E7F000FB5EE /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -2447,6 +2555,17 @@
|
||||
/* End PBXShellScriptBuildPhase 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 */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -2474,15 +2593,17 @@
|
||||
371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||
37E8B0F027B326F30024006F /* Comparable+Clamped.swift in Sources */,
|
||||
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||
372CFD15285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
||||
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
||||
37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||
37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */,
|
||||
378FFBC428660172009E3FBE /* URLParser.swift in Sources */,
|
||||
3784B23D2728B85300B09468 /* ShareButton.swift in Sources */,
|
||||
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */,
|
||||
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||
37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
||||
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||
@ -2524,6 +2645,7 @@
|
||||
37030FF727B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
|
||||
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||
3752069D285E910600CA655F /* ChaptersView.swift in Sources */,
|
||||
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
||||
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||
@ -2531,8 +2653,11 @@
|
||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
378AE940274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||
376BE50927347B5F009AD608 /* SettingsHeader.swift in Sources */,
|
||||
376527BB285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||
3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */,
|
||||
37F4AD2628613B81004D0F66 /* Color+Debug.swift in Sources */,
|
||||
3700155F271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
||||
37F13B62285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
||||
375E45F827B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */,
|
||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */,
|
||||
@ -2560,6 +2685,7 @@
|
||||
37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */,
|
||||
371114EB27B94C8800C2EF7B /* RepeatingTimer.swift in Sources */,
|
||||
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
||||
3756C2A62861131100E4B059 /* NetworkState.swift in Sources */,
|
||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||
@ -2578,9 +2704,11 @@
|
||||
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||
37520699285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||
37F4AD1F28612DFD004D0F66 /* Buffering.swift in Sources */,
|
||||
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||
371B7E5C27596B8400D21217 /* Comment.swift in Sources */,
|
||||
3703100227B0713600ECDDAA /* PlayerGestures.swift in Sources */,
|
||||
@ -2616,6 +2744,7 @@
|
||||
37599F30272B42810087F250 /* FavoriteItem.swift in Sources */,
|
||||
373197D92732015300EF734F /* RelatedView.swift in Sources */,
|
||||
37F9619B27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */,
|
||||
37F4AD1B28612B23004D0F66 /* OpeningStream.swift in Sources */,
|
||||
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||
37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */,
|
||||
@ -2631,6 +2760,7 @@
|
||||
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */,
|
||||
37030FFB27B0398000ECDDAA /* MPVClient.swift in Sources */,
|
||||
3756C2AA2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||
3758638A2721B0A9000CB14E /* ChannelCell.swift in Sources */,
|
||||
37001563271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||
3795593627B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||
@ -2678,7 +2808,9 @@
|
||||
3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */,
|
||||
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
|
||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||
@ -2690,11 +2822,13 @@
|
||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37FB285F272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||
37FD43DC270470B70073EE42 /* InstancesSettings.swift in Sources */,
|
||||
3756C2A72861131100E4B059 /* NetworkState.swift in Sources */,
|
||||
3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */,
|
||||
37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||
3752069A285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||
37484C1A26FC837400287258 /* PlayerSettings.swift in Sources */,
|
||||
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
||||
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||
@ -2704,8 +2838,10 @@
|
||||
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||
378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */,
|
||||
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||
376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
||||
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||
@ -2715,10 +2851,12 @@
|
||||
376BE50727347B57009AD608 /* SettingsHeader.swift in Sources */,
|
||||
378AE93C274EDFB2006A4EE1 /* Backport.swift in Sources */,
|
||||
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||
37F4AD2028612DFD004D0F66 /* Buffering.swift in Sources */,
|
||||
377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */,
|
||||
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||
37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */,
|
||||
37BC50AD2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||
3752069E285E910600CA655F /* ChaptersView.swift in Sources */,
|
||||
37030FF827B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
|
||||
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||
@ -2734,6 +2872,7 @@
|
||||
374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */,
|
||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
||||
37F4AD1C28612B23004D0F66 /* OpeningStream.swift in Sources */,
|
||||
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||
37579D5E27864F5F00FD0B98 /* Help.swift in Sources */,
|
||||
@ -2758,6 +2897,7 @@
|
||||
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */,
|
||||
37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
||||
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */,
|
||||
37F13B63285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
||||
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
|
||||
37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||
@ -2772,6 +2912,7 @@
|
||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||
37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||
@ -2780,6 +2921,7 @@
|
||||
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */,
|
||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
37F4AD2728613B81004D0F66 /* Color+Debug.swift in Sources */,
|
||||
37732FF12703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
||||
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
|
||||
37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||
@ -2824,7 +2966,6 @@
|
||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */,
|
||||
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -2842,32 +2983,25 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3774124C27387D2300423605 /* RecentsModel.swift in Sources */,
|
||||
3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */,
|
||||
371B7E642759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
3774124927387D2300423605 /* Channel.swift in Sources */,
|
||||
3774125727387D2300423605 /* FavoriteItem.swift in Sources */,
|
||||
3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */,
|
||||
3774126027387D2D00423605 /* AccountsBridge.swift in Sources */,
|
||||
3774125827387D2300423605 /* TrendingCategory.swift in Sources */,
|
||||
3774125F27387D2D00423605 /* Account.swift in Sources */,
|
||||
3774126827387D6D00423605 /* Double+Format.swift in Sources */,
|
||||
3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */,
|
||||
3774125627387D2300423605 /* Segment.swift in Sources */,
|
||||
373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||
3774126427387D4A00423605 /* VideosAPI.swift in Sources */,
|
||||
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
|
||||
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */,
|
||||
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */,
|
||||
37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */,
|
||||
3774125427387D2300423605 /* Store.swift in Sources */,
|
||||
3774125027387D2300423605 /* Video.swift in Sources */,
|
||||
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||
3774125327387D2300423605 /* Country.swift in Sources */,
|
||||
3774125E27387D2D00423605 /* InstancesModel.swift in Sources */,
|
||||
37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */,
|
||||
3774127227387E0B00423605 /* SiestaConfiguration.swift in Sources */,
|
||||
3774126D27387D8500423605 /* SponsorBlockAPI.swift in Sources */,
|
||||
3774126327387D2D00423605 /* InstancesBridge.swift in Sources */,
|
||||
3774125127387D2300423605 /* NavigationModel.swift in Sources */,
|
||||
3774124A27387D2300423605 /* ContentItem.swift in Sources */,
|
||||
3774126227387D2D00423605 /* AccountValidator.swift in Sources */,
|
||||
@ -2876,14 +3010,12 @@
|
||||
3774126A27387D6D00423605 /* TypedContentAccessors.swift in Sources */,
|
||||
3774127027387D9A00423605 /* SponsorBlockSegment.swift in Sources */,
|
||||
3774125A27387D2300423605 /* FavoritesModel.swift in Sources */,
|
||||
3774125D27387D2D00423605 /* Instance.swift in Sources */,
|
||||
3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */,
|
||||
3774125527387D2300423605 /* Stream.swift in Sources */,
|
||||
371B7E5F27596B8400D21217 /* Comment.swift in Sources */,
|
||||
3774126F27387D8D00423605 /* SearchQuery.swift in Sources */,
|
||||
3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */,
|
||||
3774125227387D2300423605 /* Thumbnail.swift in Sources */,
|
||||
3774122F27387C7600423605 /* VideosApp.swift in Sources */,
|
||||
37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */,
|
||||
3774126527387D6D00423605 /* Int+Format.swift in Sources */,
|
||||
3774126627387D6D00423605 /* Array+Next.swift in Sources */,
|
||||
@ -2927,7 +3059,9 @@
|
||||
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||
37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */,
|
||||
37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
||||
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
378FFBC628660172009E3FBE /* URLParser.swift in Sources */,
|
||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||
3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||
@ -2937,6 +3071,7 @@
|
||||
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||
378AE93E274EDFB4006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||
37F4AD2828613B81004D0F66 /* Color+Debug.swift in Sources */,
|
||||
37E8B0F227B326F30024006F /* Comparable+Clamped.swift in Sources */,
|
||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||
@ -2946,14 +3081,18 @@
|
||||
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
|
||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
376527BD285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||
371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
|
||||
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
||||
3756C2AC2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||
37A5DBCA285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
||||
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||
3752069B285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||
37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||
3743B86A27216D3600261544 /* ChannelCell.swift in Sources */,
|
||||
3751BA8527E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
|
||||
@ -2981,6 +3120,7 @@
|
||||
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||
3756C2A82861131100E4B059 /* NetworkState.swift in Sources */,
|
||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||
37001561271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
||||
@ -3012,9 +3152,12 @@
|
||||
37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
||||
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */,
|
||||
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||
3752069F285E910600CA655F /* ChaptersView.swift in Sources */,
|
||||
37F4AD1D28612B23004D0F66 /* OpeningStream.swift in Sources */,
|
||||
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
|
||||
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
37C3A24B27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||
37F4AD2128612DFD004D0F66 /* Buffering.swift in Sources */,
|
||||
37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||
37FAE000272ED58000330459 /* EditFavorites.swift in Sources */,
|
||||
37F961A127BD90BB00058149 /* PlayerBackendType.swift in Sources */,
|
||||
@ -3081,6 +3224,48 @@
|
||||
/* End PBXTargetDependency 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 */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@ -3088,7 +3273,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@ -3123,7 +3308,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@ -3156,7 +3341,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@ -3188,7 +3373,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@ -3356,7 +3541,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++14";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@ -3366,6 +3551,7 @@
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Need camera access to take pictures";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||
@ -3401,12 +3587,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Need camera access to take pictures";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||
@ -3442,11 +3629,12 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
@ -3482,11 +3670,12 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
@ -3626,7 +3815,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -3665,7 +3854,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 54;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -3793,6 +3982,15 @@
|
||||
/* End XCBuildConfiguration 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)" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@ -3928,6 +4126,14 @@
|
||||
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" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-log.git";
|
||||
@ -4095,6 +4301,16 @@
|
||||
package = 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "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 */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
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