mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Initial functionality of player items queue
Fix environment objects Hide video player placeholder on tvOS Queue improvements
This commit is contained in:
parent
d6b3c6637d
commit
70c089e696
16
Extensions/View+Borders.swift
Normal file
16
Extensions/View+Borders.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func borderTop(height: Double, color: Color = Color(white: 0.7, opacity: 1)) -> some View {
|
||||||
|
verticalEdgeBorder(.top, height: height, color: color)
|
||||||
|
}
|
||||||
|
|
||||||
|
func borderBottom(height: Double, color: Color = Color(white: 0.7, opacity: 1)) -> some View {
|
||||||
|
verticalEdgeBorder(.bottom, height: height, color: color)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func verticalEdgeBorder(_ edge: Alignment, height: Double, color: Color) -> some View {
|
||||||
|
overlay(Rectangle().frame(width: nil, height: height, alignment: .top).foregroundColor(color), alignment: edge)
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ extension Video {
|
|||||||
let id = "D2sxamzaHkM"
|
let id = "D2sxamzaHkM"
|
||||||
|
|
||||||
return Video(
|
return Video(
|
||||||
id: UUID().uuidString,
|
videoID: UUID().uuidString,
|
||||||
title: "Relaxing Piano Music that will make you feel amazingly good",
|
title: "Relaxing Piano Music that will make you feel amazingly good",
|
||||||
author: "Fancy Videotuber",
|
author: "Fancy Videotuber",
|
||||||
length: 582,
|
length: 582,
|
||||||
|
@ -7,11 +7,11 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
|||||||
.environmentObject(InstancesModel())
|
.environmentObject(InstancesModel())
|
||||||
.environmentObject(api)
|
.environmentObject(api)
|
||||||
.environmentObject(NavigationModel())
|
.environmentObject(NavigationModel())
|
||||||
.environmentObject(PlaybackModel())
|
.environmentObject(player)
|
||||||
.environmentObject(PlaylistsModel())
|
.environmentObject(PlaylistsModel())
|
||||||
.environmentObject(RecentsModel())
|
.environmentObject(RecentsModel())
|
||||||
.environmentObject(SearchModel())
|
.environmentObject(SearchModel())
|
||||||
.environmentObject(SubscriptionsModel(api: api))
|
.environmentObject(subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var api: InvidiousAPI {
|
private var api: InvidiousAPI {
|
||||||
@ -22,6 +22,24 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
|||||||
|
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var player: PlayerModel {
|
||||||
|
let player = PlayerModel()
|
||||||
|
|
||||||
|
player.currentItem = PlayerQueueItem(Video.fixture)
|
||||||
|
player.queue = Video.allFixtures.map { PlayerQueueItem($0) }
|
||||||
|
player.history = player.queue
|
||||||
|
|
||||||
|
return player
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subscriptions: SubscriptionsModel {
|
||||||
|
let subscriptions = SubscriptionsModel()
|
||||||
|
|
||||||
|
subscriptions.channels = Video.allFixtures.map { $0.channel }
|
||||||
|
|
||||||
|
return subscriptions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
@ -3,14 +3,20 @@ import SwiftUI
|
|||||||
|
|
||||||
final class NavigationModel: ObservableObject {
|
final class NavigationModel: ObservableObject {
|
||||||
enum TabSelection: Hashable {
|
enum TabSelection: Hashable {
|
||||||
case watchNow, subscriptions, popular, trending, playlists, channel(String), playlist(String), recentlyOpened(String), search
|
case watchNow
|
||||||
|
case subscriptions
|
||||||
|
case popular
|
||||||
|
case trending
|
||||||
|
case playlists
|
||||||
|
case channel(String)
|
||||||
|
case playlist(String)
|
||||||
|
case recentlyOpened(String)
|
||||||
|
case nowPlaying
|
||||||
|
case search
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var tabSelection: TabSelection! = .watchNow
|
@Published var tabSelection: TabSelection! = .watchNow
|
||||||
|
|
||||||
@Published var showingVideo = false
|
|
||||||
@Published var video: Video?
|
|
||||||
|
|
||||||
@Published var presentingAddToPlaylist = false
|
@Published var presentingAddToPlaylist = false
|
||||||
@Published var videoToAddToPlaylist: Video!
|
@Published var videoToAddToPlaylist: Video!
|
||||||
|
|
||||||
@ -25,11 +31,6 @@ final class NavigationModel: ObservableObject {
|
|||||||
|
|
||||||
@Published var presentingSettings = false
|
@Published var presentingSettings = false
|
||||||
|
|
||||||
func playVideo(_ video: Video) {
|
|
||||||
self.video = video
|
|
||||||
showingVideo = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var tabSelectionBinding: Binding<TabSelection> {
|
var tabSelectionBinding: Binding<TabSelection> {
|
||||||
Binding<TabSelection>(
|
Binding<TabSelection>(
|
||||||
get: {
|
get: {
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import CoreMedia
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
final class PlaybackModel: ObservableObject {
|
|
||||||
@Published var live = false
|
|
||||||
@Published var stream: Stream?
|
|
||||||
@Published var time: CMTime?
|
|
||||||
|
|
||||||
var aspectRatio: Double? {
|
|
||||||
let tracks = stream?.videoAsset.tracks(withMediaType: .video)
|
|
||||||
|
|
||||||
guard tracks != nil else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let size: CGSize! = tracks!.first.flatMap {
|
|
||||||
tracks!.isEmpty ? nil : $0.naturalSize.applying($0.preferredTransform)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard size != nil else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return size.width / size.height
|
|
||||||
}
|
|
||||||
|
|
||||||
func reset() {
|
|
||||||
stream = nil
|
|
||||||
time = nil
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
import AVFoundation
|
import AVKit
|
||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
@ -8,127 +9,114 @@ import Logging
|
|||||||
final class PlayerModel: ObservableObject {
|
final class PlayerModel: ObservableObject {
|
||||||
let logger = Logger(label: "net.arekf.Pearvidious.ps")
|
let logger = Logger(label: "net.arekf.Pearvidious.ps")
|
||||||
|
|
||||||
var video: Video!
|
private(set) var player = AVPlayer()
|
||||||
|
var controller: PlayerViewController?
|
||||||
|
#if os(tvOS)
|
||||||
|
var avPlayerViewController: AVPlayerViewController?
|
||||||
|
#endif
|
||||||
|
|
||||||
var player: AVPlayer!
|
@Published var presentingPlayer = false
|
||||||
|
|
||||||
private var compositions = [Stream: AVMutableComposition]()
|
@Published var stream: Stream?
|
||||||
|
@Published var currentRate: Float?
|
||||||
|
|
||||||
private(set) var savedTime: CMTime?
|
@Published var queue = [PlayerQueueItem]()
|
||||||
|
@Published var currentItem: PlayerQueueItem!
|
||||||
|
@Published var live = false
|
||||||
|
@Published var time: CMTime?
|
||||||
|
|
||||||
private(set) var currentRate: Float = 0.0
|
@Published var history = [PlayerQueueItem]()
|
||||||
static let availableRates: [Double] = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
|
|
||||||
|
|
||||||
var api: InvidiousAPI
|
var api: InvidiousAPI
|
||||||
var playback: PlaybackModel
|
|
||||||
var timeObserver: Any?
|
var timeObserver: Any?
|
||||||
|
|
||||||
let resolution: Stream.ResolutionSetting?
|
private var statusObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
var playingOutsideViewController = false
|
var isPlaying: Bool {
|
||||||
|
stream != nil && currentRate != 0.0
|
||||||
init(_ video: Video? = nil, playback: PlaybackModel, api: InvidiousAPI, resolution: Stream.ResolutionSetting? = nil) {
|
|
||||||
self.video = video
|
|
||||||
self.playback = playback
|
|
||||||
self.api = api
|
|
||||||
self.resolution = resolution
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
init(api: InvidiousAPI? = nil) {
|
||||||
destroyPlayer()
|
self.api = api ?? InvidiousAPI()
|
||||||
|
addItemDidPlayToEndTimeObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadVideo(_ video: Video?) {
|
func presentPlayer() {
|
||||||
guard video != nil else {
|
presentingPlayer = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func togglePlay() {
|
||||||
|
isPlaying ? pause() : play()
|
||||||
|
}
|
||||||
|
|
||||||
|
func play() {
|
||||||
|
guard !isPlaying else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
playback.reset()
|
player.play()
|
||||||
|
|
||||||
loadExtendedVideoDetails(video) { video in
|
|
||||||
self.video = video
|
|
||||||
self.playVideo(video)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadExtendedVideoDetails(_ video: Video?, onSuccess: @escaping (Video) -> Void) {
|
func pause() {
|
||||||
guard video != nil else {
|
guard isPlaying else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
api.video(video!.id).load().onSuccess { response in
|
player.pause()
|
||||||
if let video: Video = response.typedContent() {
|
|
||||||
onSuccess(video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestedResolution: Bool {
|
func playVideo(_ video: Video) {
|
||||||
resolution != nil && resolution != .hd720pFirstThenBest
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func playVideo(_ video: Video) {
|
|
||||||
playback.live = video.live
|
|
||||||
|
|
||||||
if video.live {
|
if video.live {
|
||||||
playHlsUrl()
|
playHlsUrl(video)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let stream = requestedResolution ? video.streamWithResolution(resolution!.value) : video.defaultStream
|
guard let stream = video.streamWithResolution(Defaults[.quality].value) ?? video.defaultStream else {
|
||||||
|
|
||||||
guard stream != nil else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
if stream.oneMeaningfullAsset {
|
||||||
await self.loadStream(stream!)
|
playStream(stream, for: video)
|
||||||
|
} else {
|
||||||
if resolution == .hd720pFirstThenBest {
|
Task {
|
||||||
await self.loadBestStream()
|
await playComposition(video, for: stream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func playHlsUrl() {
|
private func playHlsUrl(_ video: Video) {
|
||||||
player.replaceCurrentItem(with: playerItemWithMetadata())
|
player.replaceCurrentItem(with: playerItemWithMetadata(video))
|
||||||
player.playImmediately(atRate: 1.0)
|
player.playImmediately(atRate: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func loadStream(_ stream: Stream) async {
|
private func playStream(_ stream: Stream, for video: Video) {
|
||||||
if stream.oneMeaningfullAsset {
|
|
||||||
playStream(stream)
|
|
||||||
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
await playComposition(for: stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func playStream(_ stream: Stream) {
|
|
||||||
guard player != nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warning("loading \(stream.description) to player")
|
logger.warning("loading \(stream.description) to player")
|
||||||
|
|
||||||
|
let playerItem: AVPlayerItem! = playerItemWithMetadata(video, for: stream)
|
||||||
|
guard playerItem != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let index = queue.firstIndex(where: { $0.video.id == video.id }) {
|
||||||
|
queue[index].playerItems.append(playerItem)
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.saveTime()
|
self.stream = stream
|
||||||
self.player?.replaceCurrentItem(with: self.playerItemWithMetadata(for: stream))
|
self.player.replaceCurrentItem(with: playerItem)
|
||||||
self.playback.stream = stream
|
}
|
||||||
if self.timeObserver.isNil {
|
|
||||||
self.addTimeObserver()
|
if timeObserver.isNil {
|
||||||
}
|
addTimeObserver()
|
||||||
self.player?.play()
|
|
||||||
self.seekToSavedTime()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func playComposition(for stream: Stream) async {
|
private func playComposition(_ video: Video, for stream: Stream) async {
|
||||||
async let assetAudioTrack = stream.audioAsset.loadTracks(withMediaType: .audio)
|
async let assetAudioTrack = stream.audioAsset.loadTracks(withMediaType: .audio)
|
||||||
async let assetVideoTrack = stream.videoAsset.loadTracks(withMediaType: .video)
|
async let assetVideoTrack = stream.videoAsset.loadTracks(withMediaType: .video)
|
||||||
|
|
||||||
if let audioTrack = composition(for: stream).addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid),
|
logger.info("loading audio track")
|
||||||
|
if let audioTrack = composition(video, for: stream)?.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid),
|
||||||
let assetTrack = try? await assetAudioTrack.first
|
let assetTrack = try? await assetAudioTrack.first
|
||||||
{
|
{
|
||||||
try! audioTrack.insertTimeRange(
|
try! audioTrack.insertTimeRange(
|
||||||
@ -138,10 +126,11 @@ final class PlayerModel: ObservableObject {
|
|||||||
)
|
)
|
||||||
logger.critical("audio loaded")
|
logger.critical("audio loaded")
|
||||||
} else {
|
} else {
|
||||||
fatalError("no track")
|
logger.critical("NO audio track")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let videoTrack = composition(for: stream).addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid),
|
logger.info("loading video track")
|
||||||
|
if let videoTrack = composition(video, for: stream)?.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid),
|
||||||
let assetTrack = try? await assetVideoTrack.first
|
let assetTrack = try? await assetVideoTrack.first
|
||||||
{
|
{
|
||||||
try! videoTrack.insertTimeRange(
|
try! videoTrack.insertTimeRange(
|
||||||
@ -150,27 +139,35 @@ final class PlayerModel: ObservableObject {
|
|||||||
at: .zero
|
at: .zero
|
||||||
)
|
)
|
||||||
logger.critical("video loaded")
|
logger.critical("video loaded")
|
||||||
|
playStream(stream, for: video)
|
||||||
playStream(stream)
|
|
||||||
} else {
|
} else {
|
||||||
fatalError("no track")
|
logger.critical("NO video track")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func playerItem(for stream: Stream? = nil) -> AVPlayerItem {
|
private func playerItem(_ video: Video, for stream: Stream? = nil) -> AVPlayerItem? {
|
||||||
if stream != nil {
|
if stream != nil {
|
||||||
if stream!.oneMeaningfullAsset {
|
if stream!.oneMeaningfullAsset {
|
||||||
return AVPlayerItem(asset: stream!.videoAsset, automaticallyLoadedAssetKeys: [.isPlayable])
|
logger.info("stream has one meaningfull asset")
|
||||||
|
return AVPlayerItem(asset: AVURLAsset(url: stream!.videoAsset.url))
|
||||||
|
}
|
||||||
|
if let composition = composition(video, for: stream!) {
|
||||||
|
logger.info("stream has MANY assets, using composition")
|
||||||
|
return AVPlayerItem(asset: composition)
|
||||||
} else {
|
} else {
|
||||||
return AVPlayerItem(asset: composition(for: stream!))
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AVPlayerItem(url: video.hlsUrl!)
|
return AVPlayerItem(url: video.hlsUrl!)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func playerItemWithMetadata(for stream: Stream? = nil) -> AVPlayerItem {
|
private func playerItemWithMetadata(_ video: Video, for stream: Stream? = nil) -> AVPlayerItem? {
|
||||||
let playerItemWithMetadata = playerItem(for: stream)
|
logger.info("building player item metadata")
|
||||||
|
let playerItemWithMetadata: AVPlayerItem! = playerItem(video, for: stream)
|
||||||
|
guard playerItemWithMetadata != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var externalMetadata = [
|
var externalMetadata = [
|
||||||
makeMetadataItem(.commonIdentifierTitle, value: video.title),
|
makeMetadataItem(.commonIdentifierTitle, value: video.title),
|
||||||
@ -179,7 +176,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
]
|
]
|
||||||
|
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
if let thumbnailData = try? Data(contentsOf: video.thumbnailURL(quality: .high)!),
|
if let thumbnailData = try? Data(contentsOf: video.thumbnailURL(quality: .medium)!),
|
||||||
let image = UIImage(data: thumbnailData),
|
let image = UIImage(data: thumbnailData),
|
||||||
let pngData = image.pngData()
|
let pngData = image.pngData()
|
||||||
{
|
{
|
||||||
@ -190,92 +187,69 @@ final class PlayerModel: ObservableObject {
|
|||||||
playerItemWithMetadata.externalMetadata = externalMetadata
|
playerItemWithMetadata.externalMetadata = externalMetadata
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
playerItemWithMetadata.preferredForwardBufferDuration = 10
|
playerItemWithMetadata.preferredForwardBufferDuration = 15
|
||||||
|
|
||||||
|
statusObservation?.invalidate()
|
||||||
|
statusObservation = playerItemWithMetadata.observe(\.status, options: [.old, .new]) { playerItem, _ in
|
||||||
|
switch playerItem.status {
|
||||||
|
case .readyToPlay:
|
||||||
|
if self.isAutoplaying(playerItem) {
|
||||||
|
self.player.play()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("item metadata retrieved")
|
||||||
return playerItemWithMetadata
|
return playerItemWithMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPlayerRate(_ rate: Float) {
|
func addItemDidPlayToEndTimeObserver() {
|
||||||
currentRate = rate
|
NotificationCenter.default.addObserver(
|
||||||
player.rate = rate
|
self,
|
||||||
|
selector: #selector(itemDidPlayToEndTime),
|
||||||
|
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func composition(for stream: Stream) -> AVMutableComposition {
|
@objc func itemDidPlayToEndTime() {
|
||||||
if compositions[stream].isNil {
|
if queue.isEmpty {
|
||||||
compositions[stream] = AVMutableComposition()
|
resetQueue()
|
||||||
}
|
#if os(tvOS)
|
||||||
|
avPlayerViewController!.dismiss(animated: true) {
|
||||||
return compositions[stream]!
|
self.controller!.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
fileprivate func loadBestStream() async {
|
presentingPlayer = false
|
||||||
if let bestStream = video.bestStream {
|
} else {
|
||||||
await loadStream(bestStream)
|
advanceToNextItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func saveTime() {
|
private func composition(_ video: Video, for stream: Stream) -> AVMutableComposition? {
|
||||||
guard player != nil else {
|
if let index = queue.firstIndex(where: { $0.video == video }) {
|
||||||
return
|
if queue[index].compositions[stream].isNil {
|
||||||
|
queue[index].compositions[stream] = AVMutableComposition()
|
||||||
|
}
|
||||||
|
return queue[index].compositions[stream]!
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentTime = player.currentTime()
|
return nil
|
||||||
|
|
||||||
guard currentTime.seconds > 0 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
savedTime = currentTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func seekToSavedTime() {
|
private func addTimeObserver() {
|
||||||
guard player != nil else {
|
let interval = CMTime(seconds: 0.5, preferredTimescale: 1000)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let time = savedTime {
|
|
||||||
logger.info("seeking to \(time.seconds)")
|
|
||||||
player.seek(to: time, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func destroyPlayer() {
|
|
||||||
logger.critical("destroying player")
|
|
||||||
|
|
||||||
guard !playingOutsideViewController else {
|
|
||||||
logger.critical("cannot destroy, playing outside view controller")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
player?.currentItem?.tracks.forEach { $0.assetTrack?.asset?.cancelLoading() }
|
|
||||||
|
|
||||||
player?.replaceCurrentItem(with: nil)
|
|
||||||
|
|
||||||
if timeObserver != nil {
|
|
||||||
player?.removeTimeObserver(timeObserver!)
|
|
||||||
timeObserver = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
player = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func addTimeObserver() {
|
|
||||||
let interval = CMTime(value: 1, timescale: 1)
|
|
||||||
|
|
||||||
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
|
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
|
||||||
guard self.player != nil else {
|
self.currentRate = self.player.rate
|
||||||
return
|
self.live = self.currentVideo?.live ?? false
|
||||||
}
|
self.time = self.player.currentTime()
|
||||||
|
|
||||||
if self.player.rate != self.currentRate, self.player.rate != 0, self.currentRate != 0 {
|
|
||||||
self.player.rate = self.currentRate
|
|
||||||
}
|
|
||||||
|
|
||||||
self.playback.time = self.player.currentTime()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func makeMetadataItem(_ identifier: AVMetadataIdentifier, value: Any) -> AVMetadataItem {
|
private func makeMetadataItem(_ identifier: AVMetadataIdentifier, value: Any) -> AVMetadataItem {
|
||||||
let item = AVMutableMetadataItem()
|
let item = AVMutableMetadataItem()
|
||||||
|
|
||||||
item.identifier = identifier
|
item.identifier = identifier
|
||||||
|
145
Model/PlayerQueue.swift
Normal file
145
Model/PlayerQueue.swift
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension PlayerModel {
|
||||||
|
var currentVideo: Video? {
|
||||||
|
currentItem?.video
|
||||||
|
}
|
||||||
|
|
||||||
|
func playAll(_ videos: [Video]) {
|
||||||
|
let first = videos.first
|
||||||
|
|
||||||
|
videos.forEach { video in
|
||||||
|
enqueueVideo(video) { _, item in
|
||||||
|
if item.video == first {
|
||||||
|
self.advanceToItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func playNext(_ video: Video) {
|
||||||
|
enqueueVideo(video, prepending: true) { _, item in
|
||||||
|
if self.currentItem == nil {
|
||||||
|
self.advanceToItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func playNow(_ video: Video) {
|
||||||
|
addCurrentItemToHistory()
|
||||||
|
|
||||||
|
enqueueVideo(video, prepending: true) { _, item in
|
||||||
|
self.advanceToItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func playItem(_ item: PlayerQueueItem, video: Video? = nil) {
|
||||||
|
currentItem = item
|
||||||
|
|
||||||
|
if video != nil {
|
||||||
|
currentItem.video = video!
|
||||||
|
}
|
||||||
|
|
||||||
|
playVideo(currentItem.video)
|
||||||
|
}
|
||||||
|
|
||||||
|
func advanceToNextItem() {
|
||||||
|
addCurrentItemToHistory()
|
||||||
|
|
||||||
|
if let nextItem = queue.first {
|
||||||
|
advanceToItem(nextItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func advanceToItem(_ newItem: PlayerQueueItem) {
|
||||||
|
let item = remove(newItem)!
|
||||||
|
loadDetails(newItem.video) { video in
|
||||||
|
self.playItem(item, video: video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult func remove(_ item: PlayerQueueItem) -> PlayerQueueItem? {
|
||||||
|
if let index = queue.firstIndex(where: { $0 == item }) {
|
||||||
|
return queue.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetQueue() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.currentItem = nil
|
||||||
|
self.stream = nil
|
||||||
|
self.removeQueueItems()
|
||||||
|
self.timeObserver = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
player.replaceCurrentItem(with: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAutoplaying(_ item: AVPlayerItem) -> Bool {
|
||||||
|
player.currentItem == item
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult func enqueueVideo(
|
||||||
|
_ video: Video,
|
||||||
|
play: Bool = false,
|
||||||
|
prepending: Bool = false,
|
||||||
|
videoDetailsLoadHandler: @escaping (Video, PlayerQueueItem) -> Void = { _, _ in }
|
||||||
|
) -> PlayerQueueItem? {
|
||||||
|
let item = PlayerQueueItem(video)
|
||||||
|
|
||||||
|
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
||||||
|
|
||||||
|
loadDetails(video) { video in
|
||||||
|
videoDetailsLoadHandler(video, item)
|
||||||
|
|
||||||
|
if play {
|
||||||
|
self.playItem(item, video: video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadDetails(_ video: Video?, onSuccess: @escaping (Video) -> Void) {
|
||||||
|
guard video != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !video!.streams.isEmpty {
|
||||||
|
logger.critical("not loading video details again")
|
||||||
|
onSuccess(video!)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.video(video!.videoID).load().onSuccess { response in
|
||||||
|
if let video: Video = response.typedContent() {
|
||||||
|
onSuccess(video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCurrentItemToHistory() {
|
||||||
|
if let item = currentItem, !history.contains(where: { $0.video.videoID == item.video.videoID }) {
|
||||||
|
history.insert(item, at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult func removeHistory(_ item: PlayerQueueItem) -> PlayerQueueItem? {
|
||||||
|
if let index = history.firstIndex(where: { $0 == item }) {
|
||||||
|
return history.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeQueueItems() {
|
||||||
|
queue.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeHistoryItems() {
|
||||||
|
history.removeAll()
|
||||||
|
}
|
||||||
|
}
|
14
Model/PlayerQueueItem.swift
Normal file
14
Model/PlayerQueueItem.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PlayerQueueItem: Hashable, Identifiable {
|
||||||
|
var id = UUID()
|
||||||
|
var video: Video
|
||||||
|
|
||||||
|
init(_ video: Video) {
|
||||||
|
self.video = video
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerItems = [AVPlayerItem]()
|
||||||
|
var compositions = [Stream: AVMutableComposition]()
|
||||||
|
}
|
@ -84,7 +84,7 @@ class Stream: Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var oneMeaningfullAsset: Bool {
|
var oneMeaningfullAsset: Bool {
|
||||||
assets.dropFirst().allSatisfy { $0 == assets.first }
|
assets.dropFirst().allSatisfy { $0.url == assets.first!.url }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: Stream, rhs: Stream) -> Bool {
|
static func == (lhs: Stream, rhs: Stream) -> Bool {
|
||||||
|
@ -3,8 +3,9 @@ import AVKit
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
struct Video: Identifiable, Equatable {
|
struct Video: Identifiable, Equatable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
|
let videoID: String
|
||||||
var title: String
|
var title: String
|
||||||
var thumbnails: [Thumbnail]
|
var thumbnails: [Thumbnail]
|
||||||
var author: String
|
var author: String
|
||||||
@ -31,7 +32,8 @@ struct Video: Identifiable, Equatable {
|
|||||||
var channel: Channel
|
var channel: Channel
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: String,
|
id: String? = nil,
|
||||||
|
videoID: String,
|
||||||
title: String,
|
title: String,
|
||||||
author: String,
|
author: String,
|
||||||
length: TimeInterval,
|
length: TimeInterval,
|
||||||
@ -49,7 +51,8 @@ struct Video: Identifiable, Equatable {
|
|||||||
dislikes: Int? = nil,
|
dislikes: Int? = nil,
|
||||||
keywords: [String] = []
|
keywords: [String] = []
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id ?? UUID().uuidString
|
||||||
|
self.videoID = videoID
|
||||||
self.title = title
|
self.title = title
|
||||||
self.author = author
|
self.author = author
|
||||||
self.length = length
|
self.length = length
|
||||||
@ -69,7 +72,7 @@ struct Video: Identifiable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(_ json: JSON) {
|
init(_ json: JSON) {
|
||||||
let videoID = json["videoId"].stringValue
|
videoID = json["videoId"].stringValue
|
||||||
|
|
||||||
if let id = json["indexId"].string {
|
if let id = json["indexId"].string {
|
||||||
indexID = id
|
indexID = id
|
||||||
@ -206,4 +209,8 @@ struct Video: Identifiable, Equatable {
|
|||||||
static func == (lhs: Video, rhs: Video) -> Bool {
|
static func == (lhs: Video, rhs: Video) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,10 @@
|
|||||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||||
372915E82687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
372915E82687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||||
|
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */; };
|
||||||
|
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||||
|
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||||
|
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||||
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
|
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
|
||||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
|
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
|
||||||
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
|
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
|
||||||
@ -66,6 +70,14 @@
|
|||||||
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
||||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
||||||
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
|
||||||
|
3743CA4A270EF79400E4D32B /* SwiftUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3743CA49270EF79400E4D32B /* SwiftUIKit */; };
|
||||||
|
3743CA4C270EF7A500E4D32B /* SwiftUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3743CA4B270EF7A500E4D32B /* SwiftUIKit */; };
|
||||||
|
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
||||||
|
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
||||||
|
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
|
||||||
|
3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA51270F284F00E4D32B /* View+Borders.swift */; };
|
||||||
|
3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA51270F284F00E4D32B /* View+Borders.swift */; };
|
||||||
|
3743CA54270F284F00E4D32B /* View+Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA51270F284F00E4D32B /* View+Borders.swift */; };
|
||||||
3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; };
|
3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; };
|
||||||
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; };
|
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; };
|
||||||
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; };
|
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; };
|
||||||
@ -193,9 +205,6 @@
|
|||||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFE26D2CA3700675966 /* VideoDetails.swift */; };
|
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFE26D2CA3700675966 /* VideoDetails.swift */; };
|
||||||
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
||||||
37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
||||||
37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
|
||||||
37B81B0626D2CEDA00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
|
||||||
37B81B0726D2D6CF00675966 /* PlaybackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackModel.swift */; };
|
|
||||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
||||||
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
||||||
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
|
||||||
@ -245,6 +254,15 @@
|
|||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
|
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
|
||||||
|
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 */; };
|
||||||
|
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */; };
|
||||||
|
37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */; };
|
||||||
|
37CC3F4E270CFE1700608308 /* PlayerQueueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */; };
|
||||||
|
37CC3F50270D010D00608308 /* VideoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F4F270D010D00608308 /* VideoBanner.swift */; };
|
||||||
|
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F4F270D010D00608308 /* VideoBanner.swift */; };
|
||||||
|
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC3F4F270D010D00608308 /* VideoBanner.swift */; };
|
||||||
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
||||||
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
||||||
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */; };
|
||||||
@ -266,6 +284,9 @@
|
|||||||
37D4B19826717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
37D4B19826717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
||||||
37D4B19926717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
37D4B19926717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
||||||
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 37D4B19C2671817900C925CA /* SwiftyJSON */; };
|
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 37D4B19C2671817900C925CA /* SwiftyJSON */; };
|
||||||
|
37E2EEAB270656EC00170416 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */; };
|
||||||
|
37E2EEAC270656EC00170416 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */; };
|
||||||
|
37E2EEAD270656EC00170416 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */; };
|
||||||
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||||
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||||
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
|
||||||
@ -335,10 +356,14 @@
|
|||||||
37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
|
||||||
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
|
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
|
||||||
|
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; };
|
||||||
|
37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; };
|
||||||
373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; };
|
373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; };
|
||||||
373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; };
|
373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; };
|
||||||
373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; };
|
373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; };
|
||||||
373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToPlaylistView.swift; sourceTree = "<group>"; };
|
373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToPlaylistView.swift; sourceTree = "<group>"; };
|
||||||
|
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueRow.swift; sourceTree = "<group>"; };
|
||||||
|
3743CA51270F284F00E4D32B /* View+Borders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Borders.swift"; sourceTree = "<group>"; };
|
||||||
3748186526A7627F0084E870 /* Video+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+Fixtures.swift"; sourceTree = "<group>"; };
|
3748186526A7627F0084E870 /* Video+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail+Fixtures.swift"; sourceTree = "<group>"; };
|
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = "<group>"; };
|
3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = "<group>"; };
|
||||||
@ -384,7 +409,6 @@
|
|||||||
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = "<group>"; };
|
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = "<group>"; };
|
||||||
37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; };
|
37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; };
|
||||||
37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = "<group>"; };
|
37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = "<group>"; };
|
||||||
37B81B0426D2CEDA00675966 /* PlaybackModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackModel.swift; sourceTree = "<group>"; };
|
|
||||||
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVideosView.swift; sourceTree = "<group>"; };
|
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVideosView.swift; sourceTree = "<group>"; };
|
||||||
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelVideosView.swift; sourceTree = "<group>"; };
|
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelVideosView.swift; sourceTree = "<group>"; };
|
||||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsModel.swift; sourceTree = "<group>"; };
|
37BA794226DBA973002A0235 /* PlaylistsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsModel.swift; sourceTree = "<group>"; };
|
||||||
@ -404,6 +428,9 @@
|
|||||||
37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
|
37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
|
||||||
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentsModel.swift; sourceTree = "<group>"; };
|
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentsModel.swift; sourceTree = "<group>"; };
|
||||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
||||||
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = "<group>"; };
|
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = "<group>"; };
|
||||||
37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = "<group>"; };
|
37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = "<group>"; };
|
||||||
37D4B0C22671614700C925CA /* PearvidiousApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearvidiousApp.swift; sourceTree = "<group>"; };
|
37D4B0C22671614700C925CA /* PearvidiousApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearvidiousApp.swift; sourceTree = "<group>"; };
|
||||||
@ -422,6 +449,7 @@
|
|||||||
37D4B18B26717B3800C925CA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = "<group>"; };
|
37D4B18B26717B3800C925CA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = "<group>"; };
|
||||||
37D4B19626717E1500C925CA /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; };
|
37D4B19626717E1500C925CA /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; };
|
||||||
37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
|
||||||
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = "<group>"; };
|
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = "<group>"; };
|
||||||
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
|
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
|
||||||
37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
|
37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
|
||||||
@ -445,6 +473,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
3743CA4A270EF79400E4D32B /* SwiftUIKit in Frameworks */,
|
||||||
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */,
|
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */,
|
||||||
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */,
|
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */,
|
||||||
377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */,
|
377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */,
|
||||||
@ -459,6 +488,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */,
|
37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */,
|
||||||
|
3743CA4C270EF7A500E4D32B /* SwiftUIKit in Frameworks */,
|
||||||
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */,
|
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */,
|
||||||
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */,
|
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */,
|
||||||
37BD07C02698AC97003EBB87 /* Siesta in Frameworks */,
|
37BD07C02698AC97003EBB87 /* Siesta in Frameworks */,
|
||||||
@ -522,6 +552,8 @@
|
|||||||
children = (
|
children = (
|
||||||
37B81B0126D2CAE700675966 /* PlaybackBar.swift */,
|
37B81B0126D2CAE700675966 /* PlaybackBar.swift */,
|
||||||
37BE0BD226A1D4780092E2DB /* Player.swift */,
|
37BE0BD226A1D4780092E2DB /* Player.swift */,
|
||||||
|
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */,
|
||||||
|
37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */,
|
||||||
37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */,
|
37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */,
|
||||||
37B81AFE26D2CA3700675966 /* VideoDetails.swift */,
|
37B81AFE26D2CA3700675966 /* VideoDetails.swift */,
|
||||||
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */,
|
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */,
|
||||||
@ -554,6 +586,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
3748186D26A769D60084E870 /* DetailBadge.swift */,
|
3748186D26A769D60084E870 /* DetailBadge.swift */,
|
||||||
|
37CC3F4F270D010D00608308 /* VideoBanner.swift */,
|
||||||
37A9965926D6F8CA006E3224 /* VideosCellsHorizontal.swift */,
|
37A9965926D6F8CA006E3224 /* VideosCellsHorizontal.swift */,
|
||||||
37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */,
|
37F4AE7126828F0900BD60EA /* VideosCellsVertical.swift */,
|
||||||
37D4B18B26717B3800C925CA /* VideoView.swift */,
|
37D4B18B26717B3800C925CA /* VideoView.swift */,
|
||||||
@ -566,6 +599,7 @@
|
|||||||
children = (
|
children = (
|
||||||
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */,
|
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */,
|
||||||
37152EE926EFEB95004FB96D /* LazyView.swift */,
|
37152EE926EFEB95004FB96D /* LazyView.swift */,
|
||||||
|
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */,
|
||||||
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */,
|
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */,
|
||||||
37AAF27D26737323007FC770 /* PopularView.swift */,
|
37AAF27D26737323007FC770 /* PopularView.swift */,
|
||||||
37AAF27F26737550007FC770 /* SearchView.swift */,
|
37AAF27F26737550007FC770 /* SearchView.swift */,
|
||||||
@ -670,6 +704,7 @@
|
|||||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
||||||
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
||||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
||||||
|
3743CA51270F284F00E4D32B /* View+Borders.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -749,6 +784,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
37666BA927023AF000F869E5 /* AccountSelectionView.swift */,
|
37666BA927023AF000F869E5 /* AccountSelectionView.swift */,
|
||||||
|
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */,
|
||||||
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */,
|
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */,
|
||||||
37D4B15E267164AF00C925CA /* Assets.xcassets */,
|
37D4B15E267164AF00C925CA /* Assets.xcassets */,
|
||||||
37D4B1AE26729DEB00C925CA /* Info.plist */,
|
37D4B1AE26729DEB00C925CA /* Info.plist */,
|
||||||
@ -767,15 +803,16 @@
|
|||||||
37D4B1B72672CFE300C925CA /* Model */ = {
|
37D4B1B72672CFE300C925CA /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
37484C3026FCB8F900287258 /* AccountValidator.swift */,
|
||||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||||
37141672267A8E10006CA35D /* Country.swift */,
|
37141672267A8E10006CA35D /* Country.swift */,
|
||||||
378E50FA26FE8B9F00F49626 /* Instance.swift */,
|
378E50FA26FE8B9F00F49626 /* Instance.swift */,
|
||||||
37484C3026FCB8F900287258 /* AccountValidator.swift */,
|
|
||||||
375DFB5726F9DA010013F468 /* InstancesModel.swift */,
|
375DFB5726F9DA010013F468 /* InstancesModel.swift */,
|
||||||
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
||||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||||
37B81B0426D2CEDA00675966 /* PlaybackModel.swift */,
|
|
||||||
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
||||||
|
37319F0427103F94004ECCD0 /* PlayerQueue.swift */,
|
||||||
|
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */,
|
||||||
376578882685471400D4EA09 /* Playlist.swift */,
|
376578882685471400D4EA09 /* Playlist.swift */,
|
||||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||||
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
||||||
@ -832,6 +869,7 @@
|
|||||||
37D4B0C52671614900C925CA /* Sources */,
|
37D4B0C52671614900C925CA /* Sources */,
|
||||||
37D4B0C62671614900C925CA /* Frameworks */,
|
37D4B0C62671614900C925CA /* Frameworks */,
|
||||||
37D4B0C72671614900C925CA /* Resources */,
|
37D4B0C72671614900C925CA /* Resources */,
|
||||||
|
37CC3F48270CE89B00608308 /* ShellScript */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -845,6 +883,7 @@
|
|||||||
37BD07B82698AB2E003EBB87 /* Siesta */,
|
37BD07B82698AB2E003EBB87 /* Siesta */,
|
||||||
37BD07C62698B27B003EBB87 /* Introspect */,
|
37BD07C62698B27B003EBB87 /* Introspect */,
|
||||||
37BADCA42699FB72009BE4FB /* Alamofire */,
|
37BADCA42699FB72009BE4FB /* Alamofire */,
|
||||||
|
3743CA49270EF79400E4D32B /* SwiftUIKit */,
|
||||||
);
|
);
|
||||||
productName = "Pearvidious (iOS)";
|
productName = "Pearvidious (iOS)";
|
||||||
productReference = 37D4B0C92671614900C925CA /* Pearvidious.app */;
|
productReference = 37D4B0C92671614900C925CA /* Pearvidious.app */;
|
||||||
@ -857,6 +896,7 @@
|
|||||||
37D4B0CB2671614900C925CA /* Sources */,
|
37D4B0CB2671614900C925CA /* Sources */,
|
||||||
37D4B0CC2671614900C925CA /* Frameworks */,
|
37D4B0CC2671614900C925CA /* Frameworks */,
|
||||||
37D4B0CD2671614900C925CA /* Resources */,
|
37D4B0CD2671614900C925CA /* Resources */,
|
||||||
|
37CC3F4A270CE8D000608308 /* ShellScript */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -869,6 +909,7 @@
|
|||||||
37BD07BD2698AC96003EBB87 /* Defaults */,
|
37BD07BD2698AC96003EBB87 /* Defaults */,
|
||||||
37BD07BF2698AC97003EBB87 /* Siesta */,
|
37BD07BF2698AC97003EBB87 /* Siesta */,
|
||||||
37BADCA6269A552E009BE4FB /* Alamofire */,
|
37BADCA6269A552E009BE4FB /* Alamofire */,
|
||||||
|
3743CA4B270EF7A500E4D32B /* SwiftUIKit */,
|
||||||
);
|
);
|
||||||
productName = "Pearvidious (macOS)";
|
productName = "Pearvidious (macOS)";
|
||||||
productReference = 37D4B0CF2671614900C925CA /* Pearvidious.app */;
|
productReference = 37D4B0CF2671614900C925CA /* Pearvidious.app */;
|
||||||
@ -917,6 +958,7 @@
|
|||||||
37D4B154267164AE00C925CA /* Sources */,
|
37D4B154267164AE00C925CA /* Sources */,
|
||||||
37D4B155267164AE00C925CA /* Frameworks */,
|
37D4B155267164AE00C925CA /* Frameworks */,
|
||||||
37D4B156267164AE00C925CA /* Resources */,
|
37D4B156267164AE00C925CA /* Resources */,
|
||||||
|
37CC3F49270CE8CA00608308 /* ShellScript */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -1011,6 +1053,7 @@
|
|||||||
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */,
|
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */,
|
||||||
37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||||
37BADCA32699FB72009BE4FB /* XCRemoteSwiftPackageReference "Alamofire" */,
|
37BADCA32699FB72009BE4FB /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||||
|
3743CA48270EF79400E4D32B /* XCRemoteSwiftPackageReference "SwiftUIKit" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -1086,6 +1129,57 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
37CC3F48270CE89B00608308 /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||||
|
};
|
||||||
|
37CC3F49270CE8CA00608308 /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||||
|
};
|
||||||
|
37CC3F4A270CE8D000608308 /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||||
|
};
|
||||||
37FD43EA2704A2350073EE42 /* ShellScript */ = {
|
37FD43EA2704A2350073EE42 /* ShellScript */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -1137,8 +1231,10 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
|
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||||
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
||||||
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
|
3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */,
|
||||||
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
@ -1165,18 +1261,19 @@
|
|||||||
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||||
37FD43DE2704717F0073EE42 /* DefaultAccountHint.swift in Sources */,
|
37FD43DE2704717F0073EE42 /* DefaultAccountHint.swift in Sources */,
|
||||||
|
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */,
|
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */,
|
||||||
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */,
|
|
||||||
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||||
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
|
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
|
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
|
||||||
|
37E2EEAB270656EC00170416 /* PlayerControlsView.swift in Sources */,
|
||||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||||
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
@ -1187,7 +1284,9 @@
|
|||||||
37484C2D26FC844700287258 /* AccountsSettingsView.swift in Sources */,
|
37484C2D26FC844700287258 /* AccountsSettingsView.swift in Sources */,
|
||||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||||
|
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
|
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
||||||
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||||
379775932689365600DD52A8 /* Array+Next.swift in Sources */,
|
379775932689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||||
@ -1212,6 +1311,7 @@
|
|||||||
37484C2926FC83FF00287258 /* AccountFormView.swift in Sources */,
|
37484C2926FC83FF00287258 /* AccountFormView.swift in Sources */,
|
||||||
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||||
|
37CC3F50270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||||
378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
37484C1D26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
37484C1D26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||||
37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||||
@ -1227,6 +1327,7 @@
|
|||||||
files = (
|
files = (
|
||||||
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
||||||
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */,
|
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */,
|
||||||
|
3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */,
|
||||||
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||||
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||||
@ -1236,11 +1337,13 @@
|
|||||||
37FD43DF2704717F0073EE42 /* DefaultAccountHint.swift in Sources */,
|
37FD43DF2704717F0073EE42 /* DefaultAccountHint.swift in Sources */,
|
||||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||||
|
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
||||||
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
37FD43DC270470B70073EE42 /* InstancesSettingsView.swift in Sources */,
|
37FD43DC270470B70073EE42 /* InstancesSettingsView.swift in Sources */,
|
||||||
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
|
37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||||
37484C1A26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
37484C1A26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||||
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
||||||
@ -1254,10 +1357,11 @@
|
|||||||
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */,
|
377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */,
|
||||||
|
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||||
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
37B81B0626D2CEDA00675966 /* PlaybackModel.swift in Sources */,
|
|
||||||
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
|
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
|
3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
|
37E2EEAC270656EC00170416 /* PlayerControlsView.swift in Sources */,
|
||||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
||||||
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||||
@ -1281,6 +1385,7 @@
|
|||||||
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
|
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
|
||||||
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
|
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
@ -1288,6 +1393,7 @@
|
|||||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
37732FF12703A26300F04329 /* ValidationStatusView.swift in Sources */,
|
37732FF12703A26300F04329 /* ValidationStatusView.swift in Sources */,
|
||||||
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
|
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
|
||||||
|
37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||||
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
||||||
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
|
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
|
||||||
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||||
@ -1333,6 +1439,7 @@
|
|||||||
37AAF28026737550007FC770 /* SearchView.swift in Sources */,
|
37AAF28026737550007FC770 /* SearchView.swift in Sources */,
|
||||||
3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||||
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
|
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||||
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||||
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
@ -1348,11 +1455,11 @@
|
|||||||
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||||
|
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
||||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
37B81B0726D2D6CF00675966 /* PlaybackModel.swift in Sources */,
|
|
||||||
37732FF22703A26300F04329 /* ValidationStatusView.swift in Sources */,
|
37732FF22703A26300F04329 /* ValidationStatusView.swift in Sources */,
|
||||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
||||||
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
@ -1362,18 +1469,23 @@
|
|||||||
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
||||||
|
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */,
|
||||||
37D4B18E26717B3800C925CA /* VideoView.swift in Sources */,
|
37D4B18E26717B3800C925CA /* VideoView.swift in Sources */,
|
||||||
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||||
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
||||||
|
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||||
37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
|
37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||||
37BA795126DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
37BA795126DC3E0E002A0235 /* Int+Format.swift in Sources */,
|
||||||
3748186C26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186C26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||||
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||||
|
3743CA54270F284F00E4D32B /* View+Borders.swift in Sources */,
|
||||||
371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
|
37E2EEAD270656EC00170416 /* PlayerControlsView.swift in Sources */,
|
||||||
37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||||
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||||
@ -1399,6 +1511,7 @@
|
|||||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
37484C1B26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
37484C1B26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||||
372915E82687E3B900F5A35B /* Defaults.swift in Sources */,
|
372915E82687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||||
|
37CC3F4E270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||||
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */,
|
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */,
|
||||||
3797758D2689345500DD52A8 /* Store.swift in Sources */,
|
3797758D2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
37484C2F26FC844700287258 /* AccountsSettingsView.swift in Sources */,
|
37484C2F26FC844700287258 /* AccountsSettingsView.swift in Sources */,
|
||||||
@ -2086,6 +2199,14 @@
|
|||||||
minimumVersion = 5.0.0;
|
minimumVersion = 5.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
3743CA48270EF79400E4D32B /* XCRemoteSwiftPackageReference "SwiftUIKit" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/danielsaidi/SwiftUIKit.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
|
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
||||||
@ -2134,6 +2255,16 @@
|
|||||||
package = 372915E22687E33E00F5A35B /* XCRemoteSwiftPackageReference "Defaults" */;
|
package = 372915E22687E33E00F5A35B /* XCRemoteSwiftPackageReference "Defaults" */;
|
||||||
productName = Defaults;
|
productName = Defaults;
|
||||||
};
|
};
|
||||||
|
3743CA49270EF79400E4D32B /* SwiftUIKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 3743CA48270EF79400E4D32B /* XCRemoteSwiftPackageReference "SwiftUIKit" */;
|
||||||
|
productName = SwiftUIKit;
|
||||||
|
};
|
||||||
|
3743CA4B270EF7A500E4D32B /* SwiftUIKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 3743CA48270EF79400E4D32B /* XCRemoteSwiftPackageReference "SwiftUIKit" */;
|
||||||
|
productName = SwiftUIKit;
|
||||||
|
};
|
||||||
377FC7D4267A080300A6BBAF /* SwiftyJSON */ = {
|
377FC7D4267A080300A6BBAF /* SwiftyJSON */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
|
package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
|
||||||
|
@ -46,6 +46,15 @@
|
|||||||
"version": "0.1.3"
|
"version": "0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "SwiftUIKit",
|
||||||
|
"repositoryURL": "https://github.com/danielsaidi/SwiftUIKit.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "ad509355ba9bc87f8375a297c3df93acd42e6c01",
|
||||||
|
"version": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "SwiftyJSON",
|
"package": "SwiftyJSON",
|
||||||
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
|
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "display-p3",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.757",
|
||||||
|
"green" : "0.761",
|
||||||
|
"red" : "0.757"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "display-p3",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.259",
|
||||||
|
"green" : "0.259",
|
||||||
|
"red" : "0.259"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct AppSidebarPlaylists: View {
|
struct AppSidebarPlaylists: View {
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -15,6 +16,11 @@ struct AppSidebarPlaylists: View {
|
|||||||
}
|
}
|
||||||
.id(playlist.id)
|
.id(playlist.id)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
|
Button("Add to queue...") {
|
||||||
|
playlists.find(id: playlist.id)?.videos.forEach { video in
|
||||||
|
player.enqueueVideo(video)
|
||||||
|
}
|
||||||
|
}
|
||||||
Button("Edit") {
|
Button("Edit") {
|
||||||
navigation.presentEditPlaylistForm(playlists.find(id: playlist.id))
|
navigation.presentEditPlaylistForm(playlists.find(id: playlist.id))
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,14 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
@StateObject private var api = InvidiousAPI()
|
||||||
|
@StateObject private var instances = InstancesModel()
|
||||||
@StateObject private var navigation = NavigationModel()
|
@StateObject private var navigation = NavigationModel()
|
||||||
@StateObject private var playback = PlaybackModel()
|
@StateObject private var player = PlayerModel()
|
||||||
|
@StateObject private var playlists = PlaylistsModel()
|
||||||
@StateObject private var recents = RecentsModel()
|
@StateObject private var recents = RecentsModel()
|
||||||
|
@StateObject private var search = SearchModel()
|
||||||
|
@StateObject private var subscriptions = SubscriptionsModel()
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@ -24,34 +29,62 @@ struct ContentView: View {
|
|||||||
TVNavigationView()
|
TVNavigationView()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.onAppear(perform: configureAPI)
|
||||||
|
.environmentObject(api)
|
||||||
|
.environmentObject(instances)
|
||||||
.environmentObject(navigation)
|
.environmentObject(navigation)
|
||||||
.environmentObject(playback)
|
.environmentObject(player)
|
||||||
|
.environmentObject(playlists)
|
||||||
.environmentObject(recents)
|
.environmentObject(recents)
|
||||||
#if !os(tvOS)
|
.environmentObject(search)
|
||||||
.sheet(isPresented: $navigation.showingVideo) {
|
.environmentObject(subscriptions)
|
||||||
if let video = navigation.video {
|
#if os(iOS)
|
||||||
VideoPlayerView(video)
|
.fullScreenCover(isPresented: $player.presentingPlayer) {
|
||||||
.environmentObject(playback)
|
VideoPlayerView()
|
||||||
|
.environmentObject(api)
|
||||||
#if !os(iOS)
|
.environmentObject(navigation)
|
||||||
.frame(minWidth: 550, minHeight: 720)
|
.environmentObject(player)
|
||||||
.onExitCommand {
|
.environmentObject(subscriptions)
|
||||||
navigation.showingVideo = false
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
.sheet(isPresented: $player.presentingPlayer) {
|
||||||
|
VideoPlayerView()
|
||||||
|
.frame(minWidth: 900, minHeight: 800)
|
||||||
|
.environmentObject(api)
|
||||||
|
.environmentObject(navigation)
|
||||||
|
.environmentObject(player)
|
||||||
|
.environmentObject(subscriptions)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if !os(tvOS)
|
||||||
.sheet(isPresented: $navigation.presentingAddToPlaylist) {
|
.sheet(isPresented: $navigation.presentingAddToPlaylist) {
|
||||||
AddToPlaylistView(video: navigation.videoToAddToPlaylist)
|
AddToPlaylistView(video: navigation.videoToAddToPlaylist)
|
||||||
|
.environmentObject(api)
|
||||||
|
.environmentObject(playlists)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $navigation.presentingPlaylistForm) {
|
.sheet(isPresented: $navigation.presentingPlaylistForm) {
|
||||||
PlaylistFormView(playlist: $navigation.editedPlaylist)
|
PlaylistFormView(playlist: $navigation.editedPlaylist)
|
||||||
|
.environmentObject(api)
|
||||||
|
.environmentObject(playlists)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $navigation.presentingSettings) {
|
.sheet(isPresented: $navigation.presentingSettings) {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
|
.environmentObject(api)
|
||||||
|
.environmentObject(instances)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureAPI() {
|
||||||
|
if let account = instances.defaultAccount, api.account.isEmpty {
|
||||||
|
api.setAccount(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.api = api
|
||||||
|
playlists.api = api
|
||||||
|
search.api = api
|
||||||
|
subscriptions.api = api
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
|
@ -3,21 +3,9 @@ import SwiftUI
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
struct PearvidiousApp: App {
|
struct PearvidiousApp: App {
|
||||||
@StateObject private var api = InvidiousAPI()
|
|
||||||
@StateObject private var instances = InstancesModel()
|
|
||||||
@StateObject private var playlists = PlaylistsModel()
|
|
||||||
@StateObject private var search = SearchModel()
|
|
||||||
@StateObject private var subscriptions = SubscriptionsModel()
|
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
.onAppear(perform: configureAPI)
|
|
||||||
.environmentObject(api)
|
|
||||||
.environmentObject(instances)
|
|
||||||
.environmentObject(playlists)
|
|
||||||
.environmentObject(search)
|
|
||||||
.environmentObject(subscriptions)
|
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.commands {
|
.commands {
|
||||||
@ -28,20 +16,9 @@ struct PearvidiousApp: App {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Settings {
|
Settings {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
.onAppear(perform: configureAPI)
|
.environmentObject(InvidiousAPI())
|
||||||
.environmentObject(api)
|
.environmentObject(InstancesModel())
|
||||||
.environmentObject(instances)
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func configureAPI() {
|
|
||||||
playlists.api = api
|
|
||||||
search.api = api
|
|
||||||
subscriptions.api = api
|
|
||||||
|
|
||||||
if let account = instances.defaultAccount, api.account.isEmpty {
|
|
||||||
api.setAccount(account)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,55 +2,59 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PlaybackBar: View {
|
struct PlaybackBar: View {
|
||||||
let video: Video
|
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@EnvironmentObject private var playback: PlaybackModel
|
@Environment(\.inNavigationView) private var inNavigationView
|
||||||
|
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
closeButton
|
closeButton
|
||||||
.frame(width: 60, alignment: .leading)
|
.frame(width: 80, alignment: .leading)
|
||||||
|
|
||||||
Text(playbackStatus)
|
if player.currentItem != nil {
|
||||||
.foregroundColor(.gray)
|
Text(playbackStatus)
|
||||||
.font(.caption2)
|
.foregroundColor(.gray)
|
||||||
.frame(minWidth: 60, maxWidth: .infinity)
|
.font(.caption2)
|
||||||
|
.frame(minWidth: 130, maxWidth: .infinity)
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
if playback.stream != nil {
|
if player.stream != nil {
|
||||||
Text(currentStreamString)
|
Text(currentStreamString)
|
||||||
} else {
|
|
||||||
if video.live {
|
|
||||||
Image(systemName: "dot.radiowaves.left.and.right")
|
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "bolt.horizontal.fill")
|
if player.currentVideo!.live {
|
||||||
|
Image(systemName: "dot.radiowaves.left.and.right")
|
||||||
|
} else {
|
||||||
|
Image(systemName: "bolt.horizontal.fill")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.caption2)
|
||||||
|
.frame(width: 80, alignment: .trailing)
|
||||||
|
.fixedSize(horizontal: true, vertical: true)
|
||||||
|
} else {
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
.foregroundColor(.gray)
|
|
||||||
.font(.caption2)
|
|
||||||
.frame(width: 60, alignment: .trailing)
|
|
||||||
.fixedSize(horizontal: true, vertical: true)
|
|
||||||
}
|
}
|
||||||
.padding(4)
|
.padding(4)
|
||||||
.background(.black)
|
.background(.black)
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentStreamString: String {
|
var currentStreamString: String {
|
||||||
playback.stream != nil ? "\(playback.stream!.resolution.height)p" : ""
|
"\(player.stream!.resolution.height)p"
|
||||||
}
|
}
|
||||||
|
|
||||||
var playbackStatus: String {
|
var playbackStatus: String {
|
||||||
guard playback.time != nil else {
|
if player.live {
|
||||||
if playback.live {
|
return "LIVE"
|
||||||
return "LIVE"
|
|
||||||
} else {
|
|
||||||
return "loading..."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let remainingSeconds = video.length - playback.time!.seconds
|
guard player.time != nil, player.time!.isValid else {
|
||||||
|
return "loading..."
|
||||||
|
}
|
||||||
|
|
||||||
|
let remainingSeconds = player.currentVideo!.length - player.time!.seconds
|
||||||
|
|
||||||
if remainingSeconds < 60 {
|
if remainingSeconds < 60 {
|
||||||
return "less than a minute"
|
return "less than a minute"
|
||||||
@ -59,12 +63,15 @@ struct PlaybackBar: View {
|
|||||||
let timeFinishAt = Date.now.addingTimeInterval(remainingSeconds)
|
let timeFinishAt = Date.now.addingTimeInterval(remainingSeconds)
|
||||||
let timeFinishAtString = timeFinishAt.formatted(date: .omitted, time: .shortened)
|
let timeFinishAtString = timeFinishAt.formatted(date: .omitted, time: .shortened)
|
||||||
|
|
||||||
return "finishes at \(timeFinishAtString)"
|
return "ends at \(timeFinishAtString)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var closeButton: some View {
|
var closeButton: some View {
|
||||||
Button(action: { dismiss() }) {
|
Button {
|
||||||
Image(systemName: "xmark.circle.fill")
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Label("Close", systemImage: inNavigationView ? "chevron.backward.circle.fill" : "chevron.down.circle.fill")
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(Text("Close"))
|
.accessibilityLabel(Text("Close"))
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
|
@ -3,15 +3,23 @@ import SwiftUI
|
|||||||
|
|
||||||
struct Player: UIViewControllerRepresentable {
|
struct Player: UIViewControllerRepresentable {
|
||||||
@EnvironmentObject<InvidiousAPI> private var api
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
@EnvironmentObject<PlaybackModel> private var playback
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
var video: Video?
|
var controller: PlayerViewController?
|
||||||
|
|
||||||
|
init(controller: PlayerViewController? = nil) {
|
||||||
|
self.controller = controller
|
||||||
|
}
|
||||||
|
|
||||||
func makeUIViewController(context _: Context) -> PlayerViewController {
|
func makeUIViewController(context _: Context) -> PlayerViewController {
|
||||||
|
if self.controller != nil {
|
||||||
|
return self.controller!
|
||||||
|
}
|
||||||
|
|
||||||
let controller = PlayerViewController()
|
let controller = PlayerViewController()
|
||||||
|
|
||||||
controller.video = video
|
player.controller = controller
|
||||||
controller.playback = playback
|
controller.playerModel = player
|
||||||
controller.api = api
|
controller.api = api
|
||||||
|
|
||||||
controller.resolution = Defaults[.quality]
|
controller.resolution = Defaults[.quality]
|
||||||
|
37
Shared/Player/PlayerQueueRow.swift
Normal file
37
Shared/Player/PlayerQueueRow.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PlayerQueueRow: View {
|
||||||
|
let item: PlayerQueueItem
|
||||||
|
var history = false
|
||||||
|
@Binding var fullScreen: Bool
|
||||||
|
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
Button {
|
||||||
|
player.addCurrentItemToHistory()
|
||||||
|
|
||||||
|
if history {
|
||||||
|
let newItem = player.enqueueVideo(item.video, prepending: true)
|
||||||
|
player.advanceToItem(newItem!)
|
||||||
|
if let historyItemIndex = player.history.firstIndex(of: item) {
|
||||||
|
player.history.remove(at: historyItemIndex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
player.advanceToItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullScreen {
|
||||||
|
withAnimation {
|
||||||
|
fullScreen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
VideoBanner(video: item.video)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
Shared/Player/PlayerQueueView.swift
Normal file
100
Shared/Player/PlayerQueueView.swift
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PlayerQueueView: View {
|
||||||
|
@Binding var fullScreen: Bool
|
||||||
|
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
playingNext
|
||||||
|
playedPreviously
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
.listStyle(.groupedWithInsets)
|
||||||
|
#elseif os(iOS)
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#else
|
||||||
|
.listStyle(.plain)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var playingNext: some View {
|
||||||
|
Section(header: Text("Playing Next")) {
|
||||||
|
if player.queue.isEmpty {
|
||||||
|
Text("Playback queue is empty")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(player.queue) { item in
|
||||||
|
PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
||||||
|
.contextMenu {
|
||||||
|
removeButton(item, history: false)
|
||||||
|
removeAllButton(history: false)
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
|
removeButton(item, history: false)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var playedPreviously: some View {
|
||||||
|
Section(header: Text("Played Previously")) {
|
||||||
|
if player.history.isEmpty {
|
||||||
|
Text("History is empty")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(player.history) { item in
|
||||||
|
PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen)
|
||||||
|
.contextMenu {
|
||||||
|
removeButton(item, history: true)
|
||||||
|
removeAllButton(history: true)
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
|
removeButton(item, history: true)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
if history {
|
||||||
|
player.removeHistory(item)
|
||||||
|
} else {
|
||||||
|
player.remove(item)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Remove", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAllButton(history: Bool) -> some View {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
if history {
|
||||||
|
player.removeHistoryItems()
|
||||||
|
} else {
|
||||||
|
player.removeQueueItems()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Remove All", systemImage: "trash.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PlayerQueueView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
VStack {
|
||||||
|
PlayerQueueView(fullScreen: .constant(true))
|
||||||
|
}
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,12 @@ import Logging
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class PlayerViewController: UIViewController {
|
final class PlayerViewController: UIViewController {
|
||||||
var video: Video!
|
|
||||||
|
|
||||||
var api: InvidiousAPI!
|
var api: InvidiousAPI!
|
||||||
var playerLoaded = false
|
var playerLoaded = false
|
||||||
var player = AVPlayer()
|
|
||||||
var playerModel: PlayerModel!
|
var playerModel: PlayerModel!
|
||||||
var playback: PlaybackModel!
|
|
||||||
var playerViewController = AVPlayerViewController()
|
var playerViewController = AVPlayerViewController()
|
||||||
var resolution: Stream.ResolutionSetting!
|
var resolution: Stream.ResolutionSetting!
|
||||||
|
var shouldResume = false
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
@ -22,61 +19,42 @@ final class PlayerViewController: UIViewController {
|
|||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidDisappear(_ animated: Bool) {
|
|
||||||
#if os(iOS)
|
|
||||||
if !playerModel.playingOutsideViewController {
|
|
||||||
playerViewController.player?.replaceCurrentItem(with: nil)
|
|
||||||
playerViewController.player = nil
|
|
||||||
|
|
||||||
try? AVAudioSession.sharedInstance().setActive(false)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
super.viewDidDisappear(animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadPlayer() {
|
func loadPlayer() {
|
||||||
playerModel = PlayerModel(playback: playback, api: api, resolution: resolution)
|
|
||||||
|
|
||||||
guard !playerLoaded else {
|
guard !playerLoaded else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
playerModel.player = player
|
playerModel.controller = self
|
||||||
playerViewController.player = playerModel.player
|
playerViewController.player = playerModel.player
|
||||||
playerModel.loadVideo(video)
|
playerViewController.allowsPictureInPicturePlayback = true
|
||||||
|
playerViewController.delegate = self
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
|
playerModel.avPlayerViewController = playerViewController
|
||||||
|
playerViewController.customInfoViewControllers = [playerQueueInfoViewController]
|
||||||
present(playerViewController, animated: false)
|
present(playerViewController, animated: false)
|
||||||
|
|
||||||
addItemDidPlayToEndTimeObserver()
|
|
||||||
#else
|
#else
|
||||||
embedViewController()
|
embedViewController()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
playerViewController.allowsPictureInPicturePlayback = true
|
|
||||||
playerViewController.delegate = self
|
|
||||||
playerLoaded = true
|
playerLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
func addItemDidPlayToEndTimeObserver() {
|
var playerQueueInfoViewController: UIHostingController<AnyView> {
|
||||||
NotificationCenter.default.addObserver(
|
let controller = UIHostingController(rootView:
|
||||||
self,
|
AnyView(
|
||||||
selector: #selector(itemDidPlayToEndTime),
|
NowPlayingView(infoViewController: true)
|
||||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
.environmentObject(playerModel)
|
||||||
object: nil
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@objc func itemDidPlayToEndTime() {
|
controller.title = "Playing Next"
|
||||||
playerViewController.dismiss(animated: true) {
|
|
||||||
self.dismiss(animated: false)
|
return controller
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
func embedViewController() {
|
func embedViewController() {
|
||||||
playerViewController.exitsFullScreenWhenPlaybackEnds = true
|
|
||||||
playerViewController.view.frame = view.bounds
|
playerViewController.view.frame = view.bounds
|
||||||
|
|
||||||
addChild(playerViewController)
|
addChild(playerViewController)
|
||||||
@ -96,17 +74,22 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func playerViewControllerWillBeginDismissalTransition(_: AVPlayerViewController) {
|
||||||
|
shouldResume = playerModel.isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) {
|
func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) {
|
||||||
playerModel.playingOutsideViewController = false
|
if shouldResume {
|
||||||
|
playerModel.player.play()
|
||||||
|
}
|
||||||
|
|
||||||
dismiss(animated: false)
|
dismiss(animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerViewController(
|
func playerViewController(
|
||||||
_: AVPlayerViewController,
|
_: AVPlayerViewController,
|
||||||
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||||
) {
|
) {}
|
||||||
playerModel.playingOutsideViewController = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func playerViewController(
|
func playerViewController(
|
||||||
_: AVPlayerViewController,
|
_: AVPlayerViewController,
|
||||||
@ -114,8 +97,6 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
) {
|
) {
|
||||||
coordinator.animate(alongsideTransition: nil) { context in
|
coordinator.animate(alongsideTransition: nil) { context in
|
||||||
if !context.isCancelled {
|
if !context.isCancelled {
|
||||||
self.playerModel.playingOutsideViewController = false
|
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if self.traitCollection.verticalSizeClass == .compact {
|
if self.traitCollection.verticalSizeClass == .compact {
|
||||||
self.dismiss(animated: true)
|
self.dismiss(animated: true)
|
||||||
@ -125,11 +106,7 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
|
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {}
|
||||||
playerModel.playingOutsideViewController = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {
|
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {}
|
||||||
playerModel.playingOutsideViewController = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,160 +2,305 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoDetails: View {
|
struct VideoDetails: View {
|
||||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
enum Page {
|
||||||
|
case details, queue
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binding var sidebarQueue: Bool
|
||||||
|
@Binding var fullScreen: Bool
|
||||||
|
|
||||||
@State private var subscribed = false
|
@State private var subscribed = false
|
||||||
@State private var confirmationShown = false
|
@State private var confirmationShown = false
|
||||||
|
|
||||||
var video: Video
|
@State private var currentPage = Page.details
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
|
init(
|
||||||
|
sidebarQueue: Binding<Bool>? = nil,
|
||||||
|
fullScreen: Binding<Bool>? = nil
|
||||||
|
) {
|
||||||
|
_sidebarQueue = sidebarQueue ?? .constant(true)
|
||||||
|
_fullScreen = fullScreen ?? .constant(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var video: Video? {
|
||||||
|
player.currentItem?.video
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(video.title)
|
Group {
|
||||||
.font(.title2.bold())
|
Group {
|
||||||
.padding(.bottom, 0)
|
HStack(spacing: 0) {
|
||||||
|
title
|
||||||
|
|
||||||
Divider()
|
toggleFullScreenDetailsButton
|
||||||
|
|
||||||
HStack(alignment: .center) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
if subscribed {
|
|
||||||
Image(systemName: "star.circle.fill")
|
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading) {
|
#if os(macOS)
|
||||||
Text(video.channel.name)
|
.padding(.top, 10)
|
||||||
.font(.system(size: 13))
|
#endif
|
||||||
.bold()
|
|
||||||
if let subscribers = video.channel.subscriptionsString {
|
if !video.isNil {
|
||||||
Text("\(subscribers) subscribers")
|
Divider()
|
||||||
.font(.caption2)
|
}
|
||||||
|
|
||||||
|
subscriptionsSection
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
if !video.isNil, !sidebarQueue {
|
||||||
|
pagePicker
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onSwipeGesture(
|
||||||
|
up: {
|
||||||
|
withAnimation {
|
||||||
|
fullScreen = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: {
|
||||||
|
withAnimation {
|
||||||
|
if fullScreen {
|
||||||
|
fullScreen = false
|
||||||
|
} else {
|
||||||
|
self.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundColor(.secondary)
|
)
|
||||||
|
|
||||||
Spacer()
|
switch currentPage {
|
||||||
|
case .details:
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
detailsPage
|
||||||
|
}
|
||||||
|
case .queue:
|
||||||
|
PlayerQueueView(fullScreen: $fullScreen)
|
||||||
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
guard video != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
subscribed = subscriptions.isSubscribing(video!.channel.id)
|
||||||
if subscribed {
|
}
|
||||||
Button("Unsubscribe") {
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
confirmationShown = true
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
|
||||||
.tint(.gray)
|
var title: some View {
|
||||||
|
Group {
|
||||||
|
if video != nil {
|
||||||
|
Text(video!.title)
|
||||||
|
.onAppear {
|
||||||
|
#if !os(macOS)
|
||||||
|
currentPage = .details
|
||||||
#endif
|
#endif
|
||||||
.confirmationDialog("Are you you want to unsubscribe from \(video.channel.name)?", isPresented: $confirmationShown) {
|
}
|
||||||
|
|
||||||
|
.font(.title2.bold())
|
||||||
|
} else {
|
||||||
|
Text("Not playing")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.onAppear {
|
||||||
|
#if !os(macOS)
|
||||||
|
currentPage = .queue
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toggleFullScreenDetailsButton: some View {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
fullScreen.toggle()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Resize", systemImage: fullScreen ? "chevron.down" : "chevron.up")
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
}
|
||||||
|
.help("Toggle fullscreen details")
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.keyboardShortcut("t")
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriptionsSection: some View {
|
||||||
|
Group {
|
||||||
|
if video != nil {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
if subscribed {
|
||||||
|
Image(systemName: "star.circle.fill")
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(video!.channel.name)
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.bold()
|
||||||
|
if let subscribers = video!.channel.subscriptionsString {
|
||||||
|
Text("\(subscribers) subscribers")
|
||||||
|
.font(.caption2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Section {
|
||||||
|
if subscribed {
|
||||||
Button("Unsubscribe") {
|
Button("Unsubscribe") {
|
||||||
subscriptions.unsubscribe(video.channel.id)
|
confirmationShown = true
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.tint(.gray)
|
||||||
|
#endif
|
||||||
|
.confirmationDialog("Are you you want to unsubscribe from \(video!.channel.name)?", isPresented: $confirmationShown) {
|
||||||
|
Button("Unsubscribe") {
|
||||||
|
subscriptions.unsubscribe(video!.channel.id)
|
||||||
|
|
||||||
|
withAnimation {
|
||||||
|
subscribed.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button("Subscribe") {
|
||||||
|
subscriptions.subscribe(video!.channel.id)
|
||||||
|
|
||||||
withAnimation {
|
withAnimation {
|
||||||
subscribed.toggle()
|
subscribed.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.tint(.blue)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Button("Subscribe") {
|
.font(.system(size: 13))
|
||||||
subscriptions.subscribe(video.channel.id)
|
.buttonStyle(.borderless)
|
||||||
|
.buttonBorderShape(.roundedRectangle)
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
withAnimation {
|
var pagePicker: some View {
|
||||||
subscribed.toggle()
|
Picker("Page", selection: $currentPage) {
|
||||||
}
|
Text("Details").tag(Page.details)
|
||||||
|
Text("Queue").tag(Page.queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.onDisappear {
|
||||||
|
currentPage = .details
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishedDateSection: some View {
|
||||||
|
Group {
|
||||||
|
if let video = player.currentItem.video {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
if let published = video.publishedDate {
|
||||||
|
Text(published)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let publishedAt = video.publishedAt {
|
||||||
|
if video.publishedDate != nil {
|
||||||
|
Text("•")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.opacity(0.3)
|
||||||
}
|
}
|
||||||
.tint(.blue)
|
Text(publishedAt.formatted(date: .abbreviated, time: .omitted))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 12))
|
||||||
.buttonStyle(.borderless)
|
.padding(.bottom, -1)
|
||||||
.buttonBorderShape(.roundedRectangle)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.padding(.bottom, -1)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Divider()
|
var countsSection: some View {
|
||||||
|
Group {
|
||||||
|
if let video = player.currentItem.video {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
if let views = video.viewsCount {
|
||||||
if let published = video.publishedDate {
|
videoDetail(label: "Views", value: views, symbol: "eye.fill")
|
||||||
Text(published)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let publishedAt = video.publishedAt {
|
|
||||||
if video.publishedDate != nil {
|
|
||||||
Text("•")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.opacity(0.3)
|
|
||||||
}
|
}
|
||||||
Text(publishedAt.formatted(date: .abbreviated, time: .omitted))
|
|
||||||
|
if let likes = video.likesCount {
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup.circle.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let dislikes = video.dislikesCount {
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown.circle.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.frame(maxHeight: 35)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.font(.system(size: 12))
|
}
|
||||||
.padding(.bottom, -1)
|
}
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Divider()
|
var detailsPage: some View {
|
||||||
|
Group {
|
||||||
|
if let video = player.currentItem?.video {
|
||||||
|
Group {
|
||||||
|
publishedDateSection
|
||||||
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if let views = video.viewsCount {
|
|
||||||
videoDetail(label: "Views", value: views, symbol: "eye.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let likes = video.likesCount {
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup.circle.fill")
|
countsSection
|
||||||
}
|
}
|
||||||
|
|
||||||
if let dislikes = video.dislikesCount {
|
Divider()
|
||||||
Divider()
|
|
||||||
|
|
||||||
videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown.circle.fill")
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(maxHeight: 35)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
ScrollView(.vertical) {
|
|
||||||
Text(video.description)
|
Text(video.description)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 100, alignment: .leading)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
Text(video.description)
|
|
||||||
.font(.caption)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
|
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(video.keywords, id: \.self) { keyword in
|
ForEach(video.keywords, id: \.self) { keyword in
|
||||||
HStack(alignment: .center, spacing: 0) {
|
HStack(alignment: .center, spacing: 0) {
|
||||||
Text("#")
|
Text("#")
|
||||||
.font(.system(size: 11).bold())
|
.font(.system(size: 11).bold())
|
||||||
|
|
||||||
Text(keyword)
|
Text(keyword)
|
||||||
.frame(maxWidth: 500)
|
.frame(maxWidth: 500)
|
||||||
}.foregroundColor(.white)
|
}
|
||||||
.padding(.vertical, 4)
|
.font(.caption)
|
||||||
.padding(.horizontal, 8)
|
.foregroundColor(.white)
|
||||||
|
.padding(.vertical, 4)
|
||||||
.background(Color("VideoDetailLikesSymbolColor"))
|
.padding(.horizontal, 8)
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.background(Color("VideoDetailLikesSymbolColor"))
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
.font(.caption)
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, 10)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
.padding(.horizontal)
|
||||||
.padding([.horizontal, .bottom])
|
|
||||||
.onAppear {
|
|
||||||
subscribed = subscriptions.isSubscribing(video.channel.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoDetail(label: String, value: String, symbol: String) -> some View {
|
func videoDetail(label: String, value: String, symbol: String) -> some View {
|
||||||
@ -185,7 +330,7 @@ struct VideoDetails: View {
|
|||||||
|
|
||||||
struct VideoDetails_Previews: PreviewProvider {
|
struct VideoDetails_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VideoDetails(video: Video.fixture)
|
VideoDetails(sidebarQueue: .constant(false))
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,32 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoDetailsPaddingModifier: ViewModifier {
|
struct VideoDetailsPaddingModifier: ViewModifier {
|
||||||
|
static var defaultAdditionalDetailsPadding: Double {
|
||||||
|
#if os(macOS)
|
||||||
|
20
|
||||||
|
#else
|
||||||
|
35
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
let geometry: GeometryProxy
|
let geometry: GeometryProxy
|
||||||
let aspectRatio: Double?
|
let aspectRatio: Double?
|
||||||
let minimumHeightLeft: Double
|
let minimumHeightLeft: Double
|
||||||
let additionalPadding: Double
|
let additionalPadding: Double
|
||||||
|
let fullScreen: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
geometry: GeometryProxy,
|
geometry: GeometryProxy,
|
||||||
aspectRatio: Double? = nil,
|
aspectRatio: Double? = nil,
|
||||||
minimumHeightLeft: Double? = nil,
|
minimumHeightLeft: Double? = nil,
|
||||||
additionalPadding: Double = 35.00
|
additionalPadding: Double? = nil,
|
||||||
|
fullScreen: Bool = false
|
||||||
) {
|
) {
|
||||||
self.geometry = geometry
|
self.geometry = geometry
|
||||||
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
|
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
|
||||||
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
|
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
|
||||||
self.additionalPadding = additionalPadding
|
self.additionalPadding = additionalPadding ?? VideoDetailsPaddingModifier.defaultAdditionalDetailsPadding
|
||||||
|
self.fullScreen = fullScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
var usedAspectRatio: Double {
|
var usedAspectRatio: Double {
|
||||||
@ -32,7 +43,7 @@ struct VideoDetailsPaddingModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var topPadding: Double {
|
var topPadding: Double {
|
||||||
playerHeight + additionalPadding
|
fullScreen ? 0 : (playerHeight + additionalPadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
@ -2,7 +2,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoPlayerSizeModifier: ViewModifier {
|
struct VideoPlayerSizeModifier: ViewModifier {
|
||||||
let geometry: GeometryProxy
|
let geometry: GeometryProxy!
|
||||||
let aspectRatio: Double?
|
let aspectRatio: Double?
|
||||||
let minimumHeightLeft: Double
|
let minimumHeightLeft: Double
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
init(
|
init(
|
||||||
geometry: GeometryProxy,
|
geometry: GeometryProxy? = nil,
|
||||||
aspectRatio: Double? = nil,
|
aspectRatio: Double? = nil,
|
||||||
minimumHeightLeft: Double? = nil
|
minimumHeightLeft: Double? = nil
|
||||||
) {
|
) {
|
||||||
@ -21,10 +21,15 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
// TODO: verify if optional GeometryProxy is still used
|
||||||
.frame(maxHeight: maxHeight)
|
if geometry != nil {
|
||||||
.aspectRatio(usedAspectRatio, contentMode: usedAspectRatioContentMode)
|
content
|
||||||
.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
|
.frame(maxHeight: maxHeight)
|
||||||
|
.aspectRatio(usedAspectRatio, contentMode: usedAspectRatioContentMode)
|
||||||
|
.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
|
||||||
|
} else {
|
||||||
|
content.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var usedAspectRatio: Double {
|
var usedAspectRatio: Double {
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import AVKit
|
import AVKit
|
||||||
|
import Defaults
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
#if !os(tvOS)
|
||||||
|
import SwiftUIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
struct VideoPlayerView: View {
|
struct VideoPlayerView: View {
|
||||||
static let defaultAspectRatio: Double = 1.77777778
|
static let defaultAspectRatio: Double = 1.77777778
|
||||||
@ -12,103 +16,154 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@StateObject private var store = Store<Video>()
|
@State private var playerSize: CGSize = .zero
|
||||||
|
@State private var fullScreen = false
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@EnvironmentObject<InvidiousAPI> private var api
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlaybackModel> private var playback
|
|
||||||
|
|
||||||
var resource: Resource {
|
|
||||||
api.video(video.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
var video: Video
|
|
||||||
|
|
||||||
init(_ video: Video) {
|
|
||||||
self.video = video
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
#if os(macOS)
|
||||||
#if os(tvOS)
|
HSplitView {
|
||||||
Player(video: video)
|
content
|
||||||
.environmentObject(playback)
|
}
|
||||||
#else
|
.frame(idealWidth: 1000, maxWidth: 1100, minHeight: 700)
|
||||||
GeometryReader { geometry in
|
#else
|
||||||
VStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
#if os(iOS)
|
content
|
||||||
if verticalSizeClass == .regular {
|
}
|
||||||
PlaybackBar(video: video)
|
#if os(iOS)
|
||||||
}
|
.navigationBarHidden(true)
|
||||||
#elseif os(macOS)
|
#endif
|
||||||
PlaybackBar(video: video)
|
#endif
|
||||||
#endif
|
}
|
||||||
|
|
||||||
Player(video: video)
|
var content: some View {
|
||||||
.environmentObject(playback)
|
Group {
|
||||||
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: playback.aspectRatio))
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
}
|
#if os(tvOS)
|
||||||
.background(.black)
|
player()
|
||||||
|
#else
|
||||||
VStack(spacing: 0) {
|
GeometryReader { geometry in
|
||||||
#if os(iOS)
|
VStack(spacing: 0) {
|
||||||
if verticalSizeClass == .regular {
|
#if os(iOS)
|
||||||
ScrollView(.vertical, showsIndicators: showScrollIndicators) {
|
if verticalSizeClass == .regular {
|
||||||
if let video = store.item {
|
PlaybackBar()
|
||||||
VideoDetails(video: video)
|
|
||||||
} else {
|
|
||||||
VideoDetails(video: video)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
#elseif os(macOS)
|
||||||
#else
|
PlaybackBar()
|
||||||
if let video = store.item {
|
#endif
|
||||||
VideoDetails(video: video)
|
|
||||||
|
if player.currentItem.isNil {
|
||||||
|
playerPlaceholder(geometry: geometry)
|
||||||
} else {
|
} else {
|
||||||
VideoDetails(video: video)
|
player(geometry: geometry)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.onSwipeGesture(
|
||||||
|
up: {
|
||||||
|
withAnimation {
|
||||||
|
fullScreen = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: { dismiss() }
|
||||||
|
)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
.background(.black)
|
||||||
|
.onAppear {
|
||||||
|
self.playerSize = geometry.size
|
||||||
|
}
|
||||||
|
.onChange(of: geometry.size) { size in
|
||||||
|
self.playerSize = size
|
||||||
|
}
|
||||||
|
|
||||||
|
Group {
|
||||||
|
#if os(iOS)
|
||||||
|
if verticalSizeClass == .regular {
|
||||||
|
VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
VideoDetails(fullScreen: $fullScreen)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.background()
|
||||||
|
.modifier(VideoDetailsPaddingModifier(geometry: geometry, fullScreen: fullScreen))
|
||||||
}
|
}
|
||||||
.modifier(VideoDetailsPaddingModifier(geometry: geometry, aspectRatio: playback.aspectRatio))
|
#endif
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(minWidth: 650)
|
||||||
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
if sidebarQueue {
|
||||||
|
PlayerQueueView(fullScreen: $fullScreen)
|
||||||
|
.frame(maxWidth: 350)
|
||||||
}
|
}
|
||||||
.animation(.linear(duration: 0.2), value: playback.aspectRatio)
|
#elseif os(macOS)
|
||||||
|
PlayerQueueView(fullScreen: $fullScreen)
|
||||||
|
.frame(minWidth: 250)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onAppear {
|
}
|
||||||
resource.addObserver(store)
|
|
||||||
resource.loadIfNeeded()
|
func playerPlaceholder(geometry: GeometryProxy) -> some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
#if !os(tvOS)
|
||||||
|
Image(systemName: "ticket")
|
||||||
|
.font(.system(size: 80))
|
||||||
|
Text("What are we watching next?")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.contentShape(Rectangle())
|
||||||
resource.removeObservers(ownedBy: store)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
|
||||||
resource.invalidate()
|
}
|
||||||
}
|
|
||||||
#if os(macOS)
|
func player(geometry: GeometryProxy? = nil) -> some View {
|
||||||
.frame(maxWidth: 1000, minHeight: 700)
|
Player()
|
||||||
#elseif os(iOS)
|
#if !os(tvOS)
|
||||||
.navigationBarHidden(true)
|
.modifier(VideoPlayerSizeModifier(geometry: geometry))
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
var showScrollIndicators: Bool {
|
#if os(iOS)
|
||||||
#if os(macOS)
|
var sidebarQueue: Bool {
|
||||||
false
|
horizontalSizeClass == .regular && playerSize.width > 750
|
||||||
#else
|
}
|
||||||
true
|
|
||||||
#endif
|
var sidebarQueueBinding: Binding<Bool> {
|
||||||
}
|
Binding(
|
||||||
|
get: { self.sidebarQueue },
|
||||||
|
set: { _ in }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoPlayerView_Previews: PreviewProvider {
|
struct VideoPlayerView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
VideoPlayerView()
|
||||||
Spacer()
|
// .frame(minWidth: 1200, minHeight: 1400)
|
||||||
}
|
.injectFixtureEnvironmentObjects()
|
||||||
.sheet(isPresented: .constant(true)) {
|
|
||||||
VideoPlayerView(Video.fixture)
|
VideoPlayerView()
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
}
|
.previewInterfaceOrientation(.landscapeRight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,14 +79,8 @@ struct AddToPlaylistView: View {
|
|||||||
|
|
||||||
private var form: some View {
|
private var form: some View {
|
||||||
VStack(alignment: formAlignment) {
|
VStack(alignment: formAlignment) {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VideoBanner(video: video)
|
||||||
Text(video.title)
|
.padding(.vertical, 40)
|
||||||
.font(.headline)
|
|
||||||
Text(video.author)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.vertical, 40)
|
|
||||||
|
|
||||||
VStack(alignment: formAlignment) {
|
VStack(alignment: formAlignment) {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
|
@ -3,6 +3,7 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PlaylistsView: View {
|
struct PlaylistsView: View {
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlaylistsModel> private var model
|
@EnvironmentObject<PlaylistsModel> private var model
|
||||||
|
|
||||||
@State private var showingNewPlaylist = false
|
@State private var showingNewPlaylist = false
|
||||||
@ -18,24 +19,26 @@ struct PlaylistsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
SignInRequiredView(title: "Playlists") {
|
PlayerControlsView {
|
||||||
VStack {
|
SignInRequiredView(title: "Playlists") {
|
||||||
#if os(tvOS)
|
VStack {
|
||||||
toolbar
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if model.currentPlaylist != nil, videos.isEmpty {
|
|
||||||
hintText("Playlist is empty\n\nTap and hold on a video and then tap \"Add to Playlist\"")
|
|
||||||
} else if model.all.isEmpty {
|
|
||||||
hintText("You have no playlists\n\nTap on \"New Playlist\" to create one")
|
|
||||||
} else {
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
VideosCellsHorizontal(videos: videos)
|
toolbar
|
||||||
.padding(.top, 40)
|
|
||||||
Spacer()
|
|
||||||
#else
|
|
||||||
VideosCellsVertical(videos: videos)
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if model.currentPlaylist != nil, videos.isEmpty {
|
||||||
|
hintText("Playlist is empty\n\nTap and hold on a video and then tap \"Add to Playlist\"")
|
||||||
|
} else if model.all.isEmpty {
|
||||||
|
hintText("You have no playlists\n\nTap on \"New Playlist\" to create one")
|
||||||
|
} else {
|
||||||
|
#if os(tvOS)
|
||||||
|
VideosCellsHorizontal(videos: videos)
|
||||||
|
.padding(.top, 40)
|
||||||
|
Spacer()
|
||||||
|
#else
|
||||||
|
VideosCellsVertical(videos: videos)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,6 +116,16 @@ struct PlaylistsView: View {
|
|||||||
selectPlaylistButton
|
selectPlaylistButton
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
player.playAll(videos)
|
||||||
|
player.presentPlayer()
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 15) {
|
||||||
|
Image(systemName: "play.fill")
|
||||||
|
Text("Play All")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if model.currentPlaylist != nil {
|
if model.currentPlaylist != nil {
|
||||||
editPlaylistButton
|
editPlaylistButton
|
||||||
}
|
}
|
||||||
|
@ -25,17 +25,19 @@ struct TrendingView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
PlayerControlsView {
|
||||||
VStack(alignment: .center, spacing: 0) {
|
Section {
|
||||||
#if os(tvOS)
|
VStack(alignment: .center, spacing: 0) {
|
||||||
toolbar
|
#if os(tvOS)
|
||||||
VideosCellsHorizontal(videos: store.collection)
|
toolbar
|
||||||
.padding(.top, 40)
|
VideosCellsHorizontal(videos: store.collection)
|
||||||
|
.padding(.top, 40)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
#else
|
#else
|
||||||
VideosCellsVertical(videos: store.collection)
|
VideosCellsVertical(videos: store.collection)
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
|
71
Shared/Videos/VideoBanner.swift
Normal file
71
Shared/Videos/VideoBanner.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct VideoBanner: View {
|
||||||
|
let video: Video
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 12) {
|
||||||
|
smallThumbnail
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(video.title)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
.lineLimit(2)
|
||||||
|
.font(.headline)
|
||||||
|
.frame(alignment: .leading)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text(video.author)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let time = video.playTime {
|
||||||
|
Text(time)
|
||||||
|
.fontWeight(.light)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 100, alignment: .center)
|
||||||
|
}
|
||||||
|
|
||||||
|
var smallThumbnail: some View {
|
||||||
|
Group {
|
||||||
|
if let url = video.thumbnailURL(quality: .medium) {
|
||||||
|
AsyncImage(url: url) { image in
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
} placeholder: {
|
||||||
|
HStack {
|
||||||
|
ProgressView()
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(systemName: "exclamationmark.square")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(.gray)
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(width: 177, height: 100)
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 12))
|
||||||
|
#else
|
||||||
|
.frame(width: 88, height: 50)
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 6))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VideoBanner_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
VideoBanner(video: Video.fixture)
|
||||||
|
VideoBanner(video: Video.fixtureUpcomingWithoutPublishedOrViews)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 900)
|
||||||
|
}
|
||||||
|
}
|
@ -2,32 +2,43 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoView: View {
|
struct VideoView: View {
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
var video: Video
|
||||||
|
|
||||||
|
@State private var playerNavigationLinkActive = false
|
||||||
|
|
||||||
|
@Environment(\.inNavigationView) private var inNavigationView
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
@Environment(\.horizontalCells) private var horizontalCells
|
@Environment(\.horizontalCells) private var horizontalCells
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@Environment(\.inNavigationView) private var inNavigationView
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
var video: Video
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if inNavigationView {
|
Button(action: {
|
||||||
NavigationLink(destination: VideoPlayerView(video)) {
|
player.playNow(video)
|
||||||
content
|
|
||||||
}
|
if inNavigationView {
|
||||||
} else {
|
playerNavigationLinkActive = true
|
||||||
Button(action: { navigation.playVideo(video) }) {
|
} else {
|
||||||
content
|
player.presentPlayer()
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(isActive: $playerNavigationLinkActive, destination: {
|
||||||
|
VideoPlayerView()
|
||||||
|
.environment(\.inNavigationView, true)
|
||||||
|
}) {
|
||||||
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 12))
|
.contentShape(RoundedRectangle(cornerRadius: 12))
|
||||||
.contextMenu { VideoContextMenuView(video: video) }
|
.contextMenu { VideoContextMenuView(video: video, playerNavigationLinkActive: $playerNavigationLinkActive) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var content: some View {
|
var content: some View {
|
||||||
@ -131,7 +142,7 @@ struct VideoView: View {
|
|||||||
#else
|
#else
|
||||||
.frame(minHeight: 50, alignment: .top)
|
.frame(minHeight: 50, alignment: .top)
|
||||||
#endif
|
#endif
|
||||||
.padding(.bottom)
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
if additionalDetailsAvailable {
|
if additionalDetailsAvailable {
|
||||||
|
@ -28,14 +28,14 @@ struct VideosCellsHorizontal: View {
|
|||||||
.padding(.vertical, 30)
|
.padding(.vertical, 30)
|
||||||
#else
|
#else
|
||||||
.padding(.horizontal, 15)
|
.padding(.horizontal, 15)
|
||||||
.padding(.vertical, 20)
|
.padding(.vertical, 10)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.id(UUID())
|
.id(UUID())
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.frame(height: 560)
|
.frame(height: 560)
|
||||||
#else
|
#else
|
||||||
.frame(height: 280)
|
.frame(height: 250)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
|
@ -20,6 +20,22 @@ struct ChannelVideosView: View {
|
|||||||
@Namespace private var focusNamespace
|
@Namespace private var focusNamespace
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
#if os(iOS)
|
||||||
|
if inNavigationView {
|
||||||
|
content
|
||||||
|
} else {
|
||||||
|
PlayerControlsView {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
PlayerControlsView {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some View {
|
||||||
VStack {
|
VStack {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
HStack {
|
HStack {
|
||||||
|
109
Shared/Views/PlayerControlsView.swift
Normal file
109
Shared/Views/PlayerControlsView.swift
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
struct PlayerControlsView<Content: View>: View {
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
|
@EnvironmentObject<PlayerModel> private var model
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
|
||||||
|
init(@ViewBuilder content: @escaping () -> Content) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .bottomLeading) {
|
||||||
|
content
|
||||||
|
#if !os(tvOS)
|
||||||
|
.frame(minHeight: 0, maxHeight: .infinity)
|
||||||
|
.padding(.bottom, 50)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
controls
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var controls: some View {
|
||||||
|
HStack {
|
||||||
|
Button(action: {
|
||||||
|
model.presentingPlayer.toggle()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
if let item = model.currentItem {
|
||||||
|
HStack(spacing: 3) {
|
||||||
|
Text(item.video.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text("— \(item.video.author)")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Not playing")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
Group {
|
||||||
|
if model.isPlaying {
|
||||||
|
Button(action: {
|
||||||
|
model.pause()
|
||||||
|
}) {
|
||||||
|
Label("Pause", systemImage: "pause.fill")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(action: {
|
||||||
|
model.play()
|
||||||
|
}) {
|
||||||
|
Label("Play", systemImage: "play.fill")
|
||||||
|
}
|
||||||
|
.disabled(model.player.currentItem == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minWidth: 30)
|
||||||
|
.scaleEffect(1.7)
|
||||||
|
#if !os(tvOS)
|
||||||
|
.keyboardShortcut("p")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Button(action: { model.advanceToNextItem() }) {
|
||||||
|
Label("Next", systemImage: "forward.fill")
|
||||||
|
}
|
||||||
|
.disabled(model.queue.isEmpty)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 0)
|
||||||
|
.background(.ultraThinMaterial)
|
||||||
|
.borderTop(height: 0.4, color: Color("PlayerControlsBorderColor"))
|
||||||
|
.borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("PlayerControlsBorderColor"))
|
||||||
|
#if !os(tvOS)
|
||||||
|
.onSwipeGesture(up: {
|
||||||
|
model.presentingPlayer = true
|
||||||
|
})
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PlayerControlsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
PlayerControlsView {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
Text("Hello")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
}
|
||||||
|
}
|
@ -9,9 +9,11 @@ struct PlaylistVideosView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VideosCellsVertical(videos: playlist.videos)
|
PlayerControlsView {
|
||||||
#if !os(tvOS)
|
VideosCellsVertical(videos: playlist.videos)
|
||||||
.navigationTitle("\(playlist.title) Playlist")
|
#if !os(tvOS)
|
||||||
#endif
|
.navigationTitle("\(playlist.title) Playlist")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,15 @@ struct PopularView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VideosCellsVertical(videos: store.collection)
|
PlayerControlsView {
|
||||||
.onAppear {
|
VideosCellsVertical(videos: store.collection)
|
||||||
resource.addObserver(store)
|
.onAppear {
|
||||||
resource.loadIfNeeded()
|
resource.addObserver(store)
|
||||||
}
|
resource.loadIfNeeded()
|
||||||
#if !os(tvOS)
|
}
|
||||||
.navigationTitle("Popular")
|
#if !os(tvOS)
|
||||||
#endif
|
.navigationTitle("Popular")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,29 +30,31 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
PlayerControlsView {
|
||||||
if showRecentQueries {
|
VStack {
|
||||||
recentQueries
|
if showRecentQueries {
|
||||||
} else {
|
recentQueries
|
||||||
#if os(tvOS)
|
} else {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
#if os(tvOS)
|
||||||
filtersHorizontalStack
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
filtersHorizontalStack
|
||||||
|
|
||||||
VideosCellsHorizontal(videos: state.store.collection)
|
VideosCellsHorizontal(videos: state.store.collection)
|
||||||
|
}
|
||||||
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
|
#else
|
||||||
|
VideosCellsVertical(videos: state.store.collection)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if noResults {
|
||||||
|
Text("No results")
|
||||||
|
|
||||||
|
if searchFiltersActive {
|
||||||
|
Button("Reset search filters", action: resetFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
|
||||||
#else
|
|
||||||
VideosCellsVertical(videos: state.store.collection)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if noResults {
|
|
||||||
Text("No results")
|
|
||||||
|
|
||||||
if searchFiltersActive {
|
|
||||||
Button("Reset search filters", action: resetFilters)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ struct SignInRequiredView<Content: View>: View {
|
|||||||
openSettingsButton
|
openSettingsButton
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
|
|
||||||
var openSettingsButton: some View {
|
var openSettingsButton: some View {
|
||||||
@ -74,9 +75,12 @@ struct SignInRequiredView<Content: View>: View {
|
|||||||
|
|
||||||
struct SignInRequiredView_Previews: PreviewProvider {
|
struct SignInRequiredView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SignInRequiredView(title: "Subscriptions") {
|
PlayerControlsView {
|
||||||
Text("Only when signed in")
|
SignInRequiredView(title: "Subscriptions") {
|
||||||
|
Text("Only when signed in")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.environmentObject(PlayerModel())
|
||||||
.environmentObject(InvidiousAPI())
|
.environmentObject(InvidiousAPI())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,17 +11,19 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
SignInRequiredView(title: "Subscriptions") {
|
PlayerControlsView {
|
||||||
VideosCellsVertical(videos: store.collection)
|
SignInRequiredView(title: "Subscriptions") {
|
||||||
.onAppear {
|
VideosCellsVertical(videos: store.collection)
|
||||||
loadResources()
|
.onAppear {
|
||||||
}
|
loadResources()
|
||||||
.onChange(of: api.account) { _ in
|
}
|
||||||
loadResources(force: true)
|
.onChange(of: api.account) { _ in
|
||||||
}
|
loadResources(force: true)
|
||||||
.onChange(of: feed) { _ in
|
}
|
||||||
loadResources(force: true)
|
.onChange(of: feed) { _ in
|
||||||
}
|
loadResources(force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
loadResources(force: true)
|
loadResources(force: true)
|
||||||
|
@ -2,26 +2,72 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoContextMenuView: View {
|
struct VideoContextMenuView: View {
|
||||||
|
let video: Video
|
||||||
|
|
||||||
|
@Binding var playerNavigationLinkActive: Bool
|
||||||
|
|
||||||
|
@Environment(\.inNavigationView) private var inNavigationView
|
||||||
|
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
let video: Video
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
openChannelButton
|
Section {
|
||||||
|
playNowButton
|
||||||
subscriptionButton
|
}
|
||||||
|
Section {
|
||||||
if navigation.tabSelection != .playlists {
|
playNextButton
|
||||||
addToPlaylistButton
|
addToQueueButton
|
||||||
} else if let playlist = playlists.currentPlaylist {
|
|
||||||
removeFromPlaylistButton(playlistID: playlist.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if case let .playlist(id) = navigation.tabSelection {
|
Section {
|
||||||
removeFromPlaylistButton(playlistID: id)
|
openChannelButton
|
||||||
|
subscriptionButton
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
if navigation.tabSelection != .playlists {
|
||||||
|
addToPlaylistButton
|
||||||
|
} else if let playlist = playlists.currentPlaylist {
|
||||||
|
removeFromPlaylistButton(playlistID: playlist.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if case let .playlist(id) = navigation.tabSelection {
|
||||||
|
removeFromPlaylistButton(playlistID: id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var playNowButton: some View {
|
||||||
|
Button {
|
||||||
|
player.playNow(video)
|
||||||
|
|
||||||
|
if inNavigationView {
|
||||||
|
playerNavigationLinkActive = true
|
||||||
|
} else {
|
||||||
|
player.presentPlayer()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Play Now", systemImage: "play")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var playNextButton: some View {
|
||||||
|
Button {
|
||||||
|
player.playNext(video)
|
||||||
|
} label: {
|
||||||
|
Label("Play Next", systemImage: "text.insert")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var addToQueueButton: some View {
|
||||||
|
Button {
|
||||||
|
player.enqueueVideo(video)
|
||||||
|
} label: {
|
||||||
|
Label("Play Last", systemImage: "text.append")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,33 +6,35 @@ struct WatchNowView: View {
|
|||||||
@EnvironmentObject<InvidiousAPI> private var api
|
@EnvironmentObject<InvidiousAPI> private var api
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
PlayerControlsView {
|
||||||
if api.validInstance {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
if api.validInstance {
|
||||||
if api.signedIn {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
WatchNowSection(resource: api.feed, label: "Subscriptions")
|
if api.signedIn {
|
||||||
}
|
WatchNowSection(resource: api.feed, label: "Subscriptions")
|
||||||
WatchNowSection(resource: api.popular, label: "Popular")
|
}
|
||||||
WatchNowSection(resource: api.trending(category: .default, country: .pl), label: "Trending")
|
WatchNowSection(resource: api.popular, label: "Popular")
|
||||||
WatchNowSection(resource: api.trending(category: .movies, country: .pl), label: "Movies")
|
WatchNowSection(resource: api.trending(category: .default, country: .pl), label: "Trending")
|
||||||
WatchNowSection(resource: api.trending(category: .music, country: .pl), label: "Music")
|
WatchNowSection(resource: api.trending(category: .movies, country: .pl), label: "Movies")
|
||||||
|
WatchNowSection(resource: api.trending(category: .music, country: .pl), label: "Music")
|
||||||
|
|
||||||
// TODO: adding sections to view
|
// TODO: adding sections to view
|
||||||
// ===================
|
// ===================
|
||||||
// WatchNowPlaylistSection(id: "IVPLmRFYLGYZpq61SpujNw3EKbzzGNvoDmH")
|
// WatchNowPlaylistSection(id: "IVPLmRFYLGYZpq61SpujNw3EKbzzGNvoDmH")
|
||||||
// WatchNowSection(resource: api.channelVideos("UCBJycsmduvYEL83R_U4JriQ"), label: "MKBHD")
|
// WatchNowSection(resource: api.channelVideos("UCBJycsmduvYEL83R_U4JriQ"), label: "MKBHD")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
|
#else
|
||||||
|
.navigationTitle("Watch Now")
|
||||||
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
.background()
|
||||||
|
.frame(minWidth: 360)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
|
||||||
#else
|
|
||||||
.navigationTitle("Watch Now")
|
|
||||||
#endif
|
|
||||||
#if os(macOS)
|
|
||||||
.background()
|
|
||||||
.frame(minWidth: 360)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,19 +2,22 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct Player: NSViewControllerRepresentable {
|
struct Player: NSViewControllerRepresentable {
|
||||||
var video: Video!
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
@EnvironmentObject<InvidiousAPI> private var api
|
var controller: PlayerViewController?
|
||||||
@EnvironmentObject<PlaybackModel> private var playback
|
|
||||||
|
init(controller: PlayerViewController? = nil) {
|
||||||
|
self.controller = controller
|
||||||
|
}
|
||||||
|
|
||||||
func makeNSViewController(context _: Context) -> PlayerViewController {
|
func makeNSViewController(context _: Context) -> PlayerViewController {
|
||||||
|
if self.controller != nil {
|
||||||
|
return self.controller!
|
||||||
|
}
|
||||||
|
|
||||||
let controller = PlayerViewController()
|
let controller = PlayerViewController()
|
||||||
|
|
||||||
controller.video = video
|
controller.playerModel = player
|
||||||
controller.playback = playback
|
|
||||||
controller.api = api
|
|
||||||
|
|
||||||
controller.resolution = Defaults[.quality]
|
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
@ -2,42 +2,20 @@ import AVKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class PlayerViewController: NSViewController {
|
final class PlayerViewController: NSViewController {
|
||||||
var video: Video!
|
|
||||||
|
|
||||||
var api: InvidiousAPI!
|
|
||||||
var player = AVPlayer()
|
|
||||||
var playerModel: PlayerModel!
|
var playerModel: PlayerModel!
|
||||||
var playback: PlaybackModel!
|
|
||||||
var playerView = AVPlayerView()
|
var playerView = AVPlayerView()
|
||||||
var resolution: Stream.ResolutionSetting!
|
|
||||||
|
|
||||||
override func viewDidDisappear() {
|
override func viewDidDisappear() {
|
||||||
playerView.player?.replaceCurrentItem(with: nil)
|
// TODO: pause on disappear settings
|
||||||
playerView.player = nil
|
|
||||||
|
|
||||||
playerModel.player = nil
|
|
||||||
playerModel = nil
|
|
||||||
|
|
||||||
super.viewDidDisappear()
|
super.viewDidDisappear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
playerModel = PlayerModel(playback: playback, api: api, resolution: resolution)
|
|
||||||
|
|
||||||
guard playerModel.player.isNil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playerModel.player = player
|
|
||||||
playerView.player = playerModel.player
|
playerView.player = playerModel.player
|
||||||
|
|
||||||
playerView.allowsPictureInPicturePlayback = true
|
playerView.allowsPictureInPicturePlayback = true
|
||||||
playerView.showsFullScreenToggleButton = true
|
playerView.showsFullScreenToggleButton = true
|
||||||
|
|
||||||
view = playerView
|
view = playerView
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.playerModel.loadVideo(self.video)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
81
tvOS/NowPlayingView.swift
Normal file
81
tvOS/NowPlayingView.swift
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NowPlayingView: View {
|
||||||
|
var infoViewController = false
|
||||||
|
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if infoViewController {
|
||||||
|
content
|
||||||
|
.background(.thinMaterial)
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 24))
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some View {
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if !infoViewController, let item = player.currentItem {
|
||||||
|
Group {
|
||||||
|
header("Now Playing")
|
||||||
|
|
||||||
|
Button {
|
||||||
|
player.presentPlayer()
|
||||||
|
} label: {
|
||||||
|
VideoBanner(video: item.video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onPlayPauseCommand(perform: player.togglePlay)
|
||||||
|
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !infoViewController {
|
||||||
|
header("Playing Next")
|
||||||
|
}
|
||||||
|
|
||||||
|
if player.queue.isEmpty {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("Playback queue is empty")
|
||||||
|
.padding(.leading, 40)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(player.queue) { item in
|
||||||
|
Button {
|
||||||
|
player.advanceToItem(item)
|
||||||
|
player.presentPlayer()
|
||||||
|
} label: {
|
||||||
|
VideoBanner(video: item.video)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
player.remove(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
}
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 260, maxHeight: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
func header(_ text: String) -> some View {
|
||||||
|
Text(text)
|
||||||
|
.font(.title3.bold())
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.leading, 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NowPlayingView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NowPlayingView()
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,8 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TVNavigationView: View {
|
struct TVNavigationView: View {
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlaybackModel> private var playback
|
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SearchModel> private var search
|
@EnvironmentObject<SearchModel> private var search
|
||||||
|
|
||||||
@ -29,6 +29,10 @@ struct TVNavigationView: View {
|
|||||||
.tabItem { Text("Playlists") }
|
.tabItem { Text("Playlists") }
|
||||||
.tag(TabSelection.playlists)
|
.tag(TabSelection.playlists)
|
||||||
|
|
||||||
|
NowPlayingView()
|
||||||
|
.tabItem { Text("Now Playing") }
|
||||||
|
.tag(TabSelection.nowPlaying)
|
||||||
|
|
||||||
SearchView()
|
SearchView()
|
||||||
.tabItem { Image(systemName: "magnifyingglass") }
|
.tabItem { Image(systemName: "magnifyingglass") }
|
||||||
.tag(TabSelection.search)
|
.tag(TabSelection.search)
|
||||||
@ -39,11 +43,8 @@ struct TVNavigationView: View {
|
|||||||
AddToPlaylistView(video: video)
|
AddToPlaylistView(video: video)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $navigation.showingVideo) {
|
.fullScreenCover(isPresented: $player.presentingPlayer) {
|
||||||
if let video = navigation.video {
|
VideoPlayerView()
|
||||||
VideoPlayerView(video)
|
|
||||||
.environmentObject(playback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $navigation.isChannelOpen) {
|
.fullScreenCover(isPresented: $navigation.isChannelOpen) {
|
||||||
if let channel = recents.presentedChannel {
|
if let channel = recents.presentedChannel {
|
||||||
|
Loading…
Reference in New Issue
Block a user