mirror of
https://github.com/yattee/yattee.git
synced 2024-12-13 22:00:31 +05:30
Resolution switching support
This commit is contained in:
parent
65e5f0f426
commit
4535853ac3
@ -5,24 +5,15 @@ import SwiftUI
|
||||
struct PlayerView: View {
|
||||
@ObservedObject private var provider: VideoDetailsProvider
|
||||
|
||||
private var id: String
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
provider = VideoDetailsProvider(id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let video = provider.video {
|
||||
if video.url != nil {
|
||||
PlayerViewController(video)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
if video.error {
|
||||
Text("Video can not be loaded")
|
||||
}
|
||||
PlayerViewController(video: video)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
@ -32,37 +23,3 @@ struct PlayerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerViewController: UIViewControllerRepresentable {
|
||||
var video: Video
|
||||
|
||||
init(_ video: Video) {
|
||||
self.video = video
|
||||
}
|
||||
|
||||
private var player: AVPlayer {
|
||||
let item = AVPlayerItem(url: video.url!)
|
||||
item.externalMetadata = [makeMetadataItem(.commonIdentifierTitle, value: video.title)]
|
||||
|
||||
return AVPlayer(playerItem: item)
|
||||
}
|
||||
|
||||
private func makeMetadataItem(_ identifier: AVMetadataIdentifier, value: Any) -> AVMetadataItem {
|
||||
let item = AVMutableMetadataItem()
|
||||
item.identifier = identifier
|
||||
item.value = value as? NSCopying & NSObjectProtocol
|
||||
item.extendedLanguageTag = "und"
|
||||
return item.copy() as! AVMetadataItem
|
||||
}
|
||||
|
||||
func makeUIViewController(context _: Context) -> AVPlayerViewController {
|
||||
let controller = AVPlayerViewController()
|
||||
controller.modalPresentationStyle = .fullScreen
|
||||
controller.player = player
|
||||
controller.title = video.title
|
||||
controller.player?.play()
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_: AVPlayerViewController, context _: Context) {}
|
||||
}
|
||||
|
142
Apple TV/PlayerViewController.swift
Normal file
142
Apple TV/PlayerViewController.swift
Normal file
@ -0,0 +1,142 @@
|
||||
import AVKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct PlayerViewController: UIViewControllerRepresentable {
|
||||
@ObservedObject private var state = PlayerState()
|
||||
@ObservedObject var video: Video
|
||||
|
||||
var player = AVPlayer()
|
||||
var composition = AVMutableComposition()
|
||||
|
||||
var audioTrack: AVMutableCompositionTrack {
|
||||
composition.tracks(withMediaType: .audio).first ?? composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!
|
||||
}
|
||||
|
||||
var videoTrack: AVMutableCompositionTrack {
|
||||
composition.tracks(withMediaType: .video).first ?? composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!
|
||||
}
|
||||
|
||||
var playerItem: AVPlayerItem {
|
||||
let playerItem = AVPlayerItem(asset: composition)
|
||||
|
||||
playerItem.externalMetadata = [makeMetadataItem(.commonIdentifierTitle, value: video.title)]
|
||||
|
||||
return playerItem
|
||||
}
|
||||
|
||||
init(video: Video) {
|
||||
self.video = video
|
||||
state.currentStream = video.defaultStream
|
||||
|
||||
addTracksAndLoadAssets(state.currentStream!)
|
||||
}
|
||||
|
||||
func addTracksAndLoadAssets(_ stream: Stream) {
|
||||
composition.removeTrack(audioTrack)
|
||||
composition.removeTrack(videoTrack)
|
||||
|
||||
let keys = ["playable"]
|
||||
|
||||
stream.audioAsset.loadValuesAsynchronously(forKeys: keys) {
|
||||
DispatchQueue.main.async {
|
||||
guard let track = stream.audioAsset.tracks(withMediaType: .audio).first else {
|
||||
return
|
||||
}
|
||||
|
||||
try? audioTrack.insertTimeRange(
|
||||
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1)),
|
||||
of: track,
|
||||
at: .zero
|
||||
)
|
||||
|
||||
handleAssetLoad(stream)
|
||||
}
|
||||
}
|
||||
|
||||
stream.videoAsset.loadValuesAsynchronously(forKeys: keys) {
|
||||
DispatchQueue.main.async {
|
||||
guard let track = stream.videoAsset.tracks(withMediaType: .video).first else {
|
||||
return
|
||||
}
|
||||
|
||||
try? videoTrack.insertTimeRange(
|
||||
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1)),
|
||||
of: track,
|
||||
at: .zero
|
||||
)
|
||||
|
||||
handleAssetLoad(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAssetLoad(_ stream: Stream) {
|
||||
var error: NSError?
|
||||
let status = stream.videoAsset.statusOfValue(forKey: "playable", error: &error)
|
||||
|
||||
switch status {
|
||||
case .loaded:
|
||||
let resumeAt = player.currentTime()
|
||||
|
||||
if resumeAt.seconds > 0 {
|
||||
state.seekTo = resumeAt
|
||||
}
|
||||
|
||||
state.currentStream = stream
|
||||
|
||||
player.replaceCurrentItem(with: playerItem)
|
||||
|
||||
if let time = state.seekTo {
|
||||
player.seek(to: time)
|
||||
}
|
||||
|
||||
player.play()
|
||||
|
||||
default:
|
||||
if error != nil {
|
||||
print("loading error: \(error!)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeMetadataItem(_ identifier: AVMetadataIdentifier, value: Any) -> AVMetadataItem {
|
||||
let item = AVMutableMetadataItem()
|
||||
|
||||
item.identifier = identifier
|
||||
item.value = value as? NSCopying & NSObjectProtocol
|
||||
item.extendedLanguageTag = "und"
|
||||
|
||||
return item.copy() as! AVMetadataItem
|
||||
}
|
||||
|
||||
func makeUIViewController(context _: Context) -> AVPlayerViewController {
|
||||
let controller = AVPlayerViewController()
|
||||
|
||||
controller.transportBarCustomMenuItems = [streamingQualityMenu]
|
||||
controller.modalPresentationStyle = .fullScreen
|
||||
controller.player = player
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ controller: AVPlayerViewController, context _: Context) {
|
||||
controller.transportBarCustomMenuItems = [streamingQualityMenu]
|
||||
}
|
||||
|
||||
var streamingQualityMenu: UIMenu {
|
||||
UIMenu(title: "Streaming quality", image: UIImage(systemName: "4k.tv"), children: streamingQualityMenuActions)
|
||||
}
|
||||
|
||||
var streamingQualityMenuActions: [UIAction] {
|
||||
video.selectableStreams.map { stream in
|
||||
let image = self.state.currentStream == stream ? UIImage(systemName: "checkmark") : nil
|
||||
|
||||
return UIAction(title: stream.description, image: image) { _ in
|
||||
DispatchQueue.main.async {
|
||||
addTracksAndLoadAssets(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ struct PopularVideosView: View {
|
||||
}
|
||||
|
||||
var videos: [Video] {
|
||||
if (provider.videos.isEmpty) {
|
||||
if provider.videos.isEmpty {
|
||||
provider.load()
|
||||
}
|
||||
|
||||
|
@ -14,19 +14,7 @@ struct SearchView: View {
|
||||
}
|
||||
|
||||
var videos: [Video] {
|
||||
var newQuery = query
|
||||
|
||||
if let url = URLComponents(string: query),
|
||||
let queryItem = url.queryItems?.first(where: { item in item.name == "v" }),
|
||||
let id = queryItem.value
|
||||
{
|
||||
newQuery = id
|
||||
}
|
||||
|
||||
if newQuery != provider.query {
|
||||
provider.query = newQuery
|
||||
provider.load()
|
||||
}
|
||||
provider.load(query)
|
||||
|
||||
return provider.videos
|
||||
}
|
||||
|
@ -16,6 +16,6 @@ struct SubscriptionsView: View {
|
||||
}
|
||||
|
||||
var videos: [Video] {
|
||||
return provider.videos
|
||||
provider.videos
|
||||
}
|
||||
}
|
||||
|
@ -67,16 +67,16 @@ struct VideoThumbnailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct VideoThumbnailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VideoThumbnailView(video: Video(
|
||||
id: "A",
|
||||
title: "A very very long text which",
|
||||
thumbnailURL: URL(string: "https://invidious.home.arekf.net/vi/yXohcxCKqvo/maxres.jpg")!,
|
||||
author: "Bear",
|
||||
length: 240,
|
||||
published: "2 days ago",
|
||||
channelID: ""
|
||||
)).frame(maxWidth: 350)
|
||||
}
|
||||
}
|
||||
// struct VideoThumbnailView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// VideoThumbnailView(video: Video(
|
||||
// id: "A",
|
||||
// title: "A very very long text which",
|
||||
// thumbnailURL: URL(string: "https://invidious.home.arekf.net/vi/yXohcxCKqvo/maxres.jpg")!,
|
||||
// author: "Bear",
|
||||
// length: 240,
|
||||
// published: "2 days ago",
|
||||
// channelID: ""
|
||||
// )).frame(maxWidth: 350)
|
||||
// }
|
||||
// }
|
||||
|
@ -27,17 +27,17 @@ struct VideosView: View {
|
||||
}
|
||||
|
||||
func openChannelButton(from video: Video) -> some View {
|
||||
Button("\(video.author) Channel", action: {
|
||||
Button("\(video.author) Channel") {
|
||||
state.openChannel(from: video)
|
||||
tabSelection = .channel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func closeChannelButton(name: String) -> some View {
|
||||
Button("Close \(name) Channel", action: {
|
||||
Button("Close \(name) Channel") {
|
||||
tabSelection = .popular
|
||||
state.closeChannel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var listRowInsets: EdgeInsets {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
class AppState: ObservableObject {
|
||||
final class AppState: ObservableObject {
|
||||
@Published var showingChannel = false
|
||||
@Published var channelID: String = ""
|
||||
@Published var channel: String = ""
|
||||
|
@ -1,13 +1,15 @@
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
class ChannelVideosProvider: DataProvider {
|
||||
final class ChannelVideosProvider: DataProvider {
|
||||
@Published var videos = [Video]()
|
||||
|
||||
var channelID: String? = ""
|
||||
|
||||
func load() {
|
||||
guard channelID != nil else { return }
|
||||
guard channelID != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let searchPath = "channels/\(channelID!)"
|
||||
DataProvider.request(searchPath).responseJSON { response in
|
||||
|
@ -1,9 +1,20 @@
|
||||
import Alamofire
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable:next final_class
|
||||
class DataProvider: ObservableObject {
|
||||
static let instance = "https://invidious.home.arekf.net"
|
||||
|
||||
static func proxyURLForAsset(_ url: String) -> URL? {
|
||||
guard let instanceURLComponents = URLComponents(string: DataProvider.instance),
|
||||
var urlComponents = URLComponents(string: url) else { return nil }
|
||||
|
||||
urlComponents.scheme = instanceURLComponents.scheme
|
||||
urlComponents.host = instanceURLComponents.host
|
||||
|
||||
return urlComponents.url
|
||||
}
|
||||
|
||||
static func request(_ path: String, headers: HTTPHeaders? = nil) -> DataRequest {
|
||||
AF.request(apiURLString(path), headers: headers)
|
||||
}
|
||||
|
12
Model/MuxedStream.swift
Normal file
12
Model/MuxedStream.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
final class MuxedStream: Stream {
|
||||
var muxedAsset: AVURLAsset
|
||||
|
||||
init(muxedAsset: AVURLAsset, resolution: StreamResolution, type: StreamType, encoding: String) {
|
||||
self.muxedAsset = muxedAsset
|
||||
|
||||
super.init(audioAsset: muxedAsset, videoAsset: muxedAsset, resolution: resolution, type: type, encoding: encoding)
|
||||
}
|
||||
}
|
7
Model/PlayerState.swift
Normal file
7
Model/PlayerState.swift
Normal file
@ -0,0 +1,7 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
final class PlayerState: ObservableObject {
|
||||
@Published var currentStream: Stream!
|
||||
@Published var seekTo: CMTime?
|
||||
}
|
@ -1,13 +1,28 @@
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
class SearchedVideosProvider: DataProvider {
|
||||
final class SearchedVideosProvider: DataProvider {
|
||||
@Published var videos = [Video]()
|
||||
|
||||
var query: String = ""
|
||||
var currentQuery: String = ""
|
||||
|
||||
func load() {
|
||||
let searchPath = "search?q=\(query.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!)"
|
||||
func load(_ query: String) {
|
||||
var newQuery = query
|
||||
|
||||
if let url = URLComponents(string: query),
|
||||
let queryItem = url.queryItems?.first(where: { item in item.name == "v" }),
|
||||
let id = queryItem.value
|
||||
{
|
||||
newQuery = id
|
||||
}
|
||||
|
||||
if newQuery == currentQuery {
|
||||
return
|
||||
}
|
||||
|
||||
currentQuery = newQuery
|
||||
|
||||
let searchPath = "search?q=\(currentQuery.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!)"
|
||||
DataProvider.request(searchPath).responseJSON { response in
|
||||
switch response.result {
|
||||
case let .success(value):
|
||||
|
29
Model/Stream.swift
Normal file
29
Model/Stream.swift
Normal file
@ -0,0 +1,29 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable:next final_class
|
||||
class Stream: Equatable {
|
||||
var audioAsset: AVURLAsset
|
||||
var videoAsset: AVURLAsset
|
||||
|
||||
var resolution: StreamResolution
|
||||
var type: StreamType
|
||||
|
||||
var encoding: String
|
||||
|
||||
init(audioAsset: AVURLAsset, videoAsset: AVURLAsset, resolution: StreamResolution, type: StreamType, encoding: String) {
|
||||
self.audioAsset = audioAsset
|
||||
self.videoAsset = videoAsset
|
||||
self.resolution = resolution
|
||||
self.type = type
|
||||
self.encoding = encoding
|
||||
}
|
||||
|
||||
var description: String {
|
||||
"\(resolution.height)p"
|
||||
}
|
||||
|
||||
static func == (lhs: Stream, rhs: Stream) -> Bool {
|
||||
lhs.resolution == rhs.resolution && lhs.type == rhs.type
|
||||
}
|
||||
}
|
17
Model/StreamResolution.swift
Normal file
17
Model/StreamResolution.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
enum StreamResolution: String, CaseIterable, Comparable {
|
||||
case hd_1080p, hd_720p, sd_480p, sd_360p, sd_240p, sd_144p
|
||||
|
||||
var height: Int {
|
||||
Int(rawValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined())!
|
||||
}
|
||||
|
||||
static func from(resolution: String) -> StreamResolution? {
|
||||
allCases.first { "\($0)".contains(resolution) }
|
||||
}
|
||||
|
||||
static func < (lhs: StreamResolution, rhs: StreamResolution) -> Bool {
|
||||
lhs.height < rhs.height
|
||||
}
|
||||
}
|
18
Model/StreamType.swift
Normal file
18
Model/StreamType.swift
Normal file
@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
|
||||
enum StreamType: String, Comparable {
|
||||
case stream, adaptive
|
||||
|
||||
private var sortOrder: Int {
|
||||
switch self {
|
||||
case .stream:
|
||||
return 0
|
||||
case .adaptive:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
static func < (lhs: StreamType, rhs: StreamType) -> Bool {
|
||||
lhs.sortOrder < rhs.sortOrder
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import Alamofire
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
class SubscriptionVideosProvider: DataProvider {
|
||||
final class SubscriptionVideosProvider: DataProvider {
|
||||
@Published var videos = [Video]()
|
||||
|
||||
var sid: String = "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8="
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Alamofire
|
||||
import AVKit
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
@ -10,11 +11,9 @@ final class Video: Identifiable, ObservableObject {
|
||||
var length: TimeInterval
|
||||
var published: String
|
||||
var views: Int
|
||||
|
||||
var channelID: String
|
||||
|
||||
@Published var url: URL?
|
||||
@Published var error: Bool = false
|
||||
var streams = [Stream]()
|
||||
|
||||
init(
|
||||
id: String,
|
||||
@ -23,8 +22,8 @@ final class Video: Identifiable, ObservableObject {
|
||||
author: String,
|
||||
length: TimeInterval,
|
||||
published: String,
|
||||
channelID: String,
|
||||
views: Int = 0
|
||||
views: Int,
|
||||
channelID: String
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
@ -32,41 +31,22 @@ final class Video: Identifiable, ObservableObject {
|
||||
self.author = author
|
||||
self.length = length
|
||||
self.published = published
|
||||
self.channelID = channelID
|
||||
self.views = views
|
||||
self.channelID = channelID
|
||||
}
|
||||
|
||||
init(_ json: JSON) {
|
||||
func extractThumbnailURL(from details: JSON) -> URL? {
|
||||
if details["videoThumbnails"].arrayValue.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
let thumbnail = details["videoThumbnails"].arrayValue.first(where: { $0["quality"].stringValue == "medium" })!
|
||||
return thumbnail["url"].url!
|
||||
}
|
||||
|
||||
func extractFormatStreamURL(from streams: [JSON]) -> URL? {
|
||||
if streams.isEmpty {
|
||||
error = true
|
||||
return nil
|
||||
}
|
||||
|
||||
let stream = streams.last!
|
||||
|
||||
return stream["url"].url
|
||||
}
|
||||
|
||||
id = json["videoId"].stringValue
|
||||
title = json["title"].stringValue
|
||||
thumbnailURL = extractThumbnailURL(from: json)
|
||||
author = json["author"].stringValue
|
||||
length = json["lengthSeconds"].doubleValue
|
||||
published = json["publishedText"].stringValue
|
||||
views = json["viewCount"].intValue
|
||||
channelID = json["authorId"].stringValue
|
||||
thumbnailURL = extractThumbnailURL(from: json)
|
||||
|
||||
url = extractFormatStreamURL(from: json["formatStreams"].arrayValue)
|
||||
streams = extractFormatStreams(from: json["formatStreams"].arrayValue)
|
||||
streams.append(contentsOf: extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue))
|
||||
}
|
||||
|
||||
var playTime: String? {
|
||||
@ -97,4 +77,60 @@ final class Video: Identifiable, ObservableObject {
|
||||
|
||||
return "\(formatter.string(from: number)!)\(unit)"
|
||||
}
|
||||
|
||||
var selectableStreams: [Stream] {
|
||||
let streams = streams.sorted { $0.resolution > $1.resolution }
|
||||
var selectable = [Stream]()
|
||||
|
||||
StreamResolution.allCases.forEach { resolution in
|
||||
if let stream = streams.filter({ $0.resolution == resolution }).min(by: { $0.type < $1.type }) {
|
||||
selectable.append(stream)
|
||||
}
|
||||
}
|
||||
|
||||
return selectable
|
||||
}
|
||||
|
||||
var defaultStream: Stream? {
|
||||
selectableStreams.first { $0.type == .stream }
|
||||
}
|
||||
|
||||
private func extractThumbnailURL(from details: JSON) -> URL? {
|
||||
if details["videoThumbnails"].arrayValue.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
let thumbnail = details["videoThumbnails"].arrayValue.first { $0["quality"].stringValue == "medium" }!
|
||||
return thumbnail["url"].url!
|
||||
}
|
||||
|
||||
private func extractFormatStreams(from streams: [JSON]) -> [Stream] {
|
||||
streams.map {
|
||||
MuxedStream(
|
||||
muxedAsset: AVURLAsset(url: DataProvider.proxyURLForAsset($0["url"].stringValue)!),
|
||||
resolution: StreamResolution.from(resolution: $0["resolution"].stringValue)!,
|
||||
type: .stream,
|
||||
encoding: $0["encoding"].stringValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func extractAdaptiveFormats(from streams: [JSON]) -> [Stream] {
|
||||
let audioAssetURL = streams.first { $0["type"].stringValue.starts(with: "audio/mp4") }
|
||||
guard audioAssetURL != nil else {
|
||||
return []
|
||||
}
|
||||
|
||||
let videoAssetsURLs = streams.filter { $0["type"].stringValue.starts(with: "video/mp4") && $0["encoding"].stringValue == "h264" }
|
||||
|
||||
return videoAssetsURLs.map {
|
||||
Stream(
|
||||
audioAsset: AVURLAsset(url: DataProvider.proxyURLForAsset(audioAssetURL!["url"].stringValue)!),
|
||||
videoAsset: AVURLAsset(url: DataProvider.proxyURLForAsset($0["url"].stringValue)!),
|
||||
resolution: StreamResolution.from(resolution: $0["resolution"].stringValue)!,
|
||||
type: .adaptive,
|
||||
encoding: $0["encoding"].stringValue
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
3741B5302676213400125C5E /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741B52F2676213400125C5E /* PlayerViewController.swift */; };
|
||||
37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularVideosView.swift */; };
|
||||
37AAF28026737550007FC770 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
|
||||
37AAF2822673791F007FC770 /* SearchedVideosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */; };
|
||||
@ -29,6 +30,21 @@
|
||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||
37CEE4B52677B628005A1EFE /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B42677B628005A1EFE /* StreamType.swift */; };
|
||||
37CEE4B62677B628005A1EFE /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B42677B628005A1EFE /* StreamType.swift */; };
|
||||
37CEE4B72677B628005A1EFE /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B42677B628005A1EFE /* StreamType.swift */; };
|
||||
37CEE4B92677B63F005A1EFE /* StreamResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B82677B63F005A1EFE /* StreamResolution.swift */; };
|
||||
37CEE4BA2677B63F005A1EFE /* StreamResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B82677B63F005A1EFE /* StreamResolution.swift */; };
|
||||
37CEE4BB2677B63F005A1EFE /* StreamResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B82677B63F005A1EFE /* StreamResolution.swift */; };
|
||||
37CEE4BD2677B670005A1EFE /* MuxedStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* MuxedStream.swift */; };
|
||||
37CEE4BE2677B670005A1EFE /* MuxedStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* MuxedStream.swift */; };
|
||||
37CEE4BF2677B670005A1EFE /* MuxedStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* MuxedStream.swift */; };
|
||||
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4C02677B697005A1EFE /* Stream.swift */; };
|
||||
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4C02677B697005A1EFE /* Stream.swift */; };
|
||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4C02677B697005A1EFE /* Stream.swift */; };
|
||||
37D4B0D92671614900C925CA /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0D82671614900C925CA /* Tests_iOS.swift */; };
|
||||
37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0E22671614900C925CA /* Tests_macOS.swift */; };
|
||||
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C22671614700C925CA /* PearvidiousApp.swift */; };
|
||||
@ -87,6 +103,7 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3741B52F2676213400125C5E /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
|
||||
37AAF27D26737323007FC770 /* PopularVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularVideosView.swift; sourceTree = "<group>"; };
|
||||
37AAF27F26737550007FC770 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||
37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedVideosProvider.swift; sourceTree = "<group>"; };
|
||||
@ -97,6 +114,11 @@
|
||||
37AAF29926740A01007FC770 /* VideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosView.swift; sourceTree = "<group>"; };
|
||||
37AAF29B26741B5F007FC770 /* SubscriptionVideosProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionVideosProvider.swift; sourceTree = "<group>"; };
|
||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
|
||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = "<group>"; };
|
||||
37CEE4B42677B628005A1EFE /* StreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamType.swift; sourceTree = "<group>"; };
|
||||
37CEE4B82677B63F005A1EFE /* StreamResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamResolution.swift; sourceTree = "<group>"; };
|
||||
37CEE4BC2677B670005A1EFE /* MuxedStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuxedStream.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>"; };
|
||||
37D4B0C32671614700C925CA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
37D4B0C42671614800C925CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@ -230,6 +252,7 @@
|
||||
37AAF2892673AB89007FC770 /* ChannelView.swift */,
|
||||
37AAF27F26737550007FC770 /* SearchView.swift */,
|
||||
37D4B1822671681B00C925CA /* PlayerView.swift */,
|
||||
3741B52F2676213400125C5E /* PlayerViewController.swift */,
|
||||
37AAF29926740A01007FC770 /* VideosView.swift */,
|
||||
37D4B18B26717B3800C925CA /* VideoThumbnailView.swift */,
|
||||
37D4B1AE26729DEB00C925CA /* Info.plist */,
|
||||
@ -250,13 +273,18 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37AAF28F26740715007FC770 /* AppState.swift */,
|
||||
37D4B19626717E1500C925CA /* Video.swift */,
|
||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
|
||||
37AAF29B26741B5F007FC770 /* SubscriptionVideosProvider.swift */,
|
||||
37D4B19226717CE100C925CA /* PopularVideosProvider.swift */,
|
||||
37AAF28B2673ABD3007FC770 /* ChannelVideosProvider.swift */,
|
||||
37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */,
|
||||
37D4B1B32672A30700C925CA /* VideoDetailsProvider.swift */,
|
||||
37D4B1AF2672A01000C925CA /* DataProvider.swift */,
|
||||
37D4B19626717E1500C925CA /* Video.swift */,
|
||||
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
||||
37CEE4B42677B628005A1EFE /* StreamType.swift */,
|
||||
37CEE4B82677B63F005A1EFE /* StreamResolution.swift */,
|
||||
37CEE4BC2677B670005A1EFE /* MuxedStream.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@ -490,18 +518,23 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
37CEE4BD2677B670005A1EFE /* MuxedStream.swift in Sources */,
|
||||
37D4B19326717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
||||
37AAF29C26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
||||
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37D4B0E62671614900C925CA /* ContentView.swift in Sources */,
|
||||
37CEE4B52677B628005A1EFE /* StreamType.swift in Sources */,
|
||||
37AAF2822673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
||||
37AAF29026740715007FC770 /* AppState.swift in Sources */,
|
||||
37AAF2942674086B007FC770 /* TabSelection.swift in Sources */,
|
||||
37D4B1B02672A01000C925CA /* DataProvider.swift in Sources */,
|
||||
37AAF28C2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
||||
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||
37D4B1B42672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
37D4B19726717E1500C925CA /* Video.swift in Sources */,
|
||||
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||
37CEE4B92677B63F005A1EFE /* StreamResolution.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -509,18 +542,23 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
37CEE4BE2677B670005A1EFE /* MuxedStream.swift in Sources */,
|
||||
37D4B19426717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
||||
37AAF29D26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
||||
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37D4B0E72671614900C925CA /* ContentView.swift in Sources */,
|
||||
37CEE4B62677B628005A1EFE /* StreamType.swift in Sources */,
|
||||
37AAF2832673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
||||
37AAF29126740715007FC770 /* AppState.swift in Sources */,
|
||||
37AAF2952674086B007FC770 /* TabSelection.swift in Sources */,
|
||||
37D4B1B12672A01000C925CA /* DataProvider.swift in Sources */,
|
||||
37AAF28D2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||
37D4B1B52672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
||||
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||
37CEE4BA2677B63F005A1EFE /* StreamResolution.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -545,23 +583,29 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
37AAF28026737550007FC770 /* SearchView.swift in Sources */,
|
||||
37CEE4BF2677B670005A1EFE /* MuxedStream.swift in Sources */,
|
||||
37CEE4B72677B628005A1EFE /* StreamType.swift in Sources */,
|
||||
37D4B19526717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
||||
37AAF29E26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
||||
37D4B1842671684E00C925CA /* PlayerView.swift in Sources */,
|
||||
37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */,
|
||||
37D4B1B22672A01000C925CA /* DataProvider.swift in Sources */,
|
||||
37AAF29226740715007FC770 /* AppState.swift in Sources */,
|
||||
3741B5302676213400125C5E /* PlayerViewController.swift in Sources */,
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||
37D4B18E26717B3800C925CA /* VideoThumbnailView.swift in Sources */,
|
||||
37D4B1B62672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
||||
37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */,
|
||||
37AAF29A26740A01007FC770 /* VideosView.swift in Sources */,
|
||||
37AAF2962674086B007FC770 /* TabSelection.swift in Sources */,
|
||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */,
|
||||
37AAF28E2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
||||
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
37D4B1812671653A00C925CA /* ContentView.swift in Sources */,
|
||||
37AAF2842673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
||||
37CEE4BB2677B63F005A1EFE /* StreamResolution.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject var state = AppState()
|
||||
@StateObject private var state = AppState()
|
||||
|
||||
@State var tabSelection: TabSelection = .subscriptions
|
||||
@State private var tabSelection: TabSelection = .subscriptions
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
|
Loading…
Reference in New Issue
Block a user