mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 05:23:41 +00:00
Opening videos by URL and local files
This commit is contained in:
parent
34f7621f36
commit
402d1a2f79
@ -44,7 +44,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
||||
|
||||
player.currentItem = PlayerQueueItem(
|
||||
Video(
|
||||
videoID: "",
|
||||
videoID: "https://a/b/c",
|
||||
title: "Video Name",
|
||||
author: "",
|
||||
length: 0,
|
||||
|
@ -80,6 +80,11 @@ extension VideosAPI {
|
||||
return
|
||||
}
|
||||
|
||||
if let video = item.video, video.isLocal {
|
||||
completionHandler(item)
|
||||
return
|
||||
}
|
||||
|
||||
video(item.videoID).load()
|
||||
.onSuccess { response in
|
||||
guard let video: Video = response.typedContent() else {
|
||||
@ -87,6 +92,7 @@ extension VideosAPI {
|
||||
}
|
||||
|
||||
var newItem = item
|
||||
newItem.id = UUID()
|
||||
newItem.video = video
|
||||
|
||||
completionHandler(newItem)
|
||||
|
@ -63,4 +63,8 @@ enum VideosApp: String, CaseIterable {
|
||||
var allowsDisablingVidoesProxying: Bool {
|
||||
self == .invidious
|
||||
}
|
||||
|
||||
var supportsOpeningVideosByID: Bool {
|
||||
self != .demoApp
|
||||
}
|
||||
}
|
||||
|
25
Model/CacheModel.swift
Normal file
25
Model/CacheModel.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import Cache
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
struct CacheModel {
|
||||
static var shared = CacheModel()
|
||||
|
||||
var urlBookmarksStorage: Storage<String, Data>?
|
||||
var videoStorage: Storage<Video.ID, JSON>?
|
||||
|
||||
init() {
|
||||
let urlBookmarksStorageConfig = DiskConfig(name: "URLBookmarks", expiry: .never)
|
||||
let urlBookmarksMemoryConfig = MemoryConfig(expiry: .never, countLimit: 100, totalCostLimit: 100)
|
||||
urlBookmarksStorage = try? Storage(diskConfig: urlBookmarksStorageConfig, memoryConfig: urlBookmarksMemoryConfig, transformer: TransformerFactory.forData())
|
||||
|
||||
let videoStorageConfig = DiskConfig(name: "VideoStorage", expiry: .never)
|
||||
let videoStorageMemoryConfig = MemoryConfig(expiry: .never, countLimit: 100, totalCostLimit: 100)
|
||||
|
||||
let toData: (JSON) throws -> Data = { try $0.rawData() }
|
||||
let fromData: (Data) throws -> JSON = { try JSON(data: $0) }
|
||||
|
||||
let jsonTransformer = Transformer<JSON>(toData: toData, fromData: fromData)
|
||||
videoStorage = try? Storage<Video.ID, JSON>(diskConfig: videoStorageConfig, memoryConfig: videoStorageMemoryConfig, transformer: jsonTransformer)
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import CoreData
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
extension PlayerModel {
|
||||
func historyVideo(_ id: String) -> Video? {
|
||||
@ -13,12 +14,37 @@ extension PlayerModel {
|
||||
return
|
||||
}
|
||||
|
||||
if !Video.VideoID.isValid(id), let url = URL(string: id) {
|
||||
historyVideos.append(.local(url))
|
||||
return
|
||||
}
|
||||
|
||||
if historyItemBeingLoaded == nil {
|
||||
logger.info("loading history details: \(id)")
|
||||
historyItemBeingLoaded = id
|
||||
} else {
|
||||
logger.info("POSTPONING history load: \(id)")
|
||||
historyItemsToLoad.append(id)
|
||||
return
|
||||
}
|
||||
|
||||
playerAPI.video(id).load().onSuccess { [weak self] response in
|
||||
guard let video: Video = response.typedContent() else {
|
||||
return
|
||||
guard let self else { return }
|
||||
|
||||
if let video: Video = response.typedContent() {
|
||||
self.historyVideos.append(video)
|
||||
}
|
||||
}.onCompletion { _ in
|
||||
self.logger.info("LOADED history details: \(id)")
|
||||
|
||||
if self.historyItemBeingLoaded == id {
|
||||
self.logger.info("setting no history loaded")
|
||||
self.historyItemBeingLoaded = nil
|
||||
}
|
||||
|
||||
self?.historyVideos.append(video)
|
||||
if let id = self.historyItemsToLoad.popLast() {
|
||||
self.loadHistoryVideoDetails(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,7 @@ final class NavigationModel: ObservableObject {
|
||||
@Published var presentingPlaylist = false
|
||||
@Published var sidebarSectionChanged = false
|
||||
|
||||
@Published var presentingOpenVideos = false
|
||||
@Published var presentingSettings = false
|
||||
@Published var presentingWelcomeScreen = false
|
||||
|
||||
|
109
Model/OpenVideosModel.swift
Normal file
109
Model/OpenVideosModel.swift
Normal file
@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
struct OpenVideosModel {
|
||||
enum PlaybackMode: String, CaseIterable {
|
||||
case playNow
|
||||
case shuffleAll
|
||||
case playNext
|
||||
case playLast
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .playNow:
|
||||
return "Play Now".localized()
|
||||
case .shuffleAll:
|
||||
return "Shuffle All".localized()
|
||||
case .playNext:
|
||||
return "Play Next".localized()
|
||||
case .playLast:
|
||||
return "Play Last".localized()
|
||||
}
|
||||
}
|
||||
|
||||
var allowsRemovingQueueItems: Bool {
|
||||
self == .playNow || self == .shuffleAll
|
||||
}
|
||||
|
||||
var allowedWhenQueueIsEmpty: Bool {
|
||||
self == .playNow || self == .shuffleAll
|
||||
}
|
||||
}
|
||||
|
||||
static let shared = OpenVideosModel()
|
||||
var player: PlayerModel! = .shared
|
||||
var logger = Logger(label: "stream.yattee.open-videos")
|
||||
|
||||
func open(_ url: URL) {
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
let video = Video.local(url)
|
||||
|
||||
player.play([video], shuffling: false)
|
||||
}
|
||||
}
|
||||
|
||||
func openURLs(_ urls: [URL], removeQueueItems: Bool, playbackMode: OpenVideosModel.PlaybackMode) {
|
||||
logger.info("opening \(urls.count) urls")
|
||||
urls.forEach { logger.info("\($0.absoluteString)") }
|
||||
|
||||
if removeQueueItems, playbackMode.allowsRemovingQueueItems {
|
||||
player.removeQueueItems()
|
||||
logger.info("removing queue items")
|
||||
}
|
||||
|
||||
switch playbackMode {
|
||||
case .playNow:
|
||||
player.playbackMode = .queue
|
||||
case .shuffleAll:
|
||||
player.playbackMode = .shuffle
|
||||
case .playNext:
|
||||
player.playbackMode = .queue
|
||||
case .playLast:
|
||||
player.playbackMode = .queue
|
||||
}
|
||||
|
||||
enqueue(
|
||||
urls,
|
||||
prepending: playbackMode == .playNow || playbackMode == .playNext
|
||||
)
|
||||
|
||||
if playbackMode == .playNow || playbackMode == .shuffleAll {
|
||||
player.show()
|
||||
player.advanceToNextItem()
|
||||
}
|
||||
}
|
||||
|
||||
func enqueue(_ urls: [URL], prepending: Bool = false) {
|
||||
var videos = urls.compactMap { url in
|
||||
var video: Video!
|
||||
if canOpenVideosByID {
|
||||
let parser = URLParser(url: url)
|
||||
|
||||
if parser.destination == .video, let id = parser.videoID {
|
||||
video = Video(videoID: id)
|
||||
logger.info("identified remote video: \(id)")
|
||||
} else {
|
||||
video = .local(url)
|
||||
logger.info("identified local video: \(url.absoluteString)")
|
||||
}
|
||||
} else {
|
||||
video = .local(url)
|
||||
logger.info("identified local video: \(url.absoluteString)")
|
||||
}
|
||||
|
||||
return video
|
||||
}
|
||||
|
||||
if prepending {
|
||||
videos.reverse()
|
||||
}
|
||||
videos.forEach { video in
|
||||
player.enqueueVideo(video, play: false, prepending: prepending, loadDetails: false)
|
||||
}
|
||||
}
|
||||
|
||||
var canOpenVideosByID: Bool {
|
||||
guard let app = player.accounts.current?.app else { return false }
|
||||
return !player.accounts.isEmpty && app.supportsOpeningVideosByID
|
||||
}
|
||||
}
|
@ -37,9 +37,24 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
avPlayer.timeControlStatus == .playing
|
||||
}
|
||||
|
||||
var videoWidth: Double? {
|
||||
if let width = avPlayer.currentItem?.presentationSize.width {
|
||||
return Double(width)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var videoHeight: Double? {
|
||||
if let height = avPlayer.currentItem?.presentationSize.height {
|
||||
return Double(height)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var aspectRatio: Double {
|
||||
#if os(iOS)
|
||||
playerLayer.videoRect.width / playerLayer.videoRect.height
|
||||
videoWidth! / videoHeight!
|
||||
#else
|
||||
VideoPlayerView.defaultAspectRatio
|
||||
#endif
|
||||
@ -104,8 +119,17 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
preservingTime: Bool,
|
||||
upgrading _: Bool
|
||||
) {
|
||||
if let url = stream.singleAssetURL {
|
||||
if var url = stream.singleAssetURL {
|
||||
model.logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
|
||||
|
||||
if video.isLocal, video.localStreamIsFile, let localURL = video.localStream?.localURL {
|
||||
guard localURL.startAccessingSecurityScopedResource() else {
|
||||
model.navigation.presentAlert(title: "Could not open file")
|
||||
return
|
||||
}
|
||||
url = localURL
|
||||
}
|
||||
|
||||
loadSingleAsset(url, stream: stream, of: video, preservingTime: preservingTime)
|
||||
} else {
|
||||
model.logger.info("playing stream with many assets:")
|
||||
@ -317,6 +341,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
guard video == self.model.currentVideo else {
|
||||
return
|
||||
}
|
||||
|
||||
self.avPlayer.replaceCurrentItem(with: self.model.playerItem)
|
||||
self.seekToPreservedTime { finished in
|
||||
guard finished else {
|
||||
@ -373,7 +398,8 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
#if !os(macOS)
|
||||
var externalMetadata = [
|
||||
makeMetadataItem(.commonIdentifierTitle, value: video.title),
|
||||
makeMetadataItem(.commonIdentifierTitle, value: video.displayTitle),
|
||||
makeMetadataItem(.commonIdentifierArtist, value: video.displayAuthor),
|
||||
makeMetadataItem(.quickTimeMetadataGenre, value: video.genre ?? ""),
|
||||
makeMetadataItem(.commonIdentifierDescription, value: video.description ?? "")
|
||||
]
|
||||
|
@ -108,6 +108,10 @@ final class MPVBackend: PlayerBackend {
|
||||
client?.outputFps ?? 0
|
||||
}
|
||||
|
||||
var formattedOutputFps: String {
|
||||
String(format: "%.2ffps", outputFps)
|
||||
}
|
||||
|
||||
var hwDecoder: String {
|
||||
client?.hwDecoder ?? "unknown"
|
||||
}
|
||||
@ -120,6 +124,54 @@ final class MPVBackend: PlayerBackend {
|
||||
client?.cacheDuration ?? 0
|
||||
}
|
||||
|
||||
var videoFormat: String {
|
||||
client?.videoFormat ?? "unknown"
|
||||
}
|
||||
|
||||
var videoCodec: String {
|
||||
client?.videoCodec ?? "unknown"
|
||||
}
|
||||
|
||||
var currentVo: String {
|
||||
client?.currentVo ?? "unknown"
|
||||
}
|
||||
|
||||
var videoWidth: Double? {
|
||||
if let width = client?.width, width != "unknown" {
|
||||
return Double(width)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var videoHeight: Double? {
|
||||
if let height = client?.height, height != "unknown" {
|
||||
return Double(height)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var audioFormat: String {
|
||||
client?.audioFormat ?? "unknown"
|
||||
}
|
||||
|
||||
var audioCodec: String {
|
||||
client?.audioCodec ?? "unknown"
|
||||
}
|
||||
|
||||
var currentAo: String {
|
||||
client?.currentAo ?? "unknown"
|
||||
}
|
||||
|
||||
var audioChannels: String {
|
||||
client?.audioChannels ?? "unknown"
|
||||
}
|
||||
|
||||
var audioSampleRate: String {
|
||||
client?.audioSampleRate ?? "unknown"
|
||||
}
|
||||
|
||||
init() {
|
||||
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
|
||||
self?.getTimeUpdates()
|
||||
@ -230,6 +282,13 @@ final class MPVBackend: PlayerBackend {
|
||||
startPlaying()
|
||||
}
|
||||
|
||||
if video.isLocal, video.localStreamIsFile, let localStream = video.localStream {
|
||||
guard localStream.localURL.startAccessingSecurityScopedResource() else {
|
||||
self.model.navigation.presentAlert(title: "Could not open file")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.client.loadFile(url, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in
|
||||
self?.isLoadingVideo = true
|
||||
}
|
||||
|
@ -198,6 +198,50 @@ final class MPVClient: ObservableObject {
|
||||
mpv.isNil ? 0.0 : getDouble("demuxer-cache-duration")
|
||||
}
|
||||
|
||||
var videoFormat: String {
|
||||
stringOrUnknown("video-format")
|
||||
}
|
||||
|
||||
var videoCodec: String {
|
||||
stringOrUnknown("video-codec")
|
||||
}
|
||||
|
||||
var currentVo: String {
|
||||
stringOrUnknown("current-vo")
|
||||
}
|
||||
|
||||
var width: String {
|
||||
stringOrUnknown("width")
|
||||
}
|
||||
|
||||
var height: String {
|
||||
stringOrUnknown("height")
|
||||
}
|
||||
|
||||
var videoBitrate: Double {
|
||||
mpv.isNil ? 0.0 : getDouble("video-bitrate")
|
||||
}
|
||||
|
||||
var audioFormat: String {
|
||||
stringOrUnknown("audio-params/format")
|
||||
}
|
||||
|
||||
var audioCodec: String {
|
||||
stringOrUnknown("audio-codec")
|
||||
}
|
||||
|
||||
var currentAo: String {
|
||||
stringOrUnknown("current-ao")
|
||||
}
|
||||
|
||||
var audioChannels: String {
|
||||
stringOrUnknown("audio-params/channels")
|
||||
}
|
||||
|
||||
var audioSampleRate: String {
|
||||
stringOrUnknown("audio-params/samplerate")
|
||||
}
|
||||
|
||||
var aspectRatio: Double {
|
||||
guard !mpv.isNil else { return VideoPlayerView.defaultAspectRatio }
|
||||
let aspect = getDouble("video-params/aspect")
|
||||
@ -407,6 +451,10 @@ final class MPVClient: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func stringOrUnknown(_ name: String) -> String {
|
||||
mpv.isNil ? "unknown" : (getString(name) ?? "unknown")
|
||||
}
|
||||
|
||||
private var machine: String {
|
||||
var systeminfo = utsname()
|
||||
uname(&systeminfo)
|
||||
|
@ -25,6 +25,9 @@ protocol PlayerBackend {
|
||||
var aspectRatio: Double { get }
|
||||
var controlsUpdates: Bool { get }
|
||||
|
||||
var videoWidth: Double? { get }
|
||||
var videoHeight: Double? { get }
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
|
||||
func canPlay(_ stream: Stream) -> Bool
|
||||
|
||||
|
@ -97,6 +97,10 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
|
||||
@Published var videoBeingOpened: Video? { didSet { seek.reset() } }
|
||||
@Published var historyVideos = [Video]()
|
||||
@Published var queueItemBeingLoaded: PlayerQueueItem?
|
||||
@Published var queueItemsToLoad = [PlayerQueueItem]()
|
||||
@Published var historyItemBeingLoaded: Video.ID?
|
||||
@Published var historyItemsToLoad = [Video.ID]()
|
||||
|
||||
@Published var preservedTime: CMTime?
|
||||
|
||||
@ -373,7 +377,7 @@ final class PlayerModel: ObservableObject {
|
||||
withBackend: PlayerBackend? = nil
|
||||
) {
|
||||
playerError = nil
|
||||
if !upgrading {
|
||||
if !upgrading, !video.isLocal {
|
||||
resetSegments()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
@ -440,7 +444,7 @@ final class PlayerModel: ObservableObject {
|
||||
changeActiveBackend(from: activeBackend, to: backend)
|
||||
}
|
||||
|
||||
guard let stream = streamByQualityProfile else {
|
||||
guard let stream = ((availableStreams.count == 1 && availableStreams.first!.isLocal) ? availableStreams.first : nil) ?? streamByQualityProfile else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -842,8 +846,8 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0
|
||||
var nowPlayingInfo: [String: AnyObject] = [
|
||||
MPMediaItemPropertyTitle: video.title as AnyObject,
|
||||
MPMediaItemPropertyArtist: video.author as AnyObject,
|
||||
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
||||
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
||||
MPNowPlayingInfoPropertyIsLiveStream: live as AnyObject,
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||
@ -952,4 +956,9 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var formattedSize: String {
|
||||
guard let videoWidth = backend?.videoWidth, let videoHeight = backend?.videoHeight else { return "unknown" }
|
||||
return "\(String(format: "%.2f", videoWidth))\u{d7}\(String(format: "%.2f", videoHeight))"
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ extension PlayerModel {
|
||||
guard let playerInstance = self.playerInstance else { return }
|
||||
let streamsInstance = video.streams.compactMap(\.instance).first
|
||||
|
||||
if video.streams.isEmpty || streamsInstance != playerInstance {
|
||||
if !video.isLocal, video.streams.isEmpty || streamsInstance != playerInstance {
|
||||
self.loadAvailableStreams(video)
|
||||
} else {
|
||||
self.availableStreams = self.streamsWithInstance(instance: playerInstance, streams: video.streams)
|
||||
@ -203,6 +203,7 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
videoDetailsLoadHandler(video, item)
|
||||
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
||||
}
|
||||
|
||||
@ -210,11 +211,22 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
func prepareCurrentItemForHistory(finished: Bool = false) {
|
||||
if !currentItem.isNil, Defaults[.saveHistory] {
|
||||
if let video = currentVideo, !historyVideos.contains(where: { $0 == video }) {
|
||||
historyVideos.append(video)
|
||||
if let currentItem {
|
||||
if Defaults[.saveHistory] {
|
||||
if let video = currentVideo, !historyVideos.contains(where: { $0 == video }) {
|
||||
historyVideos.append(video)
|
||||
}
|
||||
updateWatch(finished: finished)
|
||||
}
|
||||
|
||||
if let video = currentItem.video,
|
||||
video.isLocal,
|
||||
video.localStreamIsFile,
|
||||
let localURL = video.localStream?.localURL
|
||||
{
|
||||
logger.info("stopping security scoped resource access for \(localURL)")
|
||||
localURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
updateWatch(finished: finished)
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,9 +265,35 @@ extension PlayerModel {
|
||||
func loadQueueVideoDetails(_ item: PlayerQueueItem) {
|
||||
guard !accounts.current.isNil, !item.hasDetailsLoaded else { return }
|
||||
|
||||
playerAPI.loadDetails(item, completionHandler: { newItem in
|
||||
if let index = self.queue.firstIndex(where: { $0.id == item.id }) {
|
||||
self.queue[index] = newItem
|
||||
let videoID = item.video?.videoID ?? item.videoID
|
||||
|
||||
if queueItemBeingLoaded == nil {
|
||||
logger.info("loading queue details: \(videoID)")
|
||||
queueItemBeingLoaded = item
|
||||
} else {
|
||||
logger.info("POSTPONING details load: \(videoID)")
|
||||
queueItemsToLoad.append(item)
|
||||
return
|
||||
}
|
||||
|
||||
playerAPI.loadDetails(item, completionHandler: { [weak self] newItem in
|
||||
guard let self else { return }
|
||||
|
||||
self.queue.filter { $0.videoID == item.videoID }.forEach { item in
|
||||
if let index = self.queue.firstIndex(of: item) {
|
||||
self.queue[index] = newItem
|
||||
}
|
||||
}
|
||||
|
||||
self.logger.info("LOADED queue details: \(videoID)")
|
||||
|
||||
if self.queueItemBeingLoaded == item {
|
||||
self.logger.info("setting nothing loaded")
|
||||
self.queueItemBeingLoaded = nil
|
||||
}
|
||||
|
||||
if let item = self.queueItemsToLoad.popLast() {
|
||||
self.loadQueueVideoDetails(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -42,7 +42,8 @@ struct PlayerQueueItem: Hashable, Identifiable, Defaults.Serializable {
|
||||
}
|
||||
|
||||
var hasDetailsLoaded: Bool {
|
||||
!video.isNil
|
||||
guard let video else { return false }
|
||||
return !video.streams.isEmpty
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
|
@ -25,7 +25,13 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
|
||||
}
|
||||
}
|
||||
|
||||
var localURL = ""
|
||||
if let video = value.video, video.isLocal {
|
||||
localURL = video.localStream?.localURL.absoluteString ?? ""
|
||||
}
|
||||
|
||||
return [
|
||||
"localURL": localURL,
|
||||
"videoID": value.videoID,
|
||||
"playbackTime": playbackTime,
|
||||
"videoDuration": videoDuration
|
||||
@ -33,12 +39,7 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
|
||||
}
|
||||
|
||||
func deserialize(_ object: Serializable?) -> Value? {
|
||||
guard
|
||||
let object,
|
||||
let videoID = object["videoID"]
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
guard let object else { return nil }
|
||||
|
||||
var playbackTime: CMTime?
|
||||
var videoDuration: TimeInterval?
|
||||
@ -56,6 +57,19 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
|
||||
videoDuration = TimeInterval(duration)
|
||||
}
|
||||
|
||||
if let localUrlString = object["localURL"],
|
||||
!localUrlString.isEmpty,
|
||||
let localURL = URL(string: localUrlString)
|
||||
{
|
||||
return PlayerQueueItem(
|
||||
.local(localURL),
|
||||
playbackTime: playbackTime,
|
||||
videoDuration: videoDuration
|
||||
)
|
||||
}
|
||||
|
||||
guard let videoID = object["videoID"] else { return nil }
|
||||
|
||||
return PlayerQueueItem(
|
||||
videoID: videoID,
|
||||
playbackTime: playbackTime,
|
||||
|
@ -141,6 +141,7 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
var audioAsset: AVURLAsset!
|
||||
var videoAsset: AVURLAsset!
|
||||
var hlsURL: URL!
|
||||
var localURL: URL!
|
||||
|
||||
var resolution: Resolution!
|
||||
var kind: Kind!
|
||||
@ -154,6 +155,7 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
audioAsset: AVURLAsset? = nil,
|
||||
videoAsset: AVURLAsset? = nil,
|
||||
hlsURL: URL? = nil,
|
||||
localURL: URL? = nil,
|
||||
resolution: Resolution? = nil,
|
||||
kind: Kind = .hls,
|
||||
encoding: String? = nil,
|
||||
@ -163,17 +165,25 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
self.audioAsset = audioAsset
|
||||
self.videoAsset = videoAsset
|
||||
self.hlsURL = hlsURL
|
||||
self.localURL = localURL
|
||||
self.resolution = resolution
|
||||
self.kind = kind
|
||||
self.encoding = encoding
|
||||
format = .from(videoFormat ?? "")
|
||||
}
|
||||
|
||||
var isLocal: Bool {
|
||||
localURL != nil
|
||||
}
|
||||
|
||||
var quality: String {
|
||||
kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
|
||||
guard localURL.isNil else { return "Opened File" }
|
||||
return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
|
||||
}
|
||||
|
||||
var shortQuality: String {
|
||||
guard localURL.isNil else { return "File" }
|
||||
|
||||
if kind == .hls {
|
||||
return "HLS"
|
||||
} else {
|
||||
@ -182,6 +192,7 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
guard localURL.isNil else { return resolutionAndFormat }
|
||||
let instanceString = instance.isNil ? "" : " - (\(instance!.description))"
|
||||
return "\(resolutionAndFormat)\(instanceString)"
|
||||
}
|
||||
@ -200,6 +211,10 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
var singleAssetURL: URL? {
|
||||
guard localURL.isNil else {
|
||||
return URLBookmarkModel.shared.loadBookmark(localURL) ?? localURL
|
||||
}
|
||||
|
||||
if kind == .hls {
|
||||
return hlsURL
|
||||
} else if videoAssetContainsAudio {
|
||||
|
59
Model/URLBookmarkModel.swift
Normal file
59
Model/URLBookmarkModel.swift
Normal file
@ -0,0 +1,59 @@
|
||||
import Cache
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
struct URLBookmarkModel {
|
||||
static var shared = URLBookmarkModel()
|
||||
var logger = Logger(label: "stream.yattee.url-bookmark")
|
||||
|
||||
func saveBookmark(_ url: URL) {
|
||||
if let bookmarkData = try? url.bookmarkData(options: bookmarkCreationOptions, includingResourceValuesForKeys: nil, relativeTo: nil) {
|
||||
try? CacheModel.shared.urlBookmarksStorage?.setObject(bookmarkData, forKey: url.absoluteString)
|
||||
logger.info("saved bookmark for \(url.absoluteString)")
|
||||
}
|
||||
}
|
||||
|
||||
func loadBookmark(_ url: URL) -> URL? {
|
||||
logger.info("loading bookmark for \(url.absoluteString)")
|
||||
|
||||
guard let data = try? CacheModel.shared.urlBookmarksStorage?.object(forKey: url.absoluteString) else {
|
||||
logger.info("bookmark for \(url.absoluteString) not found")
|
||||
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
var isStale = false
|
||||
let url = try URL(
|
||||
resolvingBookmarkData: data,
|
||||
options: bookmarkResolutionOptions,
|
||||
relativeTo: nil,
|
||||
bookmarkDataIsStale: &isStale
|
||||
)
|
||||
if isStale {
|
||||
saveBookmark(url)
|
||||
}
|
||||
logger.info("loaded bookmark for \(url.absoluteString)")
|
||||
|
||||
return url
|
||||
} catch {
|
||||
print("Error resolving bookmark:", error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var bookmarkCreationOptions: URL.BookmarkCreationOptions {
|
||||
#if os(macOS)
|
||||
return [.withSecurityScope, .securityScopeAllowOnlyReadAccess]
|
||||
#else
|
||||
return []
|
||||
#endif
|
||||
}
|
||||
|
||||
var bookmarkResolutionOptions: URL.BookmarkResolutionOptions {
|
||||
#if os(macOS)
|
||||
return [.withSecurityScope]
|
||||
#else
|
||||
return []
|
||||
#endif
|
||||
}
|
||||
}
|
@ -5,6 +5,12 @@ import SwiftUI
|
||||
import SwiftyJSON
|
||||
|
||||
struct Video: Identifiable, Equatable, Hashable {
|
||||
enum VideoID {
|
||||
static func isValid(_ id: Video.ID) -> Bool {
|
||||
id.count == 11
|
||||
}
|
||||
}
|
||||
|
||||
let id: String
|
||||
let videoID: String
|
||||
var title: String
|
||||
@ -84,6 +90,33 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
self.captions = captions
|
||||
}
|
||||
|
||||
static func local(_ url: URL) -> Video {
|
||||
Video(
|
||||
videoID: url.absoluteString,
|
||||
streams: [.init(localURL: url)]
|
||||
)
|
||||
}
|
||||
|
||||
var isLocal: Bool {
|
||||
!VideoID.isValid(videoID)
|
||||
}
|
||||
|
||||
var displayTitle: String {
|
||||
if isLocal {
|
||||
return localStreamFileName ?? localStream?.description ?? title
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
var displayAuthor: String {
|
||||
if isLocal, localStreamIsRemoteURL {
|
||||
return remoteUrlHost ?? "Unknown"
|
||||
}
|
||||
|
||||
return author
|
||||
}
|
||||
|
||||
var publishedDate: String? {
|
||||
(published.isEmpty || published == "0 seconds ago") ? nil : published
|
||||
}
|
||||
@ -133,4 +166,42 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
predicate: NSPredicate(format: "videoID = %@", videoID)
|
||||
)
|
||||
}
|
||||
|
||||
var localStream: Stream? {
|
||||
guard isLocal else { return nil }
|
||||
return streams.first
|
||||
}
|
||||
|
||||
var localStreamIsFile: Bool {
|
||||
guard let localStream else { return false }
|
||||
return localStream.localURL.isFileURL
|
||||
}
|
||||
|
||||
var localStreamIsRemoteURL: Bool {
|
||||
guard let localStream else { return false }
|
||||
return !localStream.localURL.isFileURL
|
||||
}
|
||||
|
||||
var remoteUrlHost: String? {
|
||||
localStreamURLComponents?.host
|
||||
}
|
||||
|
||||
var localStreamFileName: String? {
|
||||
guard let path = localStream?.localURL?.lastPathComponent else { return nil }
|
||||
|
||||
if let localStreamFileExtension {
|
||||
return String(path.dropLast(localStreamFileExtension.count + 1))
|
||||
}
|
||||
return String(path)
|
||||
}
|
||||
|
||||
var localStreamFileExtension: String? {
|
||||
guard let path = localStreamURLComponents?.path else { return nil }
|
||||
return path.contains(".") ? path.components(separatedBy: ".").last?.uppercased() : nil
|
||||
}
|
||||
|
||||
private var localStreamURLComponents: URLComponents? {
|
||||
guard let localStream else { return nil }
|
||||
return URLComponents(url: localStream.localURL, resolvingAgainstBaseURL: false)
|
||||
}
|
||||
}
|
||||
|
23
Model/VideoCacheModel.swift
Normal file
23
Model/VideoCacheModel.swift
Normal file
@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
import Logging
|
||||
import SwiftyJSON
|
||||
|
||||
struct VideoCacheModel {
|
||||
static let shared = VideoCacheModel()
|
||||
var logger = Logger(label: "stream.yattee.video-cache")
|
||||
|
||||
func saveVideo(id: Video.ID, app: VideosApp, json: JSON) {
|
||||
guard !json.isEmpty else { return }
|
||||
var jsonWithApp = json
|
||||
jsonWithApp["app"].string = app.rawValue
|
||||
try! CacheModel.shared.videoStorage!.setObject(jsonWithApp, forKey: id)
|
||||
logger.info("saving video \(id)")
|
||||
}
|
||||
|
||||
func loadVideo(id: Video.ID) -> JSON? {
|
||||
logger.info("loading video \(id)")
|
||||
|
||||
let json = try? CacheModel.shared.videoStorage?.object(forKey: id)
|
||||
return json
|
||||
}
|
||||
}
|
@ -79,9 +79,10 @@ extension Watch {
|
||||
}
|
||||
|
||||
var video: Video {
|
||||
Video(
|
||||
videoID: videoID, title: "", author: "",
|
||||
length: 0, published: "", views: -1, channel: Channel(id: "", name: "")
|
||||
)
|
||||
if !Video.VideoID.isValid(videoID), let url = URL(string: videoID) {
|
||||
return .local(url)
|
||||
}
|
||||
|
||||
return Video(videoID: videoID)
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ extension Defaults.Keys {
|
||||
|
||||
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
||||
|
||||
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 30)
|
||||
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
|
||||
|
||||
#if !os(tvOS)
|
||||
|
@ -17,6 +17,7 @@ struct HomeView: View {
|
||||
#if !os(tvOS)
|
||||
@Default(.favorites) private var favorites
|
||||
#endif
|
||||
@Default(.homeHistoryItems) private var homeHistoryItems
|
||||
|
||||
private var navigation: NavigationModel { .shared }
|
||||
|
||||
@ -56,7 +57,7 @@ struct HomeView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HistoryView(limit: 100)
|
||||
HistoryView(limit: homeHistoryItems)
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
|
@ -5,10 +5,18 @@ struct MenuCommands: Commands {
|
||||
@Binding var model: MenuModel
|
||||
|
||||
var body: some Commands {
|
||||
openVideosMenu
|
||||
navigationMenu
|
||||
playbackMenu
|
||||
}
|
||||
|
||||
private var openVideosMenu: some Commands {
|
||||
CommandGroup(after: .newItem) {
|
||||
Button("Open Videos...") { model.navigation?.presentingOpenVideos = true }
|
||||
.keyboardShortcut("t")
|
||||
}
|
||||
}
|
||||
|
||||
private var navigationMenu: some Commands {
|
||||
CommandGroup(before: .windowSize) {
|
||||
Button("Home") {
|
||||
|
@ -6,13 +6,13 @@ import SwiftUI
|
||||
|
||||
struct AppSidebarNavigation: View {
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
|
||||
#if os(iOS)
|
||||
@State private var didApplyPrimaryViewWorkAround = false
|
||||
|
||||
@EnvironmentObject<CommentsModel> private var comments
|
||||
@EnvironmentObject<InstancesModel> private var instances
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@ -74,7 +74,15 @@ struct AppSidebarNavigation: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
ToolbarItem(placement: accountsMenuToolbarItemPlacement) {
|
||||
ToolbarItemGroup(placement: openVideosToolbarItemPlacement) {
|
||||
Button {
|
||||
navigation.presentingOpenVideos = true
|
||||
} label: {
|
||||
Label("Open Videos", systemImage: "play.circle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: accountsMenuToolbarItemPlacement) {
|
||||
AccountsMenuView()
|
||||
.help(
|
||||
"Switch Instances and Accounts\n" +
|
||||
@ -96,6 +104,14 @@ struct AppSidebarNavigation: View {
|
||||
}
|
||||
}
|
||||
|
||||
var openVideosToolbarItemPlacement: ToolbarItemPlacement {
|
||||
#if os(iOS)
|
||||
return .navigationBarLeading
|
||||
#else
|
||||
return .automatic
|
||||
#endif
|
||||
}
|
||||
|
||||
var accountsMenuToolbarItemPlacement: ToolbarItemPlacement {
|
||||
#if os(iOS)
|
||||
return .bottomBar
|
||||
|
@ -139,6 +139,9 @@ struct AppTabNavigation: View {
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button(action: { navigation.presentingOpenVideos = true }) {
|
||||
Label("Open Videos", systemImage: "play.circle.fill")
|
||||
}
|
||||
AccountsMenuView()
|
||||
}
|
||||
}
|
||||
|
@ -119,6 +119,11 @@ struct ContentView: View {
|
||||
}
|
||||
)
|
||||
#endif
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $navigation.presentingOpenVideos) {
|
||||
OpenVideosView()
|
||||
}
|
||||
)
|
||||
.background(playerViewInitialize)
|
||||
.alert(isPresented: $navigation.presentingAlert) { navigation.alert }
|
||||
}
|
||||
|
@ -27,6 +27,11 @@ struct OpenURLHandler {
|
||||
}
|
||||
#endif
|
||||
|
||||
if url.isFileURL {
|
||||
OpenVideosModel.shared.open(url)
|
||||
return
|
||||
}
|
||||
|
||||
let parser = URLParser(url: urlByReplacingYatteeProtocol(url))
|
||||
|
||||
switch parser.destination {
|
||||
|
@ -9,7 +9,7 @@ struct PlaybackStatsView: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
mpvPlaybackStatRow("Hardware decoder".localized(), player.mpvBackend.hwDecoder)
|
||||
mpvPlaybackStatRow("Dropped frames".localized(), String(player.mpvBackend.frameDropCount))
|
||||
mpvPlaybackStatRow("Stream FPS".localized(), String(format: "%.2ffps", player.mpvBackend.outputFps))
|
||||
mpvPlaybackStatRow("Stream FPS".localized(), player.mpvBackend.formattedOutputFps)
|
||||
mpvPlaybackStatRow("Cached time".localized(), String(format: "%.2fs", player.mpvBackend.cacheDuration))
|
||||
}
|
||||
.padding(.top, 2)
|
||||
|
@ -34,6 +34,7 @@ struct PlayerQueueRow: View {
|
||||
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
|
||||
|
||||
player.videoBeingOpened = item.video
|
||||
player.show()
|
||||
|
||||
if history {
|
||||
player.playHistory(item, at: watchStoppedAt)
|
||||
@ -72,3 +73,11 @@ struct PlayerQueueRow: View {
|
||||
return .secondsInDefaultTimescale(seconds)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerQueueRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PlayerQueueRow(item: .init(
|
||||
.local(URL(string: "https://apple.com")!)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -74,18 +74,20 @@ struct VideoDetails: View {
|
||||
"Info".localized(),
|
||||
"info.circle", .info, !video.isNil
|
||||
)
|
||||
pageButton(
|
||||
"Chapters".localized(),
|
||||
"bookmark", .chapters, !(video?.chapters.isEmpty ?? true)
|
||||
)
|
||||
pageButton(
|
||||
"Comments".localized(),
|
||||
"text.bubble", .comments, !video.isNil
|
||||
) { comments.load() }
|
||||
pageButton(
|
||||
"Related".localized(),
|
||||
"rectangle.stack.fill", .related, !video.isNil
|
||||
)
|
||||
if let video, !video.isLocal {
|
||||
pageButton(
|
||||
"Chapters".localized(),
|
||||
"bookmark", .chapters, !video.chapters.isEmpty && !video.isLocal
|
||||
)
|
||||
pageButton(
|
||||
"Comments".localized(),
|
||||
"text.bubble", .comments, !video.isLocal
|
||||
) { comments.load() }
|
||||
pageButton(
|
||||
"Related".localized(),
|
||||
"rectangle.stack.fill", .related, !video.isLocal
|
||||
)
|
||||
}
|
||||
pageButton(
|
||||
"Queue".localized(),
|
||||
"list.number", .queue, !player.queue.isEmpty
|
||||
@ -100,6 +102,11 @@ struct VideoDetails: View {
|
||||
Pager(page: page, data: DetailsPage.allCases, id: \.self) {
|
||||
if !player.currentItem.isNil || page.index == DetailsPage.queue.index {
|
||||
detailsByPage($0)
|
||||
#if os(iOS)
|
||||
.padding(.bottom, SafeArea.insets.bottom)
|
||||
#else
|
||||
.padding(.bottom, 6)
|
||||
#endif
|
||||
} else {
|
||||
VStack {}
|
||||
}
|
||||
@ -156,7 +163,7 @@ struct VideoDetails: View {
|
||||
}
|
||||
|
||||
private var contentItem: ContentItem {
|
||||
ContentItem(video: player.currentVideo!)
|
||||
ContentItem(video: player.currentVideo)
|
||||
}
|
||||
|
||||
func pageButton(
|
||||
@ -228,12 +235,14 @@ struct VideoDetails: View {
|
||||
var detailsPage: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let video {
|
||||
VStack(spacing: 6) {
|
||||
videoProperties
|
||||
if !video.isLocal {
|
||||
VStack(spacing: 6) {
|
||||
videoProperties
|
||||
|
||||
Divider()
|
||||
Divider()
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) {
|
||||
@ -248,16 +257,82 @@ struct VideoDetails: View {
|
||||
#if os(iOS)
|
||||
.padding(.bottom, player.playingFullScreen ? 10 : SafeArea.insets.bottom)
|
||||
#endif
|
||||
} else {
|
||||
} else if !video.isLocal {
|
||||
Text("No description")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Group {
|
||||
if player.activeBackend == .mpv, player.mpvBackend.videoFormat != "unknown" {
|
||||
videoDetailGroupHeading("Video")
|
||||
|
||||
videoDetailRow("Format", value: player.mpvBackend.videoFormat)
|
||||
videoDetailRow("Codec", value: player.mpvBackend.videoCodec)
|
||||
videoDetailRow("Hardware Decoder", value: player.mpvBackend.hwDecoder)
|
||||
videoDetailRow("Driver", value: player.mpvBackend.currentVo)
|
||||
videoDetailRow("Size", value: player.formattedSize)
|
||||
videoDetailRow("FPS", value: player.mpvBackend.formattedOutputFps)
|
||||
} else if player.activeBackend == .appleAVPlayer, let width = player.backend.videoWidth, width > 0 {
|
||||
videoDetailGroupHeading("Video")
|
||||
videoDetailRow("Size", value: player.formattedSize)
|
||||
}
|
||||
}
|
||||
|
||||
if player.activeBackend == .mpv, player.mpvBackend.audioFormat != "unknown" {
|
||||
Group {
|
||||
videoDetailGroupHeading("Audio")
|
||||
videoDetailRow("Format", value: player.mpvBackend.audioFormat)
|
||||
videoDetailRow("Codec", value: player.mpvBackend.audioCodec)
|
||||
videoDetailRow("Driver", value: player.mpvBackend.currentAo)
|
||||
videoDetailRow("Channels", value: player.mpvBackend.audioChannels)
|
||||
videoDetailRow("Sample Rate", value: player.mpvBackend.audioSampleRate)
|
||||
}
|
||||
}
|
||||
|
||||
if video.localStream != nil || video.localStreamFileExtension != nil {
|
||||
videoDetailGroupHeading("File")
|
||||
}
|
||||
|
||||
if let fileExtension = video.localStreamFileExtension {
|
||||
videoDetailRow("File Extension", value: fileExtension)
|
||||
}
|
||||
|
||||
if let url = video.localStream?.localURL, video.localStreamIsRemoteURL {
|
||||
videoDetailRow("URL", value: url.absoluteString)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
@ViewBuilder func videoDetailGroupHeading(_ heading: String) -> some View {
|
||||
Text(heading.uppercased())
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
@ViewBuilder func videoDetailRow(_ detail: String, value: String) -> some View {
|
||||
HStack {
|
||||
Text(detail)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
let value = Text(value)
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
value
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
@ViewBuilder var videoProperties: some View {
|
||||
HStack(spacing: 2) {
|
||||
publishedDateSection
|
||||
|
@ -328,7 +328,6 @@ struct VideoPlayerView: View {
|
||||
if !fullScreenPlayer {
|
||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
|
||||
#if os(iOS)
|
||||
// .zIndex(-1)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
#endif
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
|
@ -24,15 +24,38 @@ struct VideoBanner: View {
|
||||
#endif
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(video?.title ?? "Loading...".localized())
|
||||
.truncationMode(.middle)
|
||||
.lineLimit(2)
|
||||
.font(.headline)
|
||||
.frame(alignment: .leading)
|
||||
Group {
|
||||
if let video {
|
||||
HStack(alignment: .top) {
|
||||
Text(video.displayTitle + "\n")
|
||||
if video.isLocal, let fileExtension = video.localStreamFileExtension {
|
||||
Spacer()
|
||||
Text(fileExtension)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Loading contents of the video, please wait")
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
.truncationMode(.middle)
|
||||
.lineLimit(2)
|
||||
.font(.headline)
|
||||
.frame(alignment: .leading)
|
||||
|
||||
HStack {
|
||||
Text(video?.author ?? "")
|
||||
.lineLimit(1)
|
||||
Group {
|
||||
if let video {
|
||||
if !video.isLocal || video.localStreamIsRemoteURL {
|
||||
Text(video.displayAuthor)
|
||||
}
|
||||
} else {
|
||||
Text("Video Author")
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
@ -40,10 +63,8 @@ struct VideoBanner: View {
|
||||
progressView
|
||||
#endif
|
||||
|
||||
if let time = (videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() {
|
||||
Text(time)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
Text((videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() ?? PlayerTimeModel.timePlaceholder)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@ -71,20 +92,30 @@ struct VideoBanner: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private var smallThumbnail: some View {
|
||||
let url = video?.thumbnailURL(quality: .medium)
|
||||
|
||||
WebImage(url: url)
|
||||
.resizable()
|
||||
.placeholder {
|
||||
ProgressView()
|
||||
Group {
|
||||
if let video {
|
||||
if let thumbnail = video.thumbnailURL(quality: .medium) {
|
||||
WebImage(url: thumbnail)
|
||||
.resizable()
|
||||
.placeholder {
|
||||
ProgressView()
|
||||
}
|
||||
.indicator(.activity)
|
||||
} else if video.localStreamIsFile {
|
||||
Image(systemName: "folder")
|
||||
} else if video.localStreamIsRemoteURL {
|
||||
Image(systemName: "globe")
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
.indicator(.activity)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(width: thumbnailWidth, height: 140)
|
||||
.mask(RoundedRectangle(cornerRadius: 12))
|
||||
.frame(width: thumbnailWidth, height: thumbnailHeight)
|
||||
.mask(RoundedRectangle(cornerRadius: 12))
|
||||
#else
|
||||
.frame(width: thumbnailWidth, height: 60)
|
||||
.mask(RoundedRectangle(cornerRadius: 6))
|
||||
.frame(width: thumbnailWidth, height: thumbnailHeight)
|
||||
.mask(RoundedRectangle(cornerRadius: 6))
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -96,6 +127,14 @@ struct VideoBanner: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
private var thumbnailHeight: Double {
|
||||
#if os(tvOS)
|
||||
140
|
||||
#else
|
||||
60
|
||||
#endif
|
||||
}
|
||||
|
||||
private var progressView: some View {
|
||||
Group {
|
||||
if !playbackTime.isNil, !(video?.live ?? false) {
|
||||
@ -120,6 +159,9 @@ struct VideoBanner_Previews: PreviewProvider {
|
||||
VStack(spacing: 20) {
|
||||
VideoBanner(video: Video.fixture, playbackTime: CMTime(seconds: 400, preferredTimescale: 10000))
|
||||
VideoBanner(video: Video.fixtureUpcomingWithoutPublishedOrViews)
|
||||
VideoBanner(video: .local(URL(string: "https://apple.com/a/directory/of/video+that+has+very+long+title+that+will+likely.mp4")!))
|
||||
VideoBanner(video: .local(URL(string: "file://a/b/c/d/e/f.mkv")!))
|
||||
VideoBanner()
|
||||
}
|
||||
.frame(maxWidth: 900)
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ struct ControlsBar: View {
|
||||
if let video = model.currentVideo {
|
||||
Group {
|
||||
Section {
|
||||
if accounts.app.supportsUserPlaylists && accounts.signedIn {
|
||||
if accounts.app.supportsUserPlaylists && accounts.signedIn, !video.isLocal {
|
||||
Section {
|
||||
Button {
|
||||
navigation.presentAddToPlaylist(video)
|
||||
@ -180,36 +180,38 @@ struct ControlsBar: View {
|
||||
#endif
|
||||
|
||||
Section {
|
||||
Button {
|
||||
NavigationModel.openChannel(
|
||||
video.channel,
|
||||
player: model,
|
||||
recents: recents,
|
||||
navigation: navigation,
|
||||
navigationStyle: navigationStyle
|
||||
)
|
||||
} label: {
|
||||
Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
|
||||
}
|
||||
if !video.isLocal {
|
||||
Button {
|
||||
NavigationModel.openChannel(
|
||||
video.channel,
|
||||
player: model,
|
||||
recents: recents,
|
||||
navigation: navigation,
|
||||
navigationStyle: navigationStyle
|
||||
)
|
||||
} label: {
|
||||
Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
|
||||
}
|
||||
|
||||
if accounts.app.supportsSubscriptions, accounts.signedIn {
|
||||
if subscriptions.isSubscribing(video.channel.id) {
|
||||
Button {
|
||||
#if os(tvOS)
|
||||
subscriptions.unsubscribe(video.channel.id)
|
||||
#else
|
||||
navigation.presentUnsubscribeAlert(video.channel, subscriptions: subscriptions)
|
||||
#endif
|
||||
} label: {
|
||||
Label("Unsubscribe", systemImage: "xmark.circle")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
subscriptions.subscribe(video.channel.id) {
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
if accounts.app.supportsSubscriptions, accounts.signedIn {
|
||||
if subscriptions.isSubscribing(video.channel.id) {
|
||||
Button {
|
||||
#if os(tvOS)
|
||||
subscriptions.unsubscribe(video.channel.id)
|
||||
#else
|
||||
navigation.presentUnsubscribeAlert(video.channel, subscriptions: subscriptions)
|
||||
#endif
|
||||
} label: {
|
||||
Label("Unsubscribe", systemImage: "xmark.circle")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
subscriptions.subscribe(video.channel.id) {
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
}
|
||||
} label: {
|
||||
Label("Subscribe", systemImage: "star.circle")
|
||||
}
|
||||
} label: {
|
||||
Label("Subscribe", systemImage: "star.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,7 +230,7 @@ struct ControlsBar: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
let notPlaying = "Not Playing".localized()
|
||||
Text(model.currentVideo?.title ?? notPlaying)
|
||||
Text(model.currentVideo?.displayTitle ?? notPlaying)
|
||||
.font(.system(size: 14))
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(model.currentVideo.isNil ? .secondary : .accentColor)
|
||||
@ -236,12 +238,12 @@ struct ControlsBar: View {
|
||||
.lineLimit(titleLineLimit)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
if let video = model.currentVideo {
|
||||
if let video = model.currentVideo, !video.localStreamIsFile {
|
||||
HStack(spacing: 2) {
|
||||
Text(video.author)
|
||||
Text(video.displayAuthor)
|
||||
.font(.system(size: 12))
|
||||
|
||||
if !presentingControls {
|
||||
if !presentingControls && !video.isLocal {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "person.2.fill")
|
||||
|
||||
@ -271,7 +273,7 @@ struct ControlsBar: View {
|
||||
|
||||
private var authorAvatar: some View {
|
||||
Group {
|
||||
if let video = model.currentItem?.video, let url = video.channel.thumbnailURL {
|
||||
if let url = model.currentItem?.video?.channel.thumbnailURL {
|
||||
WebImage(url: url)
|
||||
.resizable()
|
||||
.placeholder {
|
||||
@ -284,10 +286,20 @@ struct ControlsBar: View {
|
||||
Color(white: 0.6)
|
||||
.opacity(0.5)
|
||||
|
||||
Image(systemName: "play.rectangle")
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 20))
|
||||
.contentShape(Rectangle())
|
||||
Group {
|
||||
if let video = model.currentItem?.video, video.isLocal {
|
||||
if video.localStreamIsFile {
|
||||
Image(systemName: "folder")
|
||||
} else if video.localStreamIsRemoteURL {
|
||||
Image(systemName: "globe")
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "play.rectangle")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 20))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
167
Shared/Views/OpenVideosView.swift
Normal file
167
Shared/Views/OpenVideosView.swift
Normal file
@ -0,0 +1,167 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OpenVideosView: View {
|
||||
@State private var presentingFileImporter = false
|
||||
@State private var urlsToOpenText = "https://r.yattee.stream/demo/mp4/1.mp4\nhttps://r.yattee.stream/demo/mp4/2.mp4\nhttps://r.yattee.stream/demo/mp4/3.mp4\nhttps://www.youtube.com/watch?v=N9WHp8DG2WY"
|
||||
@State private var playbackMode = OpenVideosModel.PlaybackMode.playNow
|
||||
@State private var removeQueueItems = false
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
|
||||
@Environment(\.openURL) private var openURL
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
openVideos
|
||||
.frame(minWidth: 600, maxWidth: 800, minHeight: 250)
|
||||
#else
|
||||
NavigationView {
|
||||
openVideos
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: { presentationMode.wrappedValue.dismiss() }) {
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.navigationTitle("Open Videos")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var openVideos: some View {
|
||||
VStack(alignment: .leading) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
#if os(tvOS)
|
||||
TextField("URLs to Open", text: $urlsToOpenText)
|
||||
#else
|
||||
TextEditor(text: $urlsToOpenText)
|
||||
.padding(2)
|
||||
.border(Color(white: 0.8), width: 1)
|
||||
.frame(maxHeight: 200)
|
||||
#if !os(macOS)
|
||||
.keyboardType(.URL)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
Text("Enter or paste URLs to open, one per line")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Picker("Playback Mode", selection: $playbackMode) {
|
||||
ForEach(OpenVideosModel.PlaybackMode.allCases, id: \.rawValue) { mode in
|
||||
Text(mode.description).tag(mode)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.padding(.bottom, 5)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Toggle(isOn: $removeQueueItems) {
|
||||
Text("Clear queue before opening")
|
||||
}
|
||||
.disabled(!playbackMode.allowsRemovingQueueItems)
|
||||
.padding(.bottom)
|
||||
|
||||
HStack {
|
||||
Group {
|
||||
Button {
|
||||
openURLs(urlsToOpenFromText)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "network")
|
||||
Text("Open URLs")
|
||||
.fontWeight(.bold)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.disabled(urlsToOpenFromText.isEmpty)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
presentingFileImporter = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "folder")
|
||||
Text("Open Files")
|
||||
.fontWeight(.bold)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.foregroundColor(Color.accentColor.opacity(0.33))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
#if !os(tvOS)
|
||||
.fileImporter(
|
||||
isPresented: $presentingFileImporter,
|
||||
allowedContentTypes: [.audiovisualContent],
|
||||
allowsMultipleSelection: true
|
||||
) { result in
|
||||
do {
|
||||
let selectedFiles = try result.get()
|
||||
let urlsToOpen = selectedFiles.map { url in
|
||||
if let bookmarkURL = URLBookmarkModel.shared.loadBookmark(url) {
|
||||
return bookmarkURL
|
||||
}
|
||||
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
URLBookmarkModel.shared.saveBookmark(url)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
openURLs(selectedFiles)
|
||||
} catch {
|
||||
NavigationModel.shared.presentAlert(title: "Could not open Files")
|
||||
}
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var urlsToOpenFromText: [URL] {
|
||||
urlsToOpenText.split(whereSeparator: \.isNewline).compactMap { URL(string: String($0)) }
|
||||
}
|
||||
|
||||
func openURLs(_ urls: [URL]) {
|
||||
OpenVideosModel.shared.openURLs(urls, removeQueueItems: removeQueueItems, playbackMode: playbackMode)
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenVideosView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
OpenVideosView()
|
||||
#if os(iOS)
|
||||
.navigationViewStyle(.stack)
|
||||
#endif
|
||||
}
|
||||
}
|
@ -70,7 +70,7 @@ struct VideoContextMenuView: View {
|
||||
addToQueueButton
|
||||
}
|
||||
|
||||
if accounts.app.supportsUserPlaylists, accounts.signedIn {
|
||||
if accounts.app.supportsUserPlaylists, accounts.signedIn, !video.isLocal {
|
||||
Section {
|
||||
addToPlaylistButton
|
||||
addToLastPlaylistButton
|
||||
@ -87,7 +87,7 @@ struct VideoContextMenuView: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
if !inChannelView, !inChannelPlaylistView {
|
||||
if !inChannelView, !inChannelPlaylistView, !video.isLocal {
|
||||
Section {
|
||||
openChannelButton
|
||||
|
||||
|
@ -4,12 +4,16 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.bookmarks.app-scope</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -394,6 +394,9 @@
|
||||
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; };
|
||||
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; };
|
||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; };
|
||||
3763C989290C7A50004D3B5F /* OpenVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763C988290C7A50004D3B5F /* OpenVideosView.swift */; };
|
||||
3763C98A290C7A50004D3B5F /* OpenVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763C988290C7A50004D3B5F /* OpenVideosView.swift */; };
|
||||
3763C98B290C7A50004D3B5F /* OpenVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763C988290C7A50004D3B5F /* OpenVideosView.swift */; };
|
||||
37648B69286CF5F1003D330B /* TVControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37648B68286CF5F1003D330B /* TVControls.swift */; };
|
||||
376527BB285F60F700102284 /* PlayerTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376527BA285F60F700102284 /* PlayerTimeModel.swift */; };
|
||||
376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376527BA285F60F700102284 /* PlayerTimeModel.swift */; };
|
||||
@ -411,6 +414,9 @@
|
||||
3765917E27237D2A009F956E /* PINCache in Frameworks */ = {isa = PBXBuildFile; productRef = 3765917D27237D2A009F956E /* PINCache */; };
|
||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37666BA927023AF000F869E5 /* AccountSelectionView.swift */; };
|
||||
3766AFD2273DA97D00686348 /* Int+FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */; };
|
||||
376787BC291C4B7B00D356A4 /* VideoCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376787BB291C4B7B00D356A4 /* VideoCacheModel.swift */; };
|
||||
376787BD291C4B7B00D356A4 /* VideoCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376787BB291C4B7B00D356A4 /* VideoCacheModel.swift */; };
|
||||
376787BE291C4B7B00D356A4 /* VideoCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376787BB291C4B7B00D356A4 /* VideoCacheModel.swift */; };
|
||||
3769537928A877C4005D87C3 /* StreamControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3795593527B08538007FF8F4 /* StreamControl.swift */; };
|
||||
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
||||
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
||||
@ -517,6 +523,9 @@
|
||||
377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoCell.swift */; };
|
||||
377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
|
||||
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
|
||||
377FF88B291A60310028EB0B /* OpenVideosModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377FF88A291A60310028EB0B /* OpenVideosModel.swift */; };
|
||||
377FF88C291A60310028EB0B /* OpenVideosModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377FF88A291A60310028EB0B /* OpenVideosModel.swift */; };
|
||||
377FF88D291A60310028EB0B /* OpenVideosModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377FF88A291A60310028EB0B /* OpenVideosModel.swift */; };
|
||||
377FF88F291A99580028EB0B /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377FF88E291A99580028EB0B /* HistoryView.swift */; };
|
||||
377FF890291A99580028EB0B /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377FF88E291A99580028EB0B /* HistoryView.swift */; };
|
||||
377FF891291A99580028EB0B /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377FF88E291A99580028EB0B /* HistoryView.swift */; };
|
||||
@ -537,6 +546,8 @@
|
||||
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
||||
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
||||
3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
||||
3788AD3E291D042D00C53C9B /* Cache in Frameworks */ = {isa = PBXBuildFile; productRef = 3788AD3D291D042D00C53C9B /* Cache */; };
|
||||
3788AD40291D043200C53C9B /* Cache in Frameworks */ = {isa = PBXBuildFile; productRef = 3788AD3F291D043200C53C9B /* Cache */; };
|
||||
378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */; };
|
||||
378AE93C274EDFB2006A4EE1 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBD274DA401005EA4D6 /* Backport.swift */; };
|
||||
378AE93D274EDFB3006A4EE1 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBD274DA401005EA4D6 /* Backport.swift */; };
|
||||
@ -854,6 +865,13 @@
|
||||
37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
||||
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
||||
37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; };
|
||||
37F5E8B4291BE97A006C15F5 /* Cache in Frameworks */ = {isa = PBXBuildFile; productRef = 37F5E8B3291BE97A006C15F5 /* Cache */; };
|
||||
37F5E8B6291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */; };
|
||||
37F5E8B7291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */; };
|
||||
37F5E8B8291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */; };
|
||||
37F5E8BA291BEF69006C15F5 /* CacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B9291BEF69006C15F5 /* CacheModel.swift */; };
|
||||
37F5E8BB291BEF69006C15F5 /* CacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B9291BEF69006C15F5 /* CacheModel.swift */; };
|
||||
37F5E8BC291BEF69006C15F5 /* CacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B9291BEF69006C15F5 /* CacheModel.swift */; };
|
||||
37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; };
|
||||
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; };
|
||||
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; };
|
||||
@ -1139,6 +1157,7 @@
|
||||
375F740F289DC35A00747050 /* PlayerBackendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackendView.swift; sourceTree = "<group>"; };
|
||||
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
||||
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = "<group>"; };
|
||||
3763C988290C7A50004D3B5F /* OpenVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenVideosView.swift; sourceTree = "<group>"; };
|
||||
37648B68286CF5F1003D330B /* TVControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVControls.swift; sourceTree = "<group>"; };
|
||||
376527BA285F60F700102284 /* PlayerTimeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeModel.swift; sourceTree = "<group>"; };
|
||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
|
||||
@ -1147,6 +1166,7 @@
|
||||
37658ED428E1C567004BF6A2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
37666BA927023AF000F869E5 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = "<group>"; };
|
||||
376787BA291C43CD00D356A4 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
376787BB291C4B7B00D356A4 /* VideoCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCacheModel.swift; sourceTree = "<group>"; };
|
||||
3768122C28E8D0BC0036FC8D /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderProgressView.swift; sourceTree = "<group>"; };
|
||||
376A33DF2720CAD6000C1D6B /* VideosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosApp.swift; sourceTree = "<group>"; };
|
||||
@ -1176,6 +1196,7 @@
|
||||
377ABC43286E4B74009C986F /* ManifestedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManifestedInstance.swift; sourceTree = "<group>"; };
|
||||
377ABC47286E5887009C986F /* Sequence+Unique.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Unique.swift"; sourceTree = "<group>"; };
|
||||
377ABC4B286E6A78009C986F /* LocationsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsSettings.swift; sourceTree = "<group>"; };
|
||||
377FF88A291A60310028EB0B /* OpenVideosModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenVideosModel.swift; sourceTree = "<group>"; };
|
||||
377FF88E291A99580028EB0B /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
||||
3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = "<group>"; };
|
||||
3782B9512755667600990149 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = "<group>"; };
|
||||
@ -1326,6 +1347,8 @@
|
||||
37F4AD1E28612DFD004D0F66 /* Buffering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buffering.swift; sourceTree = "<group>"; };
|
||||
37F4AD2528613B81004D0F66 /* Color+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Debug.swift"; sourceTree = "<group>"; };
|
||||
37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; };
|
||||
37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBookmarkModel.swift; sourceTree = "<group>"; };
|
||||
37F5E8B9291BEF69006C15F5 /* CacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheModel.swift; sourceTree = "<group>"; };
|
||||
37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; };
|
||||
37F7AB4C28A9361F00FB46B5 /* UIDevice+Cellular.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Cellular.swift"; sourceTree = "<group>"; };
|
||||
37F7AB4E28A94E0600FB46B5 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
|
||||
@ -1392,6 +1415,7 @@
|
||||
3736A21A286BB72300C9E5EE /* libmpv.xcframework in Frameworks */,
|
||||
3765917C27237D21009F956E /* PINCache in Frameworks */,
|
||||
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */,
|
||||
37F5E8B4291BE97A006C15F5 /* Cache in Frameworks */,
|
||||
3736A20C286BB72300C9E5EE /* libavutil.xcframework in Frameworks */,
|
||||
37FB285627220D9000A57617 /* SDWebImagePINPlugin in Frameworks */,
|
||||
3736A212286BB72300C9E5EE /* libswresample.xcframework in Frameworks */,
|
||||
@ -1431,6 +1455,7 @@
|
||||
370F4FC927CC16CB001B35DC /* libssl.3.dylib in Frameworks */,
|
||||
3703206827D2BB45007A0CB8 /* Defaults in Frameworks */,
|
||||
3703206A27D2BB49007A0CB8 /* Alamofire in Frameworks */,
|
||||
3788AD3E291D042D00C53C9B /* Cache in Frameworks */,
|
||||
370F4FD427CC16CB001B35DC /* libfreetype.6.dylib in Frameworks */,
|
||||
3797104B28D3D18800D5F53C /* SDWebImageSwiftUI in Frameworks */,
|
||||
370F4FE227CC16CB001B35DC /* libXdmcp.6.dylib in Frameworks */,
|
||||
@ -1492,6 +1517,7 @@
|
||||
37FB2849272207F000A57617 /* SDWebImageWebPCoder in Frameworks */,
|
||||
3736A20D286BB72300C9E5EE /* libavutil.xcframework in Frameworks */,
|
||||
3736A213286BB72300C9E5EE /* libswresample.xcframework in Frameworks */,
|
||||
3788AD40291D043200C53C9B /* Cache in Frameworks */,
|
||||
37FB285427220D8400A57617 /* SDWebImagePINPlugin in Frameworks */,
|
||||
3732BFD028B83763009F3F4D /* KeychainAccess in Frameworks */,
|
||||
3772003927E8EEB700CB2475 /* AVFoundation.framework in Frameworks */,
|
||||
@ -1699,6 +1725,7 @@
|
||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
|
||||
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
|
||||
37E70922271CD43000D34DDE /* WelcomeScreen.swift */,
|
||||
3763C988290C7A50004D3B5F /* OpenVideosView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -2143,6 +2170,7 @@
|
||||
3751BA8127E69131007B1A60 /* ReturnYouTubeDislike */,
|
||||
37FB283F2721B20800A57617 /* Search */,
|
||||
374C0539272436DA009BDDBE /* SponsorBlock */,
|
||||
37F5E8B9291BEF69006C15F5 /* CacheModel.swift */,
|
||||
3776ADD5287381240078EBC4 /* Captions.swift */,
|
||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
||||
@ -2161,6 +2189,7 @@
|
||||
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||
3756C2A92861151C00E4B059 /* NetworkStateModel.swift */,
|
||||
377FF88A291A60310028EB0B /* OpenVideosModel.swift */,
|
||||
37130A5E277657300033018A /* PersistenceController.swift */,
|
||||
376578882685471400D4EA09 /* Playlist.swift */,
|
||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||
@ -2178,9 +2207,11 @@
|
||||
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
||||
37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */,
|
||||
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
||||
37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */,
|
||||
37D4B19626717E1500C925CA /* Video.swift */,
|
||||
3784CDDE27772EE40055BBF2 /* Watch.swift */,
|
||||
37130A59277657090033018A /* Yattee.xcdatamodeld */,
|
||||
376787BB291C4B7B00D356A4 /* VideoCacheModel.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@ -2383,6 +2414,7 @@
|
||||
3799AC0828B03CED001376F9 /* ActiveLabel */,
|
||||
375B8AB028B57F4200397B31 /* KeychainAccess */,
|
||||
3797104828D3D10600D5F53C /* SDWebImageSwiftUI */,
|
||||
37F5E8B3291BE97A006C15F5 /* Cache */,
|
||||
);
|
||||
productName = "Yattee (iOS)";
|
||||
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
||||
@ -2420,6 +2452,7 @@
|
||||
372AA413286D06A10000B1DC /* Repeat */,
|
||||
375B8AB628B583BD00397B31 /* KeychainAccess */,
|
||||
3797104A28D3D18800D5F53C /* SDWebImageSwiftUI */,
|
||||
3788AD3D291D042D00C53C9B /* Cache */,
|
||||
);
|
||||
productName = "Yattee (macOS)";
|
||||
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
||||
@ -2497,6 +2530,7 @@
|
||||
37E80F42287B7AAF00561799 /* SwiftUIPager */,
|
||||
3732BFCF28B83763009F3F4D /* KeychainAccess */,
|
||||
3797104C28D3D19100D5F53C /* SDWebImageSwiftUI */,
|
||||
3788AD3F291D043200C53C9B /* Cache */,
|
||||
);
|
||||
productName = Yattee;
|
||||
productReference = 37D4B158267164AE00C925CA /* Yattee.app */;
|
||||
@ -2605,6 +2639,7 @@
|
||||
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */,
|
||||
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||
3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
|
||||
37F5E8B2291BE97A006C15F5 /* XCRemoteSwiftPackageReference "Cache" */,
|
||||
);
|
||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -2866,7 +2901,9 @@
|
||||
3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */,
|
||||
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||
376787BC291C4B7B00D356A4 /* VideoCacheModel.swift in Sources */,
|
||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||
377FF88B291A60310028EB0B /* OpenVideosModel.swift in Sources */,
|
||||
37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||
377ABC40286E4AD5009C986F /* InstancesManifest.swift in Sources */,
|
||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
||||
@ -3019,6 +3056,7 @@
|
||||
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||
3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
|
||||
373031F528383A89000CFD59 /* PiPDelegate.swift in Sources */,
|
||||
37F5E8BA291BEF69006C15F5 /* CacheModel.swift in Sources */,
|
||||
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
|
||||
370015A928BBAE7F000149FD /* ProgressBar.swift in Sources */,
|
||||
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||
@ -3031,6 +3069,7 @@
|
||||
377ABC4C286E6A78009C986F /* LocationsSettings.swift in Sources */,
|
||||
3744A96028B99ADD005DE0A7 /* PlayerControlsLayout.swift in Sources */,
|
||||
376BE50B27349108009AD608 /* BrowsingSettings.swift in Sources */,
|
||||
3763C989290C7A50004D3B5F /* OpenVideosView.swift in Sources */,
|
||||
37F0F4EE286F734400C06C2E /* AdvancedSettings.swift in Sources */,
|
||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||
@ -3051,6 +3090,7 @@
|
||||
375E45F527B1976B00BA7902 /* MPVOGLView.swift in Sources */,
|
||||
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||
37F5E8B6291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */,
|
||||
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */,
|
||||
37030FFB27B0398000ECDDAA /* MPVClient.swift in Sources */,
|
||||
3756C2AA2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||
@ -3132,6 +3172,7 @@
|
||||
3756C2A72861131100E4B059 /* NetworkState.swift in Sources */,
|
||||
37D6025A28C17375009E8D98 /* PlaybackStatsView.swift in Sources */,
|
||||
3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */,
|
||||
3763C98A290C7A50004D3B5F /* OpenVideosView.swift in Sources */,
|
||||
37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
374AB3DC28BCAF7E00DF56FB /* SeekType.swift in Sources */,
|
||||
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
@ -3145,6 +3186,7 @@
|
||||
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||
37EAD870267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||
377FF88C291A60310028EB0B /* OpenVideosModel.swift in Sources */,
|
||||
378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */,
|
||||
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||
376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||
@ -3175,6 +3217,7 @@
|
||||
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||
37F5E8BB291BEF69006C15F5 /* CacheModel.swift in Sources */,
|
||||
3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
37030FFC27B0398000ECDDAA /* MPVClient.swift in Sources */,
|
||||
3751B4B327836902000B7DF4 /* SearchPage.swift in Sources */,
|
||||
@ -3183,6 +3226,7 @@
|
||||
37BF662027308884008CCFB0 /* DropFavoriteOutside.swift in Sources */,
|
||||
3776ADD7287381240078EBC4 /* Captions.swift in Sources */,
|
||||
37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */,
|
||||
37F5E8B7291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */,
|
||||
374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */,
|
||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
||||
@ -3278,6 +3322,7 @@
|
||||
3743B86927216D3600261544 /* ChannelCell.swift in Sources */,
|
||||
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||
3782B95E2755858100990149 /* NSTextField+FocusRingType.swift in Sources */,
|
||||
376787BD291C4B7B00D356A4 /* VideoCacheModel.swift in Sources */,
|
||||
37C3A252272366440087A57A /* ChannelPlaylistView.swift in Sources */,
|
||||
3754B01628B7F84D009717C8 /* Constants.swift in Sources */,
|
||||
37270F1D28E06E3E00856150 /* String+Localizable.swift in Sources */,
|
||||
@ -3373,6 +3418,7 @@
|
||||
37648B69286CF5F1003D330B /* TVControls.swift in Sources */,
|
||||
374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
||||
37BE0BD426A1D47D0092E2DB /* AppleAVPlayerView.swift in Sources */,
|
||||
37F5E8BC291BEF69006C15F5 /* CacheModel.swift in Sources */,
|
||||
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||
3769537928A877C4005D87C3 /* StreamControl.swift in Sources */,
|
||||
3700155D271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
||||
@ -3408,6 +3454,7 @@
|
||||
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||
378AE93E274EDFB4006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||
377FF88D291A60310028EB0B /* OpenVideosModel.swift in Sources */,
|
||||
37F4AD2828613B81004D0F66 /* Color+Debug.swift in Sources */,
|
||||
37E8B0F227B326F30024006F /* Comparable+Clamped.swift in Sources */,
|
||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
||||
@ -3422,6 +3469,7 @@
|
||||
371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
|
||||
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
||||
3756C2AC2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||
37F5E8B8291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */,
|
||||
37A5DBCA285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||
37E80F46287B7AEC00561799 /* PlayerQueueView.swift in Sources */,
|
||||
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
@ -3445,6 +3493,7 @@
|
||||
3784CDE427772EE40055BBF2 /* Watch.swift in Sources */,
|
||||
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */,
|
||||
37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||
376787BE291C4B7B00D356A4 /* VideoCacheModel.swift in Sources */,
|
||||
37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */,
|
||||
375E45F627B1976B00BA7902 /* MPVOGLView.swift in Sources */,
|
||||
375EC95B289EEB8200751258 /* QualityProfileForm.swift in Sources */,
|
||||
@ -3463,6 +3512,7 @@
|
||||
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||
370F4FAA27CC163B001B35DC /* PlayerBackend.swift in Sources */,
|
||||
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
||||
3763C98B290C7A50004D3B5F /* OpenVideosView.swift in Sources */,
|
||||
375F7412289DC35A00747050 /* PlayerBackendView.swift in Sources */,
|
||||
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||
@ -3942,6 +3992,9 @@
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Need camera access to take pictures";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@ -3985,6 +4038,9 @@
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Need camera access to take pictures";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@ -4210,6 +4266,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Yattee;
|
||||
INFOPLIST_KEY_CFBundleName = "Yattee (Apple TV)";
|
||||
INFOPLIST_KEY_CFBundleVersion = 1;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIUserInterfaceStyle = Automatic;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -4247,6 +4304,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Yattee;
|
||||
INFOPLIST_KEY_CFBundleName = "Yattee (Apple TV)";
|
||||
INFOPLIST_KEY_CFBundleVersion = 1;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIUserInterfaceStyle = Automatic;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -4591,6 +4649,14 @@
|
||||
minimumVersion = 5.1.0;
|
||||
};
|
||||
};
|
||||
37F5E8B2291BE97A006C15F5 /* XCRemoteSwiftPackageReference "Cache" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/hyperoslo/Cache.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 6.0.0;
|
||||
};
|
||||
};
|
||||
37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SDWebImage/SDWebImageWebPCoder.git";
|
||||
@ -4730,6 +4796,16 @@
|
||||
package = 37B767DE2678C5BF0098BAA8 /* XCRemoteSwiftPackageReference "swift-log" */;
|
||||
productName = Logging;
|
||||
};
|
||||
3788AD3D291D042D00C53C9B /* Cache */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 37F5E8B2291BE97A006C15F5 /* XCRemoteSwiftPackageReference "Cache" */;
|
||||
productName = Cache;
|
||||
};
|
||||
3788AD3F291D043200C53C9B /* Cache */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 37F5E8B2291BE97A006C15F5 /* XCRemoteSwiftPackageReference "Cache" */;
|
||||
productName = Cache;
|
||||
};
|
||||
3797104828D3D10600D5F53C /* SDWebImageSwiftUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
||||
@ -4825,6 +4901,11 @@
|
||||
package = 37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */;
|
||||
productName = Reachability;
|
||||
};
|
||||
37F5E8B3291BE97A006C15F5 /* Cache */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 37F5E8B2291BE97A006C15F5 /* XCRemoteSwiftPackageReference "Cache" */;
|
||||
productName = Cache;
|
||||
};
|
||||
37FB2848272207F000A57617 /* SDWebImageWebPCoder */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */;
|
||||
|
@ -18,6 +18,15 @@
|
||||
"version" : "5.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cache",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/hyperoslo/Cache.git",
|
||||
"state" : {
|
||||
"revision" : "c7f4d633049c3bd649a353bad36f6c17e9df085f",
|
||||
"version" : "6.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "defaults",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -2,6 +2,39 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>mpeg4Movie</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.mpeg-4</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>url</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.url</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>fileURL</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.file-url</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@ -15,14 +48,6 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Need camera access to take pictures</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
@ -30,5 +55,22 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string></string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -2,6 +2,21 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>mpeg4Movie</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.mpeg-4</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
Loading…
Reference in New Issue
Block a user