mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Quality profiles
This commit is contained in:
parent
57d8698f86
commit
ac9abaec5a
28
Extensions/UIDevice+Cellular.swift
Normal file
28
Extensions/UIDevice+Cellular.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIDevice {
|
||||||
|
/// A Boolean value indicating whether the device has cellular data capabilities (true) or not (false).
|
||||||
|
var hasCellularCapabilites: Bool {
|
||||||
|
var addrs: UnsafeMutablePointer<ifaddrs>?
|
||||||
|
var cursor: UnsafeMutablePointer<ifaddrs>?
|
||||||
|
|
||||||
|
defer { freeifaddrs(addrs) }
|
||||||
|
|
||||||
|
guard getifaddrs(&addrs) == 0 else { return false }
|
||||||
|
cursor = addrs
|
||||||
|
|
||||||
|
while cursor != nil {
|
||||||
|
guard
|
||||||
|
let utf8String = cursor?.pointee.ifa_name,
|
||||||
|
let name = NSString(utf8String: utf8String),
|
||||||
|
name == "pdp_ip0"
|
||||||
|
else {
|
||||||
|
cursor = cursor?.pointee.ifa_next
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -76,6 +76,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var stream: Stream?
|
@Published var stream: Stream?
|
||||||
@Published var currentRate: Float = 1.0 { didSet { backend.setRate(currentRate) } }
|
@Published var currentRate: Float = 1.0 { didSet { backend.setRate(currentRate) } }
|
||||||
|
|
||||||
|
@Published var qualityProfileSelection: QualityProfile? { didSet { handleQualityProfileChange() }}
|
||||||
|
|
||||||
@Published var availableStreams = [Stream]() { didSet { handleAvailableStreamsChange() } }
|
@Published var availableStreams = [Stream]() { didSet { handleAvailableStreamsChange() } }
|
||||||
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
||||||
|
|
||||||
@ -156,6 +158,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
#endif
|
#endif
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
@Default(.qualityProfiles) var qualityProfiles
|
||||||
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||||
@Default(.closePiPOnNavigation) var closePiPOnNavigation
|
@Default(.closePiPOnNavigation) var closePiPOnNavigation
|
||||||
@Default(.closePiPOnOpeningPlayer) var closePiPOnOpeningPlayer
|
@Default(.closePiPOnOpeningPlayer) var closePiPOnOpeningPlayer
|
||||||
@ -421,7 +424,11 @@ final class PlayerModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let stream = preferredStream(availableStreams) else {
|
if let qualityProfileBackend = qualityProfile?.backend, qualityProfileBackend != activeBackend {
|
||||||
|
changeActiveBackend(from: activeBackend, to: qualityProfileBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let stream = streamByQualityProfile else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,12 +452,6 @@ final class PlayerModel: ObservableObject {
|
|||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.backend.setNeedsDrawing(self.presentingPlayer)
|
self.backend.setNeedsDrawing(self.presentingPlayer)
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
if self.presentingPlayer {
|
|
||||||
self.controls.show()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controls.hide()
|
controls.hide()
|
||||||
@ -483,6 +484,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pause()
|
||||||
|
|
||||||
Defaults[.activeBackend] = to
|
Defaults[.activeBackend] = to
|
||||||
self.activeBackend = to
|
self.activeBackend = to
|
||||||
|
|
||||||
@ -496,8 +499,6 @@ final class PlayerModel: ObservableObject {
|
|||||||
musicMode = false
|
musicMode = false
|
||||||
}
|
}
|
||||||
|
|
||||||
inactiveBackends().forEach { $0.pause() }
|
|
||||||
|
|
||||||
let fromBackend: PlayerBackend = from == .appleAVPlayer ? avPlayerBackend : mpvBackend
|
let fromBackend: PlayerBackend = from == .appleAVPlayer ? avPlayerBackend : mpvBackend
|
||||||
let toBackend: PlayerBackend = to == .appleAVPlayer ? avPlayerBackend : mpvBackend
|
let toBackend: PlayerBackend = to == .appleAVPlayer ? avPlayerBackend : mpvBackend
|
||||||
|
|
||||||
@ -516,7 +517,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !backend.canPlay(stream) || (to == .mpv && !stream.hlsURL.isNil) {
|
if !backend.canPlay(stream) || (to == .mpv && !stream.hlsURL.isNil) {
|
||||||
guard let preferredStream = preferredStream(availableStreams) else {
|
guard let preferredStream = streamByQualityProfile else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,8 +534,16 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func inactiveBackends() -> [PlayerBackend] {
|
func handleQualityProfileChange() {
|
||||||
[activeBackend == PlayerBackendType.mpv ? avPlayerBackend : mpvBackend]
|
guard let profile = qualityProfile else { return }
|
||||||
|
|
||||||
|
if activeBackend != profile.backend { changeActiveBackend(from: activeBackend, to: profile.backend) }
|
||||||
|
guard let profileStream = streamByQualityProfile, stream != profileStream else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.streamSelection = profileStream
|
||||||
|
self?.upgradeToStream(profileStream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rateLabel(_ rate: Float) -> String {
|
func rateLabel(_ rate: Float) -> String {
|
||||||
|
@ -39,7 +39,7 @@ extension PlayerModel {
|
|||||||
func playItem(_ item: PlayerQueueItem, at time: CMTime? = nil) {
|
func playItem(_ item: PlayerQueueItem, at time: CMTime? = nil) {
|
||||||
advancing = false
|
advancing = false
|
||||||
|
|
||||||
if !playingInPictureInPicture {
|
if !playingInPictureInPicture, !currentItem.isNil {
|
||||||
backend.closeItem()
|
backend.closeItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,8 +70,21 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func preferredStream(_ streams: [Stream]) -> Stream? {
|
var qualityProfile: QualityProfile? {
|
||||||
backend.bestPlayable(streams.filter { backend.canPlay($0) }, maxResolution: Defaults[.quality])
|
qualityProfileSelection ?? QualityProfilesModel.shared.automaticProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
var streamByQualityProfile: Stream? {
|
||||||
|
let profile = qualityProfile ?? .defaultProfile
|
||||||
|
|
||||||
|
if let streamPreferredForProfile = backend.bestPlayable(
|
||||||
|
availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) },
|
||||||
|
maxResolution: profile.resolution
|
||||||
|
) {
|
||||||
|
return streamPreferredForProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
return backend.bestPlayable(availableStreams.filter { backend.canPlay($0) }, maxResolution: profile.resolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
func advanceToNextItem() {
|
func advanceToNextItem() {
|
||||||
@ -97,6 +110,8 @@ extension PlayerModel {
|
|||||||
|
|
||||||
if let nextItem = nextItem {
|
if let nextItem = nextItem {
|
||||||
advanceToItem(nextItem)
|
advanceToItem(nextItem)
|
||||||
|
} else {
|
||||||
|
advancing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
112
Model/QualityProfile.swift
Normal file
112
Model/QualityProfile.swift
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||||
|
static var bridge = QualityProfileBridge()
|
||||||
|
static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream])
|
||||||
|
static var highQualityProfile = Self(id: "highQuality", backend: .mpv, resolution: .best, formats: [.webm, .mp4, .av1, .avc1])
|
||||||
|
|
||||||
|
enum Format: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
|
case hls
|
||||||
|
case stream
|
||||||
|
case mp4
|
||||||
|
case avc1
|
||||||
|
case av1
|
||||||
|
case webm
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .stream:
|
||||||
|
return "Stream"
|
||||||
|
case .webm:
|
||||||
|
return "WebM"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return rawValue.uppercased()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var streamFormat: Stream.Format? {
|
||||||
|
switch self {
|
||||||
|
case .hls:
|
||||||
|
return nil
|
||||||
|
case .stream:
|
||||||
|
return nil
|
||||||
|
case .mp4:
|
||||||
|
return .mp4
|
||||||
|
case .webm:
|
||||||
|
return .webm
|
||||||
|
case .avc1:
|
||||||
|
return .avc1
|
||||||
|
case .av1:
|
||||||
|
return .av1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = UUID().uuidString
|
||||||
|
|
||||||
|
var name: String?
|
||||||
|
var backend: PlayerBackendType
|
||||||
|
var resolution: ResolutionSetting
|
||||||
|
var formats: [Format]
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
if let name = name, !name.isEmpty { return name }
|
||||||
|
return "\(backend.label) - \(resolution.description) - \(formats.map(\.description).joined(separator: ", "))"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPreferred(_ stream: Stream) -> Bool {
|
||||||
|
if formats.contains(.hls), stream.kind == .hls {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolutionMatch = !stream.resolution.isNil && (resolution == .best || (resolution.value >= stream.resolution))
|
||||||
|
|
||||||
|
if resolutionMatch, formats.contains(.stream), stream.kind == .stream {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatMatch = formats.compactMap(\.streamFormat).contains(stream.format)
|
||||||
|
|
||||||
|
return resolutionMatch && formatMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QualityProfileBridge: Defaults.Bridge {
|
||||||
|
static let formatsSeparator = ","
|
||||||
|
|
||||||
|
typealias Value = QualityProfile
|
||||||
|
typealias Serializable = [String: String]
|
||||||
|
|
||||||
|
func serialize(_ value: Value?) -> Serializable? {
|
||||||
|
guard let value = value else { return nil }
|
||||||
|
|
||||||
|
return [
|
||||||
|
"id": value.id,
|
||||||
|
"name": value.name ?? "",
|
||||||
|
"backend": value.backend.rawValue,
|
||||||
|
"resolution": value.resolution.rawValue,
|
||||||
|
"formats": value.formats.map { $0.rawValue }.joined(separator: Self.formatsSeparator)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
func deserialize(_ object: Serializable?) -> Value? {
|
||||||
|
guard let object = object,
|
||||||
|
let id = object["id"],
|
||||||
|
let backend = PlayerBackendType(rawValue: object["backend"] ?? ""),
|
||||||
|
let resolution = ResolutionSetting(rawValue: object["resolution"] ?? "")
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = object["name"]
|
||||||
|
let formats = (object["formats"] ?? "").components(separatedBy: Self.formatsSeparator).compactMap { QualityProfile.Format(rawValue: $0) }
|
||||||
|
|
||||||
|
return .init(id: id, name: name, backend: backend, resolution: resolution, formats: formats)
|
||||||
|
}
|
||||||
|
}
|
102
Model/QualityProfilesModel.swift
Normal file
102
Model/QualityProfilesModel.swift
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
#if os(iOS)
|
||||||
|
import Reachability
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct QualityProfilesModel {
|
||||||
|
static let shared = QualityProfilesModel()
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
var tvOSProfile: QualityProfile? {
|
||||||
|
find(Defaults[.batteryNonCellularProfile])
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
func find(_ id: QualityProfile.ID) -> QualityProfile? {
|
||||||
|
if id == "default" {
|
||||||
|
return QualityProfile.defaultProfile
|
||||||
|
} else if id == "highQuality" {
|
||||||
|
return QualityProfile.highQualityProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
return Defaults[.qualityProfiles].first { $0.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(_ qualityProfile: QualityProfile) {
|
||||||
|
Defaults[.qualityProfiles].append(qualityProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(_ from: QualityProfile, _ to: QualityProfile) {
|
||||||
|
if let index = Defaults[.qualityProfiles].firstIndex(where: { $0.id == from.id }) {
|
||||||
|
Defaults[.qualityProfiles][index] = to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(_ qualityProfile: QualityProfile) {
|
||||||
|
if let index = Defaults[.qualityProfiles].firstIndex(where: { $0.id == qualityProfile.id }) {
|
||||||
|
Defaults[.qualityProfiles].remove(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyToAll(_ qualityProfile: QualityProfile) {
|
||||||
|
Defaults[.batteryCellularProfile] = qualityProfile.id
|
||||||
|
Defaults[.batteryNonCellularProfile] = qualityProfile.id
|
||||||
|
Defaults[.chargingCellularProfile] = qualityProfile.id
|
||||||
|
Defaults[.chargingNonCellularProfile] = qualityProfile.id
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
private func findCurrentConnection() -> Reachability.Connection? {
|
||||||
|
do {
|
||||||
|
let reachability: Reachability = try Reachability()
|
||||||
|
return reachability.connection
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var automaticProfile: QualityProfile? {
|
||||||
|
var id: QualityProfile.ID?
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||||
|
let unplugged = UIDevice.current.batteryState == .unplugged
|
||||||
|
let connection = findCurrentConnection()
|
||||||
|
|
||||||
|
if unplugged {
|
||||||
|
switch connection {
|
||||||
|
case .wifi:
|
||||||
|
id = Defaults[.batteryNonCellularProfile]
|
||||||
|
default:
|
||||||
|
id = Defaults[.batteryCellularProfile]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch connection {
|
||||||
|
case .wifi:
|
||||||
|
id = Defaults[.chargingNonCellularProfile]
|
||||||
|
default:
|
||||||
|
id = Defaults[.chargingCellularProfile]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
if Power.hasInternalBattery {
|
||||||
|
if Power.isConnectedToPower {
|
||||||
|
id = Defaults[.chargingNonCellularProfile]
|
||||||
|
} else {
|
||||||
|
id = Defaults[.batteryNonCellularProfile]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = Defaults[.chargingNonCellularProfile]
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
id = Defaults[.chargingNonCellularProfile]
|
||||||
|
#endif
|
||||||
|
|
||||||
|
guard let id = id else { return nil }
|
||||||
|
|
||||||
|
return find(id)
|
||||||
|
}
|
||||||
|
}
|
@ -6,18 +6,6 @@ import SwiftUI
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
#if os(tvOS)
|
|
||||||
static let defaultForPauseOnHidingPlayer = true
|
|
||||||
#else
|
|
||||||
static let defaultForPauseOnHidingPlayer = false
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
static let defaultForPlayerDetailsPageButtonLabelStyle = PlayerDetailsPageButtonLabelStyle.iconAndText
|
|
||||||
#else
|
|
||||||
static let defaultForPlayerDetailsPageButtonLabelStyle = UIDevice.current.userInterfaceIdiom == .phone ? PlayerDetailsPageButtonLabelStyle.iconOnly : .iconAndText
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static let instancesManifest = Key<String>("instancesManifest", default: "")
|
static let instancesManifest = Key<String>("instancesManifest", default: "")
|
||||||
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
|
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
|
||||||
|
|
||||||
@ -50,7 +38,12 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
|
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
|
||||||
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
|
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
|
||||||
static let quality = Key<ResolutionSetting>("quality", default: .best)
|
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: [QualityProfile.defaultProfile, QualityProfile.highQualityProfile])
|
||||||
|
static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: QualityProfile.defaultProfile.id)
|
||||||
|
static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: QualityProfile.defaultProfile.id)
|
||||||
|
static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: QualityProfile.defaultProfile.id)
|
||||||
|
static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: QualityProfile.defaultProfile.id)
|
||||||
|
|
||||||
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
|
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
|
||||||
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
||||||
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
||||||
@ -58,12 +51,25 @@ extension Defaults.Keys {
|
|||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
||||||
#endif
|
#endif
|
||||||
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: defaultForPauseOnHidingPlayer)
|
|
||||||
|
#if os(tvOS)
|
||||||
|
static let pauseOnHidingPlayerDefault = true
|
||||||
|
#else
|
||||||
|
static let pauseOnHidingPlayerDefault = false
|
||||||
|
#endif
|
||||||
|
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault)
|
||||||
|
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
||||||
#endif
|
#endif
|
||||||
static let closeLastItemOnPlaybackEnd = Key<Bool>("closeLastItemOnPlaybackEnd", default: false)
|
static let closeLastItemOnPlaybackEnd = Key<Bool>("closeLastItemOnPlaybackEnd", default: false)
|
||||||
static let closePlayerOnItemClose = Key<Bool>("closePlayerOnItemClose", default: false)
|
|
||||||
|
#if os(tvOS)
|
||||||
|
static let closePlayerOnItemCloseDefault = true
|
||||||
|
#else
|
||||||
|
static let closePlayerOnItemCloseDefault = false
|
||||||
|
#endif
|
||||||
|
static let closePlayerOnItemClose = Key<Bool>("closePlayerOnItemClose", default: closePlayerOnItemCloseDefault)
|
||||||
|
|
||||||
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
|
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
|
||||||
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
|
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
|
||||||
@ -100,7 +106,12 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
||||||
|
|
||||||
static let playerDetailsPageButtonLabelStyle = Key<PlayerDetailsPageButtonLabelStyle>("playerDetailsPageButtonLabelStyle", default: defaultForPlayerDetailsPageButtonLabelStyle)
|
#if os(macOS)
|
||||||
|
static let playerDetailsPageButtonLabelStyleDefault = PlayerDetailsPageButtonLabelStyle.iconAndText
|
||||||
|
#else
|
||||||
|
static let playerDetailsPageButtonLabelStyleDefault = UIDevice.current.userInterfaceIdiom == .phone ? PlayerDetailsPageButtonLabelStyle.iconOnly : .iconAndText
|
||||||
|
#endif
|
||||||
|
static let playerDetailsPageButtonLabelStyle = Key<PlayerDetailsPageButtonLabelStyle>("playerDetailsPageButtonLabelStyle", default: playerDetailsPageButtonLabelStyleDefault)
|
||||||
|
|
||||||
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
|
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
|
||||||
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "20")
|
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "20")
|
||||||
|
@ -6,43 +6,125 @@ struct ControlsOverlay: View {
|
|||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlayerControlsModel> private var model
|
@EnvironmentObject<PlayerControlsModel> private var model
|
||||||
|
|
||||||
|
@State private var contentSize: CGSize = .zero
|
||||||
|
|
||||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||||
|
@Default(.qualityProfiles) private var qualityProfiles
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
enum Field: Hashable {
|
||||||
|
case qualityProfile
|
||||||
|
case stream
|
||||||
|
case increaseRate
|
||||||
|
case decreaseRate
|
||||||
|
case captions
|
||||||
|
}
|
||||||
|
|
||||||
|
@FocusState private var focusedField: Field?
|
||||||
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 6) {
|
VStack {
|
||||||
HStack {
|
Section(header: controlsHeader("Rate & Captions")) {
|
||||||
backendButtons
|
HStack(spacing: rateButtonsSpacing) {
|
||||||
}
|
decreaseRateButton
|
||||||
qualityButton
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .decreaseRate)
|
||||||
|
#endif
|
||||||
|
rateButton
|
||||||
|
increaseRateButton
|
||||||
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .increaseRate)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
if player.activeBackend == .mpv {
|
|
||||||
captionsButton
|
captionsButton
|
||||||
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .captions)
|
||||||
|
#endif
|
||||||
|
.disabled(player.activeBackend != .mpv)
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
Section(header: controlsHeader("Quality Profile")) {
|
||||||
decreaseRateButton
|
qualityProfileButton
|
||||||
rateButton
|
#if os(tvOS)
|
||||||
increaseRateButton
|
.focused($focusedField, equals: .qualityProfile)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: controlsHeader("Stream & Player")) {
|
||||||
|
qualityButton
|
||||||
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .stream)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
HStack {
|
||||||
|
backendButtons
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if player.activeBackend == .mpv,
|
if player.activeBackend == .mpv,
|
||||||
showMPVPlaybackStats
|
showMPVPlaybackStats
|
||||||
{
|
{
|
||||||
mpvPlaybackStats
|
Section(header: controlsHeader("Statistics")) {
|
||||||
|
mpvPlaybackStats
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(width: 400)
|
||||||
|
#else
|
||||||
|
.frame(width: 240)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.overlay(
|
||||||
|
GeometryReader { geometry in
|
||||||
|
Color.clear.onAppear {
|
||||||
|
contentSize = geometry.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
#if os(tvOS)
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
.frame(maxHeight: overlayHeight)
|
||||||
|
#if os(tvOS)
|
||||||
|
.onAppear {
|
||||||
|
focusedField = .qualityProfile
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var overlayHeight: Double {
|
||||||
|
#if os(tvOS)
|
||||||
|
contentSize.height + 50.0
|
||||||
|
#else
|
||||||
|
contentSize.height
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func controlsHeader(_ text: String) -> some View {
|
||||||
|
Text(text)
|
||||||
|
.font(.system(.caption))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var backendButtons: some View {
|
private var backendButtons: some View {
|
||||||
ForEach(PlayerBackendType.allCases, id: \.self) { backend in
|
ForEach(PlayerBackendType.allCases, id: \.self) { backend in
|
||||||
backendButton(backend)
|
backendButton(backend)
|
||||||
|
.frame(height: 40)
|
||||||
|
#if os(iOS)
|
||||||
|
.frame(maxWidth: 115)
|
||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,11 +136,48 @@ struct ControlsOverlay: View {
|
|||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text(backend.label)
|
Text(backend.label)
|
||||||
.padding(6)
|
|
||||||
.foregroundColor(player.activeBackend == backend ? .accentColor : .secondary)
|
.foregroundColor(player.activeBackend == backend ? .accentColor : .secondary)
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
#if os(macOS)
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
#else
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var rateButton: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
ratePicker
|
||||||
|
.labelsHidden()
|
||||||
|
.frame(maxWidth: 100)
|
||||||
|
#elseif os(iOS)
|
||||||
|
Menu {
|
||||||
|
ratePicker
|
||||||
|
} label: {
|
||||||
|
Text(player.rateLabel(player.currentRate))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(width: 123)
|
||||||
|
}
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(width: 123, height: 40)
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
#else
|
||||||
|
Text(player.rateLabel(player.currentRate))
|
||||||
|
.frame(minWidth: 120)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var ratePicker: some View {
|
||||||
|
Picker("Rate", selection: $player.currentRate) {
|
||||||
|
ForEach(PlayerModel.availableRates, id: \.self) { rate in
|
||||||
|
Text(player.rateLabel(rate)).tag(rate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var increaseRateButton: some View {
|
private var increaseRateButton: some View {
|
||||||
@ -72,12 +191,12 @@ struct ControlsOverlay: View {
|
|||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.frame(height: 30)
|
.frame(width: 50, height: 40)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
#else
|
#elseif os(iOS)
|
||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
#endif
|
#endif
|
||||||
@ -96,18 +215,76 @@ struct ControlsOverlay: View {
|
|||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.frame(height: 30)
|
.frame(width: 50, height: 40)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
#else
|
#elseif os(iOS)
|
||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
#endif
|
#endif
|
||||||
.disabled(decreasedRate.isNil)
|
.disabled(decreasedRate.isNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var rateButtonsSpacing: Double {
|
||||||
|
#if os(tvOS)
|
||||||
|
10
|
||||||
|
#else
|
||||||
|
8
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var qualityProfileButton: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
qualityProfilePicker
|
||||||
|
.labelsHidden()
|
||||||
|
.frame(maxWidth: 300)
|
||||||
|
#elseif os(iOS)
|
||||||
|
Menu {
|
||||||
|
qualityProfilePicker
|
||||||
|
} label: {
|
||||||
|
Text(player.qualityProfileSelection?.description ?? "Auto")
|
||||||
|
.frame(maxWidth: 240)
|
||||||
|
}
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(maxWidth: 240)
|
||||||
|
.frame(height: 40)
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
#else
|
||||||
|
Button {} label: {
|
||||||
|
Text(player.qualityProfileSelection?.description ?? "Auto")
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(maxWidth: 320)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
ForEach(qualityProfiles) { qualityProfile in
|
||||||
|
Button("Default") { player.qualityProfileSelection = nil }
|
||||||
|
Button {
|
||||||
|
player.qualityProfileSelection = qualityProfile
|
||||||
|
} label: {
|
||||||
|
Text(qualityProfile.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var qualityProfilePicker: some View {
|
||||||
|
Picker("Quality Profile", selection: $player.qualityProfileSelection) {
|
||||||
|
Text("Automatic").tag(QualityProfile?.none)
|
||||||
|
ForEach(qualityProfiles) { qualityProfile in
|
||||||
|
Text(qualityProfile.description).tag(qualityProfile as QualityProfile?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder private var qualityButton: some View {
|
@ViewBuilder private var qualityButton: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
StreamControl()
|
StreamControl()
|
||||||
@ -116,23 +293,20 @@ struct ControlsOverlay: View {
|
|||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
Menu {
|
||||||
StreamControl()
|
StreamControl()
|
||||||
.frame(width: 45, height: 30)
|
|
||||||
#if os(iOS)
|
|
||||||
.modifier(ControlBackgroundModifier())
|
|
||||||
#endif
|
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
|
||||||
} label: {
|
} label: {
|
||||||
Text(player.streamSelection?.shortQuality ?? "loading")
|
Text(player.streamSelection?.shortQuality ?? "loading")
|
||||||
.frame(width: 140, height: 30)
|
.frame(width: 140, height: 40)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
|
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.frame(width: 140, height: 30)
|
.frame(width: 240, height: 40)
|
||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
#else
|
||||||
|
StreamControl()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,8 +318,6 @@ struct ControlsOverlay: View {
|
|||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
Menu {
|
||||||
captionsPicker
|
captionsPicker
|
||||||
.frame(width: 140, height: 30)
|
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "text.bubble")
|
Image(systemName: "text.bubble")
|
||||||
@ -154,14 +326,32 @@ struct ControlsOverlay: View {
|
|||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 140, height: 30)
|
.frame(width: 240)
|
||||||
|
.frame(height: 40)
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.frame(width: 140, height: 30)
|
.frame(width: 240)
|
||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
#else
|
||||||
|
Button {} label: {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "text.bubble")
|
||||||
|
if let captions = captionsBinding.wrappedValue {
|
||||||
|
Text(captions.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 320)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
ForEach(player.currentVideo?.captions ?? []) { caption in
|
||||||
|
Button(caption.description) { captionsBinding.wrappedValue = caption }
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,58 +380,29 @@ struct ControlsOverlay: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var rateButton: some View {
|
|
||||||
#if os(macOS)
|
|
||||||
ratePicker
|
|
||||||
.labelsHidden()
|
|
||||||
.frame(maxWidth: 100)
|
|
||||||
#elseif os(iOS)
|
|
||||||
Menu {
|
|
||||||
ratePicker
|
|
||||||
.frame(width: 100, height: 30)
|
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
|
||||||
} label: {
|
|
||||||
Text(player.rateLabel(player.currentRate))
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.frame(width: 80)
|
|
||||||
}
|
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.frame(width: 100, height: 30)
|
|
||||||
.modifier(ControlBackgroundModifier())
|
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
var ratePicker: some View {
|
|
||||||
Picker("Rate", selection: rateBinding) {
|
|
||||||
ForEach(PlayerModel.availableRates, id: \.self) { rate in
|
|
||||||
Text(player.rateLabel(rate)).tag(rate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var rateBinding: Binding<Float> {
|
|
||||||
.init(get: { player.currentRate }, set: { rate in player.currentRate = rate })
|
|
||||||
}
|
|
||||||
|
|
||||||
var mpvPlaybackStats: some View {
|
var mpvPlaybackStats: some View {
|
||||||
Group {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
mpvPlaybackStatRow("Hardware decoder", player.mpvBackend.hwDecoder)
|
||||||
Text("hw decoder: \(player.mpvBackend.hwDecoder)")
|
mpvPlaybackStatRow("Dropped frames", String(player.mpvBackend.frameDropCount))
|
||||||
Text("dropped: \(player.mpvBackend.frameDropCount)")
|
mpvPlaybackStatRow("Stream FPS", String(format: "%.2ffps", player.mpvBackend.outputFps))
|
||||||
Text("video: \(String(format: "%.2ffps", player.mpvBackend.outputFps))")
|
mpvPlaybackStatRow("Cached time", String(format: "%.2fs", player.mpvBackend.cacheDuration))
|
||||||
Text("buffering: \(String(format: "%.0f%%", networkState.bufferingState))")
|
|
||||||
Text("cache: \(String(format: "%.2fs", player.mpvBackend.cacheDuration))")
|
|
||||||
}
|
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
.padding(.top, 2)
|
||||||
.font(.system(size: 9))
|
#if os(tvOS)
|
||||||
|
.font(.system(size: 20))
|
||||||
|
#else
|
||||||
|
.font(.system(size: 11))
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mpvPlaybackStatRow(_ label: String, _ value: String) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ControlsOverlay_Previews: PreviewProvider {
|
struct ControlsOverlay_Previews: PreviewProvider {
|
||||||
|
@ -18,6 +18,8 @@ struct PlayerControls: View {
|
|||||||
case play
|
case play
|
||||||
case backward
|
case backward
|
||||||
case forward
|
case forward
|
||||||
|
case settings
|
||||||
|
case close
|
||||||
}
|
}
|
||||||
|
|
||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
@ -41,23 +43,25 @@ struct PlayerControls: View {
|
|||||||
|
|
||||||
if model.presentingControls && !model.presentingOverlays {
|
if model.presentingControls && !model.presentingOverlays {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
buttonsBar
|
#if !os(tvOS)
|
||||||
|
buttonsBar
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
if !player.currentVideo.isNil, fullScreenLayout {
|
if !player.currentVideo.isNil, fullScreenLayout {
|
||||||
Button {
|
Button {
|
||||||
withAnimation(Self.animation) {
|
withAnimation(Self.animation) {
|
||||||
model.presentingDetailsOverlay = true
|
model.presentingDetailsOverlay = true
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
ControlsBar(fullScreen: $model.presentingDetailsOverlay, presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
.frame(maxWidth: 300, alignment: .leading)
|
||||||
}
|
}
|
||||||
} label: {
|
.buttonStyle(.plain)
|
||||||
ControlsBar(fullScreen: $model.presentingDetailsOverlay, presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
|
||||||
.frame(maxWidth: 300, alignment: .leading)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
Spacer()
|
||||||
}
|
}
|
||||||
Spacer()
|
#endif
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -86,28 +90,15 @@ struct PlayerControls: View {
|
|||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.onChange(of: model.presentingControls) { _ in
|
.onChange(of: model.presentingControls) { newValue in
|
||||||
if model.presentingControls {
|
if newValue { focusedField = .play }
|
||||||
focusedField = .play
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: focusedField) { _ in
|
|
||||||
model.resetTimer()
|
|
||||||
}
|
}
|
||||||
|
.onChange(of: focusedField) { _ in model.resetTimer() }
|
||||||
#else
|
#else
|
||||||
.background(PlayerGestures())
|
.background(PlayerGestures())
|
||||||
.background(controlsBackground)
|
.background(controlsBackground)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if model.presentingControlsOverlay {
|
|
||||||
ControlsOverlay()
|
|
||||||
.frame(height: overlayHeight)
|
|
||||||
.padding()
|
|
||||||
.modifier(ControlBackgroundModifier())
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
|
||||||
.transition(.opacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
if model.presentingDetailsOverlay {
|
if model.presentingDetailsOverlay {
|
||||||
VideoDetailsOverlay()
|
VideoDetailsOverlay()
|
||||||
.frame(maxWidth: detailsWidth, maxHeight: detailsHeight)
|
.frame(maxWidth: detailsWidth, maxHeight: detailsHeight)
|
||||||
@ -117,7 +108,7 @@ struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !model.presentingControls,
|
if !model.presentingControls,
|
||||||
!model.presentingControls,
|
!model.presentingOverlays,
|
||||||
let segment = player.lastSkipped
|
let segment = player.lastSkipped
|
||||||
{
|
{
|
||||||
Button {
|
Button {
|
||||||
@ -140,18 +131,22 @@ struct PlayerControls: View {
|
|||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: player.controls.presentingOverlays) { newValue in
|
.onChange(of: model.presentingOverlays) { newValue in
|
||||||
if newValue {
|
if newValue {
|
||||||
player.backend.stopControlsUpdates()
|
player.backend.stopControlsUpdates()
|
||||||
} else {
|
} else {
|
||||||
|
#if os(tvOS)
|
||||||
|
focusedField = .play
|
||||||
|
#endif
|
||||||
player.backend.startControlsUpdates()
|
player.backend.startControlsUpdates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
#if os(tvOS)
|
||||||
|
.onReceive(model.reporter) { _ in
|
||||||
var overlayHeight: Double {
|
model.show()
|
||||||
guard let player = player, player.playerSize.height.isFinite else { return 0 }
|
model.resetTimer()
|
||||||
return [0, [player.playerSize.height - 40, 140].min()!].max()!
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
var detailsWidth: Double {
|
var detailsWidth: Double {
|
||||||
@ -226,24 +221,17 @@ struct PlayerControls: View {
|
|||||||
|
|
||||||
var buttonsBar: some View {
|
var buttonsBar: some View {
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
#if !os(tvOS)
|
fullscreenButton
|
||||||
fullscreenButton
|
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
pipButton
|
pipButton
|
||||||
lockOrientationButton
|
lockOrientationButton
|
||||||
#endif
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) {
|
|
||||||
withAnimation(Self.animation) {
|
|
||||||
model.presentingControlsOverlay.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeVideoButton
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
settingsButton
|
||||||
|
closeVideoButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,10 +247,24 @@ struct PlayerControls: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var settingsButton: some View {
|
||||||
|
button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) {
|
||||||
|
withAnimation(Self.animation) {
|
||||||
|
model.presentingControlsOverlay.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .settings)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private var closeVideoButton: some View {
|
private var closeVideoButton: some View {
|
||||||
button("Close", systemImage: "xmark") {
|
button("Close", systemImage: "xmark") {
|
||||||
player.closeCurrentItem()
|
player.closeCurrentItem()
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.focused($focusedField, equals: .close)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var musicModeButton: some View {
|
private var musicModeButton: some View {
|
||||||
@ -308,6 +310,9 @@ struct PlayerControls: View {
|
|||||||
advanceToNextItemButton
|
advanceToNextItemButton
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
musicModeButton
|
musicModeButton
|
||||||
|
#else
|
||||||
|
settingsButton
|
||||||
|
closeVideoButton
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
@ -388,7 +393,12 @@ struct PlayerControls: View {
|
|||||||
active: Bool = false,
|
active: Bool = false,
|
||||||
action: @escaping () -> Void = {}
|
action: @escaping () -> Void = {}
|
||||||
) -> some View {
|
) -> some View {
|
||||||
Button {
|
#if os(tvOS)
|
||||||
|
let useBackground = false
|
||||||
|
#else
|
||||||
|
let useBackground = background
|
||||||
|
#endif
|
||||||
|
return Button {
|
||||||
action()
|
action()
|
||||||
model.resetTimer()
|
model.resetTimer()
|
||||||
} label: {
|
} label: {
|
||||||
@ -408,7 +418,7 @@ struct PlayerControls: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||||
.frame(width: width ?? size, height: height ?? size)
|
.frame(width: width ?? size, height: height ?? size)
|
||||||
.modifier(ControlBackgroundModifier(enabled: background))
|
.modifier(ControlBackgroundModifier(enabled: useBackground))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ struct StreamControl: View {
|
|||||||
}
|
}
|
||||||
.disabled(player.isLoadingAvailableStreams)
|
.disabled(player.isLoadingAvailableStreams)
|
||||||
|
|
||||||
#else
|
#elseif os(iOS)
|
||||||
Picker("", selection: $player.streamSelection) {
|
Picker("", selection: $player.streamSelection) {
|
||||||
ForEach(InstancesModel.all) { instance in
|
ForEach(InstancesModel.all) { instance in
|
||||||
let instanceStreams = availableStreamsForInstance(instance)
|
let instanceStreams = availableStreamsForInstance(instance)
|
||||||
@ -46,16 +46,26 @@ struct StreamControl: View {
|
|||||||
.frame(minWidth: 110)
|
.frame(minWidth: 110)
|
||||||
.fixedSize(horizontal: true, vertical: true)
|
.fixedSize(horizontal: true, vertical: true)
|
||||||
.disabled(player.isLoadingAvailableStreams)
|
.disabled(player.isLoadingAvailableStreams)
|
||||||
|
#else
|
||||||
|
Button {} label: {
|
||||||
|
Text(player.streamSelection?.shortQuality ?? "loading")
|
||||||
|
.frame(maxWidth: 320)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
ForEach(player.availableStreamsSorted) { stream in
|
||||||
|
Button(stream.description) { player.streamSelection = stream }
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Close", role: .cancel) {}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
.onChange(of: player.streamSelection) { selection in
|
.onChange(of: player.streamSelection) { selection in
|
||||||
guard !selection.isNil else {
|
guard let selection = selection else { return }
|
||||||
return
|
player.upgradeToStream(selection)
|
||||||
}
|
player.controls.hideOverlays()
|
||||||
|
|
||||||
player.upgradeToStream(selection!)
|
|
||||||
}
|
}
|
||||||
.frame(alignment: .trailing)
|
.frame(alignment: .trailing)
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,42 @@ struct VideoPlayerView: View {
|
|||||||
@EnvironmentObject<ThumbnailsModel> private var thumbnails
|
@EnvironmentObject<ThumbnailsModel> private var thumbnails
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ZStack(alignment: overlayAlignment) {
|
||||||
|
videoPlayer
|
||||||
|
#if os(iOS)
|
||||||
|
.gesture(playerControls.presentingControlsOverlay ? videoPlayerCloseControlsOverlayGesture : nil)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if playerControls.presentingControlsOverlay {
|
||||||
|
HStack {
|
||||||
|
ControlsOverlay()
|
||||||
|
#if os(tvOS)
|
||||||
|
.onExitCommand {
|
||||||
|
withAnimation(PlayerControls.animation) {
|
||||||
|
playerControls.hideOverlays()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onPlayPauseCommand {
|
||||||
|
player.togglePlay()
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
.frame(maxWidth: overlayWidth)
|
||||||
|
#endif
|
||||||
|
.padding()
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
#else
|
||||||
|
.frame(maxWidth: player.playerSize.width)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var videoPlayer: some View {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// TODO: remove
|
// TODO: remove
|
||||||
if #available(iOS 15.0, macOS 12.0, *) {
|
if #available(iOS 15.0, macOS 12.0, *) {
|
||||||
@ -66,8 +102,13 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
return HSplitView {
|
return GeometryReader { geometry in
|
||||||
content
|
HSplitView {
|
||||||
|
content
|
||||||
|
.onAppear {
|
||||||
|
playerSize = geometry.size
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.alert(isPresented: $navigation.presentingAlertInVideoPlayer) { navigation.alert }
|
.alert(isPresented: $navigation.presentingAlertInVideoPlayer) { navigation.alert }
|
||||||
.onOpenURL {
|
.onOpenURL {
|
||||||
@ -124,7 +165,7 @@ struct VideoPlayerView: View {
|
|||||||
Orientation.lockOrientation(.allButUpsideDown)
|
Orientation.lockOrientation(.allButUpsideDown)
|
||||||
}
|
}
|
||||||
stopOrientationUpdates()
|
stopOrientationUpdates()
|
||||||
player.controls.hideOverlays()
|
playerControls.hideOverlays()
|
||||||
|
|
||||||
player.lockedOrientation = nil
|
player.lockedOrientation = nil
|
||||||
#endif
|
#endif
|
||||||
@ -139,7 +180,28 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var overlayWidth: Double {
|
||||||
|
guard playerSize.width.isFinite else { return 200 }
|
||||||
|
return [playerSize.width - 50, 250].min()!
|
||||||
|
}
|
||||||
|
|
||||||
|
var overlayAlignment: Alignment {
|
||||||
|
#if os(tvOS)
|
||||||
|
return .bottomTrailing
|
||||||
|
#else
|
||||||
|
return .top
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
var videoPlayerCloseControlsOverlayGesture: some Gesture {
|
||||||
|
TapGesture().onEnded {
|
||||||
|
withAnimation(PlayerControls.animation) {
|
||||||
|
playerControls.hideOverlays()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var playerOffset: Double {
|
var playerOffset: Double {
|
||||||
dragGestureState ? dragGestureOffset.height : viewDragOffset
|
dragGestureState ? dragGestureOffset.height : viewDragOffset
|
||||||
}
|
}
|
||||||
@ -153,9 +215,14 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playerEdgesIgnoringSafeArea: Edge.Set {
|
var playerEdgesIgnoringSafeArea: Edge.Set {
|
||||||
|
if let orientation = player.lockedOrientation, orientation.contains(.portrait) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
if fullScreenLayout, UIDevice.current.orientation.isLandscape {
|
if fullScreenLayout, UIDevice.current.orientation.isLandscape {
|
||||||
return [.vertical]
|
return [.vertical]
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -170,33 +237,6 @@ struct VideoPlayerView: View {
|
|||||||
tvControls
|
tvControls
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onMoveCommand { direction in
|
|
||||||
if direction == .up || direction == .down {
|
|
||||||
playerControls.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
playerControls.resetTimer()
|
|
||||||
|
|
||||||
guard !playerControls.presentingControls else { return }
|
|
||||||
|
|
||||||
if direction == .left {
|
|
||||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
|
||||||
}
|
|
||||||
if direction == .right {
|
|
||||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onPlayPauseCommand {
|
|
||||||
player.togglePlay()
|
|
||||||
}
|
|
||||||
|
|
||||||
.onExitCommand {
|
|
||||||
if playerControls.presentingControls {
|
|
||||||
playerControls.hide()
|
|
||||||
} else {
|
|
||||||
player.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
PlayerBackendView()
|
PlayerBackendView()
|
||||||
@ -259,6 +299,41 @@ struct VideoPlayerView: View {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 650)
|
.frame(minWidth: 650)
|
||||||
#endif
|
#endif
|
||||||
|
#if os(tvOS)
|
||||||
|
.onMoveCommand { direction in
|
||||||
|
if direction == .up {
|
||||||
|
playerControls.show()
|
||||||
|
} else if direction == .down, !playerControls.presentingControlsOverlay {
|
||||||
|
withAnimation(PlayerControls.animation) {
|
||||||
|
playerControls.presentingControlsOverlay = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerControls.resetTimer()
|
||||||
|
|
||||||
|
guard !playerControls.presentingControls else { return }
|
||||||
|
|
||||||
|
if direction == .left {
|
||||||
|
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||||
|
}
|
||||||
|
if direction == .right {
|
||||||
|
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onPlayPauseCommand {
|
||||||
|
player.togglePlay()
|
||||||
|
}
|
||||||
|
.onExitCommand {
|
||||||
|
if playerControls.presentingOverlays {
|
||||||
|
playerControls.hideOverlays()
|
||||||
|
}
|
||||||
|
if playerControls.presentingControls {
|
||||||
|
playerControls.hide()
|
||||||
|
} else {
|
||||||
|
player.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if !fullScreenLayout {
|
if !fullScreenLayout {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if sidebarQueue {
|
if sidebarQueue {
|
||||||
@ -277,7 +352,7 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: fullScreenLayout) { newValue in
|
.onChange(of: fullScreenLayout) { newValue in
|
||||||
if !newValue { playerControls.presentingDetailsOverlay = false }
|
if !newValue { playerControls.hideOverlays() }
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.statusBar(hidden: fullScreenLayout)
|
.statusBar(hidden: fullScreenLayout)
|
||||||
@ -346,8 +421,8 @@ struct VideoPlayerView: View {
|
|||||||
guard player.presentingPlayer,
|
guard player.presentingPlayer,
|
||||||
!playerControls.presentingControlsOverlay else { return }
|
!playerControls.presentingControlsOverlay else { return }
|
||||||
|
|
||||||
if player.controls.presentingControls {
|
if playerControls.presentingControls {
|
||||||
player.controls.presentingControls = false
|
playerControls.presentingControls = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let drag = value.translation.height
|
let drag = value.translation.height
|
||||||
@ -401,7 +476,7 @@ struct VideoPlayerView: View {
|
|||||||
!player.playingInPictureInPicture
|
!player.playingInPictureInPicture
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
player.controls.presentingControls = false
|
playerControls.presentingControls = false
|
||||||
player.enterFullScreen(showControls: false)
|
player.enterFullScreen(showControls: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,7 +510,7 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if orientation.isLandscape {
|
if orientation.isLandscape {
|
||||||
player.controls.presentingControls = false
|
playerControls.presentingControls = false
|
||||||
player.enterFullScreen(showControls: false)
|
player.enterFullScreen(showControls: false)
|
||||||
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
|
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
|
||||||
} else {
|
} else {
|
||||||
@ -455,10 +530,6 @@ struct VideoPlayerView: View {
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
var tvControls: some View {
|
var tvControls: some View {
|
||||||
TVControls(model: playerControls, player: player, thumbnails: thumbnails)
|
TVControls(model: playerControls, player: player, thumbnails: thumbnails)
|
||||||
.onReceive(playerControls.reporter) { _ in
|
|
||||||
playerControls.show()
|
|
||||||
playerControls.resetTimer()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import SwiftUI
|
|||||||
struct PlayerSettings: View {
|
struct PlayerSettings: View {
|
||||||
@Default(.instances) private var instances
|
@Default(.instances) private var instances
|
||||||
@Default(.playerInstanceID) private var playerInstanceID
|
@Default(.playerInstanceID) private var playerInstanceID
|
||||||
@Default(.quality) private var quality
|
|
||||||
|
|
||||||
@Default(.playerSidebar) private var playerSidebar
|
@Default(.playerSidebar) private var playerSidebar
|
||||||
@Default(.showHistoryInPlayer) private var showHistory
|
@Default(.showHistoryInPlayer) private var showHistory
|
||||||
@ -59,7 +58,6 @@ struct PlayerSettings: View {
|
|||||||
Group {
|
Group {
|
||||||
Section(header: SettingsHeader(text: "Playback")) {
|
Section(header: SettingsHeader(text: "Playback")) {
|
||||||
sourcePicker
|
sourcePicker
|
||||||
qualityPicker
|
|
||||||
pauseOnHidingPlayerToggle
|
pauseOnHidingPlayerToggle
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
pauseOnEnteringBackgroundToogle
|
pauseOnEnteringBackgroundToogle
|
||||||
@ -107,7 +105,7 @@ struct PlayerSettings: View {
|
|||||||
|
|
||||||
private var sourcePicker: some View {
|
private var sourcePicker: some View {
|
||||||
Picker("Source", selection: $playerInstanceID) {
|
Picker("Source", selection: $playerInstanceID) {
|
||||||
Text("Best available stream").tag(String?.none)
|
Text("Account Instance").tag(String?.none)
|
||||||
|
|
||||||
ForEach(instances) { instance in
|
ForEach(instances) { instance in
|
||||||
Text(instance.description).tag(Optional(instance.id))
|
Text(instance.description).tag(Optional(instance.id))
|
||||||
@ -135,15 +133,6 @@ struct PlayerSettings: View {
|
|||||||
.modifier(SettingsPickerModifier())
|
.modifier(SettingsPickerModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
private var qualityPicker: some View {
|
|
||||||
Picker("Quality", selection: $quality) {
|
|
||||||
ForEach(ResolutionSetting.allCases, id: \.self) { resolution in
|
|
||||||
Text(resolution.description).tag(resolution)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.modifier(SettingsPickerModifier())
|
|
||||||
}
|
|
||||||
|
|
||||||
private var sidebarPicker: some View {
|
private var sidebarPicker: some View {
|
||||||
Picker("Sidebar", selection: $playerSidebar) {
|
Picker("Sidebar", selection: $playerSidebar) {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
312
Shared/Settings/QualityProfileForm.swift
Normal file
312
Shared/Settings/QualityProfileForm.swift
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct QualityProfileForm: View {
|
||||||
|
var qualityProfileID: QualityProfile.ID!
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
|
|
||||||
|
@State private var valid = false
|
||||||
|
|
||||||
|
@State private var name = ""
|
||||||
|
@State private var backend = PlayerBackendType.mpv
|
||||||
|
@State private var resolution = ResolutionSetting.best
|
||||||
|
@State private var formats = [QualityProfile.Format]()
|
||||||
|
|
||||||
|
var qualityProfile: QualityProfile! {
|
||||||
|
if let id = qualityProfileID {
|
||||||
|
return QualityProfilesModel.shared.find(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack {
|
||||||
|
Group {
|
||||||
|
header
|
||||||
|
#if os(iOS)
|
||||||
|
NavigationView {
|
||||||
|
EmptyView()
|
||||||
|
|
||||||
|
form
|
||||||
|
.navigationBarHidden(true)
|
||||||
|
.navigationBarTitle(Text("Back"))
|
||||||
|
.edgesIgnoringSafeArea([.top, .bottom])
|
||||||
|
}
|
||||||
|
.navigationViewStyle(.stack)
|
||||||
|
#else
|
||||||
|
form
|
||||||
|
#endif
|
||||||
|
footer
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 1000)
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.padding(20)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.onAppear(perform: initializeForm)
|
||||||
|
.onChange(of: backend, perform: backendChanged)
|
||||||
|
.onChange(of: formats) { _ in validate() }
|
||||||
|
#if os(iOS)
|
||||||
|
.padding(.vertical)
|
||||||
|
#elseif os(tvOS)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||||
|
.background(Color.background(scheme: colorScheme))
|
||||||
|
#else
|
||||||
|
.frame(width: 400, height: 400)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var header: some View {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Text(editing ? "Edit Quality Profile" : "Add Quality Profile")
|
||||||
|
.font(.title2.bold())
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("Cancel") {
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
var form: some View {
|
||||||
|
#if !os(tvOS)
|
||||||
|
Form {
|
||||||
|
formFields
|
||||||
|
#if os(macOS)
|
||||||
|
.padding(.horizontal)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
formFields
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var formFields: some View {
|
||||||
|
Group {
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
nameHeader
|
||||||
|
TextField("Name", text: $name, onCommit: validate)
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
Section(header: Text("Resolution")) {
|
||||||
|
qualityButton
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
backendPicker
|
||||||
|
qualityPicker
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
Section(header: Text("Preferred Formats"), footer: formatsFooter) {
|
||||||
|
formatsPicker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var nameHeader: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
Text("Name")
|
||||||
|
#else
|
||||||
|
EmptyView()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatsFooter: some View {
|
||||||
|
Text("Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply).")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var qualityPicker: some View {
|
||||||
|
Picker("Resolution", selection: $resolution) {
|
||||||
|
ForEach(availableResolutions, id: \.self) { resolution in
|
||||||
|
Text(resolution.description).tag(resolution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modifier(SettingsPickerModifier())
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
var qualityButton: some View {
|
||||||
|
Button(resolution.description) {
|
||||||
|
resolution = resolution.next()
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
ForEach(availableResolutions, id: \.self) { resolution in
|
||||||
|
Button(resolution.description) {
|
||||||
|
self.resolution = resolution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var availableResolutions: [ResolutionSetting] {
|
||||||
|
ResolutionSetting.allCases.filter { !isResolutionDisabled($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var backendPicker: some View {
|
||||||
|
Picker("Backend", selection: $backend) {
|
||||||
|
ForEach(PlayerBackendType.allCases, id: \.self) { backend in
|
||||||
|
Text(backend.label).tag(backend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modifier(SettingsPickerModifier())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var formatsPicker: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
let list = ForEach(QualityProfile.Format.allCases, id: \.self) { format in
|
||||||
|
MultiselectRow(
|
||||||
|
title: format.description,
|
||||||
|
selected: isFormatSelected(format),
|
||||||
|
disabled: isFormatDisabled(format)
|
||||||
|
) { value in
|
||||||
|
toggleFormat(format, value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Group {
|
||||||
|
if #available(macOS 12.0, *) {
|
||||||
|
list
|
||||||
|
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||||
|
} else {
|
||||||
|
list
|
||||||
|
.listStyle(.inset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
#else
|
||||||
|
ForEach(QualityProfile.Format.allCases, id: \.self) { format in
|
||||||
|
MultiselectRow(
|
||||||
|
title: format.description,
|
||||||
|
selected: isFormatSelected(format),
|
||||||
|
disabled: isFormatDisabled(format)
|
||||||
|
) { value in
|
||||||
|
toggleFormat(format, value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFormatSelected(_ format: QualityProfile.Format) -> Bool {
|
||||||
|
(editing && formats.isEmpty ? qualityProfile.formats : formats).contains(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleFormat(_ format: QualityProfile.Format, value: Bool) {
|
||||||
|
if let index = formats.firstIndex(where: { $0 == format }), !value {
|
||||||
|
formats.remove(at: index)
|
||||||
|
} else if value {
|
||||||
|
formats.append(format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var footer: some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button("Save", action: submitForm)
|
||||||
|
.disabled(!valid)
|
||||||
|
#if !os(tvOS)
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.frame(minHeight: 35)
|
||||||
|
#if os(tvOS)
|
||||||
|
.padding(.top, 30)
|
||||||
|
#endif
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
var editing: Bool {
|
||||||
|
!qualityProfile.isNil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFormatDisabled(_ format: QualityProfile.Format) -> Bool {
|
||||||
|
guard backend == .appleAVPlayer else { return false }
|
||||||
|
|
||||||
|
let avPlayerFormats = [QualityProfile.Format.hls, .stream]
|
||||||
|
|
||||||
|
return !avPlayerFormats.contains(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
|
||||||
|
guard backend == .appleAVPlayer else { return false }
|
||||||
|
|
||||||
|
return resolution != .best && resolution.value.height > 720
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeForm() {
|
||||||
|
guard editing else {
|
||||||
|
validate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
self.name = qualityProfile.name ?? ""
|
||||||
|
self.backend = qualityProfile.backend
|
||||||
|
self.resolution = qualityProfile.resolution
|
||||||
|
self.formats = .init(qualityProfile.formats)
|
||||||
|
}
|
||||||
|
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func backendChanged(_: PlayerBackendType) {
|
||||||
|
formats.filter { isFormatDisabled($0) }.forEach { format in
|
||||||
|
if let index = formats.firstIndex(where: { $0 == format }) {
|
||||||
|
formats.remove(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let newResolution = availableResolutions.first {
|
||||||
|
resolution = newResolution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate() {
|
||||||
|
valid = !formats.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitForm() {
|
||||||
|
guard valid else { return }
|
||||||
|
|
||||||
|
formats = formats.unique()
|
||||||
|
|
||||||
|
let formProfile = QualityProfile(
|
||||||
|
id: qualityProfile?.id ?? UUID().uuidString,
|
||||||
|
name: name,
|
||||||
|
backend: backend,
|
||||||
|
resolution: resolution,
|
||||||
|
formats: Array(formats)
|
||||||
|
)
|
||||||
|
|
||||||
|
if editing {
|
||||||
|
QualityProfilesModel.shared.update(qualityProfile, formProfile)
|
||||||
|
} else {
|
||||||
|
QualityProfilesModel.shared.add(formProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QualityProfileForm_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
QualityProfileForm(qualityProfileID: QualityProfile.defaultProfile.id)
|
||||||
|
}
|
||||||
|
}
|
184
Shared/Settings/QualitySettings.swift
Normal file
184
Shared/Settings/QualitySettings.swift
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct QualitySettings: View {
|
||||||
|
@State private var presentingProfileForm = false
|
||||||
|
@State private var editedProfile: QualityProfile?
|
||||||
|
|
||||||
|
@Default(.qualityProfiles) private var qualityProfiles
|
||||||
|
|
||||||
|
@Default(.batteryCellularProfile) private var batteryCellularProfile
|
||||||
|
@Default(.batteryNonCellularProfile) private var batteryNonCellularProfile
|
||||||
|
@Default(.chargingCellularProfile) private var chargingCellularProfile
|
||||||
|
@Default(.chargingNonCellularProfile) private var chargingNonCellularProfile
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
#if os(macOS)
|
||||||
|
sections
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
#else
|
||||||
|
List {
|
||||||
|
sections
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $presentingProfileForm) {
|
||||||
|
QualityProfileForm(qualityProfileID: editedProfile?.id)
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(maxWidth: 1000)
|
||||||
|
#elseif os(iOS)
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#endif
|
||||||
|
.navigationTitle("Quality")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sections: some View {
|
||||||
|
Group {
|
||||||
|
Group {
|
||||||
|
#if os(tvOS)
|
||||||
|
Section(header: Text("Default Profile")) {
|
||||||
|
Text("\(QualityProfilesModel.shared.tvOSProfile?.description ?? "None")")
|
||||||
|
}
|
||||||
|
#elseif os(iOS)
|
||||||
|
if UIDevice.current.hasCellularCapabilites {
|
||||||
|
Section(header: Text("Battery")) {
|
||||||
|
Picker("Wi-Fi", selection: $batteryNonCellularProfile) { profilePickerOptions }
|
||||||
|
Picker("Cellular", selection: $batteryCellularProfile) { profilePickerOptions }
|
||||||
|
}
|
||||||
|
Section(header: Text("Charging")) {
|
||||||
|
Picker("Wi-Fi", selection: $chargingNonCellularProfile) { profilePickerOptions }
|
||||||
|
Picker("Cellular", selection: $chargingCellularProfile) { profilePickerOptions }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nonCellularBatteryDevicesProfilesPickers
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if Power.hasInternalBattery {
|
||||||
|
nonCellularBatteryDevicesProfilesPickers
|
||||||
|
} else {
|
||||||
|
Picker("Default", selection: $chargingNonCellularProfile) { profilePickerOptions }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.disabled(qualityProfiles.isEmpty)
|
||||||
|
Section(header: SettingsHeader(text: "Profiles"), footer: profilesFooter) {
|
||||||
|
profilesList
|
||||||
|
|
||||||
|
Button {
|
||||||
|
editedProfile = nil
|
||||||
|
presentingProfileForm = true
|
||||||
|
} label: {
|
||||||
|
Label("Add profile...", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var nonCellularBatteryDevicesProfilesPickers: some View {
|
||||||
|
Picker("Battery", selection: $batteryNonCellularProfile) { profilePickerOptions }
|
||||||
|
Picker("Charging", selection: $chargingNonCellularProfile) { profilePickerOptions }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func profileControl(_ qualityProfile: QualityProfile) -> some View {
|
||||||
|
#if os(tvOS)
|
||||||
|
Button {
|
||||||
|
QualityProfilesModel.shared.applyToAll(qualityProfile)
|
||||||
|
} label: {
|
||||||
|
Text(qualityProfile.description)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
Text(qualityProfile.description)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var profilePickerOptions: some View {
|
||||||
|
ForEach(qualityProfiles) { qualityProfile in
|
||||||
|
Text(qualityProfile.description).tag(qualityProfile.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var profilesFooter: some View {
|
||||||
|
#if os(tvOS)
|
||||||
|
Text("You can switch between profiles in playback settings controls.")
|
||||||
|
#else
|
||||||
|
Text("You can use automatic profile selection based on current device status or switch it in video playback settings controls.")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var profilesList: some View {
|
||||||
|
let list = ForEach(qualityProfiles) { qualityProfile in
|
||||||
|
profileControl(qualityProfile)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
QualityProfilesModel.shared.applyToAll(qualityProfile)
|
||||||
|
} label: {
|
||||||
|
#if os(tvOS)
|
||||||
|
Text("Make default")
|
||||||
|
#elseif os(iOS)
|
||||||
|
Label("Apply to all", systemImage: "wand.and.stars")
|
||||||
|
#else
|
||||||
|
if Power.hasInternalBattery {
|
||||||
|
Text("Apply to all")
|
||||||
|
} else {
|
||||||
|
Text("Make default")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
editedProfile = qualityProfile
|
||||||
|
presentingProfileForm = true
|
||||||
|
} label: {
|
||||||
|
Label("Edit...", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
QualityProfilesModel.shared.remove(qualityProfile)
|
||||||
|
} label: {
|
||||||
|
Label("Remove", systemImage: "trash")
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if #available(macOS 12.0, *) {
|
||||||
|
#if os(macOS)
|
||||||
|
List {
|
||||||
|
list
|
||||||
|
}
|
||||||
|
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||||
|
#else
|
||||||
|
list
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
#if os(macOS)
|
||||||
|
List {
|
||||||
|
list
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
list
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QualitySettings_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
QualitySettings()
|
||||||
|
#else
|
||||||
|
NavigationView {
|
||||||
|
EmptyView()
|
||||||
|
QualitySettings()
|
||||||
|
}
|
||||||
|
.navigationViewStyle(.stack)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,13 @@
|
|||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
static let matrixURL = URL(string: "https://tinyurl.com/matrix-yattee")!
|
static let matrixURL = URL(string: "https://tinyurl.com/matrix-yattee")!
|
||||||
static let discordURL = URL(string: "https://yattee.stream/discord")!
|
static let discordURL = URL(string: "https://yattee.stream/discord")!
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private enum Tabs: Hashable {
|
private enum Tabs: Hashable {
|
||||||
case locations, browsing, player, history, sponsorBlock, advanced, help
|
case locations, browsing, player, quality, history, sponsorBlock, advanced, help
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var selection = Tabs.locations
|
@State private var selection = Tabs.locations
|
||||||
@ -59,6 +58,14 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.tag(Tabs.player)
|
.tag(Tabs.player)
|
||||||
|
|
||||||
|
Form {
|
||||||
|
QualitySettings()
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("Quality", systemImage: "4k.tv")
|
||||||
|
}
|
||||||
|
.tag(Tabs.quality)
|
||||||
|
|
||||||
Form {
|
Form {
|
||||||
HistorySettings()
|
HistorySettings()
|
||||||
}
|
}
|
||||||
@ -92,18 +99,14 @@ struct SettingsView: View {
|
|||||||
.tag(Tabs.help)
|
.tag(Tabs.help)
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.frame(width: 480, height: windowHeight)
|
.frame(width: 520, height: windowHeight)
|
||||||
#else
|
#else
|
||||||
Group {
|
NavigationView {
|
||||||
|
settingsList
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
settingsList
|
.navigationBarHidden(true)
|
||||||
#else
|
|
||||||
NavigationView {
|
|
||||||
settingsList
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +145,12 @@ struct SettingsView: View {
|
|||||||
Label("Player", systemImage: "play.rectangle")
|
Label("Player", systemImage: "play.rectangle")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
QualitySettings()
|
||||||
|
} label: {
|
||||||
|
Label("Quality", systemImage: "4k.tv")
|
||||||
|
}
|
||||||
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
HistorySettings()
|
HistorySettings()
|
||||||
} label: {
|
} label: {
|
||||||
@ -219,6 +228,8 @@ struct SettingsView: View {
|
|||||||
return 390
|
return 390
|
||||||
case .player:
|
case .player:
|
||||||
return 420
|
return 420
|
||||||
|
case .quality:
|
||||||
|
return 400
|
||||||
case .history:
|
case .history:
|
||||||
return 480
|
return 480
|
||||||
case .sponsorBlock:
|
case .sponsorBlock:
|
||||||
|
@ -340,6 +340,15 @@
|
|||||||
375E45F627B1976B00BA7902 /* MPVOGLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F427B1976B00BA7902 /* MPVOGLView.swift */; };
|
375E45F627B1976B00BA7902 /* MPVOGLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F427B1976B00BA7902 /* MPVOGLView.swift */; };
|
||||||
375E45F827B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */; };
|
375E45F827B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */; };
|
||||||
375E45F927B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */; };
|
375E45F927B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */; };
|
||||||
|
375EC959289EEB8200751258 /* QualityProfileForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC958289EEB8200751258 /* QualityProfileForm.swift */; };
|
||||||
|
375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC958289EEB8200751258 /* QualityProfileForm.swift */; };
|
||||||
|
375EC95B289EEB8200751258 /* QualityProfileForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC958289EEB8200751258 /* QualityProfileForm.swift */; };
|
||||||
|
375EC95D289EEEE000751258 /* QualityProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC95C289EEEE000751258 /* QualityProfile.swift */; };
|
||||||
|
375EC95E289EEEE000751258 /* QualityProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC95C289EEEE000751258 /* QualityProfile.swift */; };
|
||||||
|
375EC95F289EEEE000751258 /* QualityProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC95C289EEEE000751258 /* QualityProfile.swift */; };
|
||||||
|
375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC969289F232600751258 /* QualityProfilesModel.swift */; };
|
||||||
|
375EC96B289F232600751258 /* QualityProfilesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC969289F232600751258 /* QualityProfilesModel.swift */; };
|
||||||
|
375EC96C289F232600751258 /* QualityProfilesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC969289F232600751258 /* QualityProfilesModel.swift */; };
|
||||||
375EC972289F2ABF00751258 /* MultiselectRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC971289F2ABF00751258 /* MultiselectRow.swift */; };
|
375EC972289F2ABF00751258 /* MultiselectRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC971289F2ABF00751258 /* MultiselectRow.swift */; };
|
||||||
375EC973289F2ABF00751258 /* MultiselectRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC971289F2ABF00751258 /* MultiselectRow.swift */; };
|
375EC973289F2ABF00751258 /* MultiselectRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC971289F2ABF00751258 /* MultiselectRow.swift */; };
|
||||||
375EC974289F2ABF00751258 /* MultiselectRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC971289F2ABF00751258 /* MultiselectRow.swift */; };
|
375EC974289F2ABF00751258 /* MultiselectRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375EC971289F2ABF00751258 /* MultiselectRow.swift */; };
|
||||||
@ -368,6 +377,7 @@
|
|||||||
3765917E27237D2A009F956E /* PINCache in Frameworks */ = {isa = PBXBuildFile; productRef = 3765917D27237D2A009F956E /* PINCache */; };
|
3765917E27237D2A009F956E /* PINCache in Frameworks */ = {isa = PBXBuildFile; productRef = 3765917D27237D2A009F956E /* PINCache */; };
|
||||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37666BA927023AF000F869E5 /* AccountSelectionView.swift */; };
|
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37666BA927023AF000F869E5 /* AccountSelectionView.swift */; };
|
||||||
3766AFD2273DA97D00686348 /* Int+FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */; };
|
3766AFD2273DA97D00686348 /* Int+FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */; };
|
||||||
|
3769537928A877C4005D87C3 /* StreamControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3795593527B08538007FF8F4 /* StreamControl.swift */; };
|
||||||
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
||||||
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
||||||
3769C0302779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
3769C0302779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
||||||
@ -518,6 +528,9 @@
|
|||||||
379775942689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
|
379775942689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
|
||||||
379775952689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
|
379775952689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
|
||||||
379B0253287A1CDF001015B5 /* OrientationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B0252287A1CDF001015B5 /* OrientationTracker.swift */; };
|
379B0253287A1CDF001015B5 /* OrientationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B0252287A1CDF001015B5 /* OrientationTracker.swift */; };
|
||||||
|
379F141F289ECE7F00DE48B5 /* QualitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */; };
|
||||||
|
379F1420289ECE7F00DE48B5 /* QualitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */; };
|
||||||
|
379F1421289ECE7F00DE48B5 /* QualitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */; };
|
||||||
37A3B15A27255E7F000FB5EE /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A3B15927255E7F000FB5EE /* SafariWebExtensionHandler.swift */; };
|
37A3B15A27255E7F000FB5EE /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A3B15927255E7F000FB5EE /* SafariWebExtensionHandler.swift */; };
|
||||||
37A3B15F27255E7F000FB5EE /* images in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B15E27255E7F000FB5EE /* images */; };
|
37A3B15F27255E7F000FB5EE /* images in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B15E27255E7F000FB5EE /* images */; };
|
||||||
37A3B16127255E7F000FB5EE /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16027255E7F000FB5EE /* manifest.json */; };
|
37A3B16127255E7F000FB5EE /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16027255E7F000FB5EE /* manifest.json */; };
|
||||||
@ -746,6 +759,7 @@
|
|||||||
37EBD8CB27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; };
|
37EBD8CB27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; };
|
||||||
37EBD8CC27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; };
|
37EBD8CC27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; };
|
||||||
37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37ECED55289FE166002BC2C9 /* SafeArea.swift */; };
|
37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37ECED55289FE166002BC2C9 /* SafeArea.swift */; };
|
||||||
|
37EE6DC528A305AD00BFD632 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 37EE6DC428A305AD00BFD632 /* Reachability */; };
|
||||||
37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
|
37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
|
||||||
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
|
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
|
||||||
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
|
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
|
||||||
@ -781,6 +795,9 @@
|
|||||||
37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; };
|
37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; };
|
||||||
37F64FE526FE70A60081B69E /* 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 */; };
|
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; };
|
||||||
|
37F7AB4D28A9361F00FB46B5 /* UIDevice+Cellular.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7AB4C28A9361F00FB46B5 /* UIDevice+Cellular.swift */; };
|
||||||
|
37F7AB5228A94EB900FB46B5 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37F7AB4E28A94E0600FB46B5 /* IOKit.framework */; };
|
||||||
|
37F7AB5528A951B200FB46B5 /* Power.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7AB5428A951B200FB46B5 /* Power.swift */; };
|
||||||
37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; };
|
37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; };
|
||||||
37F7D82D289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; };
|
37F7D82D289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; };
|
||||||
37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; };
|
37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; };
|
||||||
@ -1041,6 +1058,9 @@
|
|||||||
375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = "<group>"; };
|
375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = "<group>"; };
|
||||||
375E45F427B1976B00BA7902 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = "<group>"; };
|
375E45F427B1976B00BA7902 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = "<group>"; };
|
||||||
375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = "<group>"; };
|
375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = "<group>"; };
|
||||||
|
375EC958289EEB8200751258 /* QualityProfileForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualityProfileForm.swift; sourceTree = "<group>"; };
|
||||||
|
375EC95C289EEEE000751258 /* QualityProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualityProfile.swift; sourceTree = "<group>"; };
|
||||||
|
375EC969289F232600751258 /* QualityProfilesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualityProfilesModel.swift; sourceTree = "<group>"; };
|
||||||
375EC971289F2ABF00751258 /* MultiselectRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiselectRow.swift; sourceTree = "<group>"; };
|
375EC971289F2ABF00751258 /* MultiselectRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiselectRow.swift; sourceTree = "<group>"; };
|
||||||
375F740F289DC35A00747050 /* PlayerBackendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackendView.swift; sourceTree = "<group>"; };
|
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>"; };
|
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
||||||
@ -1093,6 +1113,7 @@
|
|||||||
379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = "<group>"; };
|
379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = "<group>"; };
|
||||||
37992DC726CC50BC003D4C27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
37992DC726CC50BC003D4C27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
379B0252287A1CDF001015B5 /* OrientationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrientationTracker.swift; sourceTree = "<group>"; };
|
379B0252287A1CDF001015B5 /* OrientationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrientationTracker.swift; sourceTree = "<group>"; };
|
||||||
|
379F141E289ECE7F00DE48B5 /* QualitySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettings.swift; sourceTree = "<group>"; };
|
||||||
37A3B15727255E7F000FB5EE /* Open in Yattee - macOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Open in Yattee - macOS.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
37A3B15727255E7F000FB5EE /* Open in Yattee - macOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Open in Yattee - macOS.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
37A3B15927255E7F000FB5EE /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
|
37A3B15927255E7F000FB5EE /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
|
||||||
37A3B15E27255E7F000FB5EE /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = "<group>"; };
|
37A3B15E27255E7F000FB5EE /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = "<group>"; };
|
||||||
@ -1217,6 +1238,9 @@
|
|||||||
37F4AD2528613B81004D0F66 /* Color+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Debug.swift"; sourceTree = "<group>"; };
|
37F4AD2528613B81004D0F66 /* Color+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Debug.swift"; sourceTree = "<group>"; };
|
||||||
37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; };
|
37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; };
|
||||||
37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; };
|
37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; };
|
||||||
|
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; };
|
||||||
|
37F7AB5428A951B200FB46B5 /* Power.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Power.swift; sourceTree = "<group>"; };
|
||||||
37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPickerModifier.swift; sourceTree = "<group>"; };
|
37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPickerModifier.swift; sourceTree = "<group>"; };
|
||||||
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapRecognizerViewModifier.swift; sourceTree = "<group>"; };
|
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapRecognizerViewModifier.swift; sourceTree = "<group>"; };
|
||||||
37F9619E27BD90BB00058149 /* PlayerBackendType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackendType.swift; sourceTree = "<group>"; };
|
37F9619E27BD90BB00058149 /* PlayerBackendType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackendType.swift; sourceTree = "<group>"; };
|
||||||
@ -1271,6 +1295,7 @@
|
|||||||
372AA410286D067B0000B1DC /* Repeat in Frameworks */,
|
372AA410286D067B0000B1DC /* Repeat in Frameworks */,
|
||||||
37C2212327ADA3F200305B41 /* libiconv.tbd in Frameworks */,
|
37C2212327ADA3F200305B41 /* libiconv.tbd in Frameworks */,
|
||||||
37C2212127ADA3A600305B41 /* libbz2.tbd in Frameworks */,
|
37C2212127ADA3A600305B41 /* libbz2.tbd in Frameworks */,
|
||||||
|
37EE6DC528A305AD00BFD632 /* Reachability in Frameworks */,
|
||||||
37C2211F27ADA3A200305B41 /* libz.tbd in Frameworks */,
|
37C2211F27ADA3A200305B41 /* libz.tbd in Frameworks */,
|
||||||
37FB284B2722099E00A57617 /* SDWebImageSwiftUI in Frameworks */,
|
37FB284B2722099E00A57617 /* SDWebImageSwiftUI in Frameworks */,
|
||||||
3736A210286BB72300C9E5EE /* libavcodec.xcframework in Frameworks */,
|
3736A210286BB72300C9E5EE /* libavcodec.xcframework in Frameworks */,
|
||||||
@ -1303,6 +1328,7 @@
|
|||||||
372AA414286D06A10000B1DC /* Repeat in Frameworks */,
|
372AA414286D06A10000B1DC /* Repeat in Frameworks */,
|
||||||
370F4FD227CC16CB001B35DC /* libavformat.59.16.100.dylib in Frameworks */,
|
370F4FD227CC16CB001B35DC /* libavformat.59.16.100.dylib in Frameworks */,
|
||||||
370F4FD327CC16CB001B35DC /* libass.9.dylib in Frameworks */,
|
370F4FD327CC16CB001B35DC /* libass.9.dylib in Frameworks */,
|
||||||
|
37F7AB5228A94EB900FB46B5 /* IOKit.framework in Frameworks */,
|
||||||
370F4FDF27CC16CB001B35DC /* libxcb-shape.0.0.0.dylib in Frameworks */,
|
370F4FDF27CC16CB001B35DC /* libxcb-shape.0.0.0.dylib in Frameworks */,
|
||||||
370F4FE127CC16CB001B35DC /* libuchardet.0.0.7.dylib in Frameworks */,
|
370F4FE127CC16CB001B35DC /* libuchardet.0.0.7.dylib in Frameworks */,
|
||||||
370F4FDB27CC16CB001B35DC /* libswscale.6.4.100.dylib in Frameworks */,
|
370F4FDB27CC16CB001B35DC /* libswscale.6.4.100.dylib in Frameworks */,
|
||||||
@ -1688,6 +1714,8 @@
|
|||||||
374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */,
|
374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */,
|
||||||
376BE50627347B57009AD608 /* SettingsHeader.swift */,
|
376BE50627347B57009AD608 /* SettingsHeader.swift */,
|
||||||
37B044B626F7AB9000E1419D /* SettingsView.swift */,
|
37B044B626F7AB9000E1419D /* SettingsView.swift */,
|
||||||
|
379F141E289ECE7F00DE48B5 /* QualitySettings.swift */,
|
||||||
|
375EC958289EEB8200751258 /* QualityProfileForm.swift */,
|
||||||
375EC971289F2ABF00751258 /* MultiselectRow.swift */,
|
375EC971289F2ABF00751258 /* MultiselectRow.swift */,
|
||||||
);
|
);
|
||||||
path = Settings;
|
path = Settings;
|
||||||
@ -1762,6 +1790,7 @@
|
|||||||
377FC7D1267A080300A6BBAF /* Frameworks */ = {
|
377FC7D1267A080300A6BBAF /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
37F7AB4E28A94E0600FB46B5 /* IOKit.framework */,
|
||||||
37C2212A27ADA43700305B41 /* VideoToolbox.framework */,
|
37C2212A27ADA43700305B41 /* VideoToolbox.framework */,
|
||||||
37C2212827ADA41400305B41 /* CoreMedia.framework */,
|
37C2212827ADA41400305B41 /* CoreMedia.framework */,
|
||||||
3772003227E8EEA100CB2475 /* AudioToolbox.framework */,
|
3772003227E8EEA100CB2475 /* AudioToolbox.framework */,
|
||||||
@ -1854,6 +1883,7 @@
|
|||||||
37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
|
37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
|
||||||
3751BA7D27E63F1D007B1A60 /* MPVOGLView.swift */,
|
3751BA7D27E63F1D007B1A60 /* MPVOGLView.swift */,
|
||||||
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */,
|
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */,
|
||||||
|
37F7AB5428A951B200FB46B5 /* Power.swift */,
|
||||||
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */,
|
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */,
|
||||||
3751BA7F27E64244007B1A60 /* VideoLayer.swift */,
|
3751BA7F27E64244007B1A60 /* VideoLayer.swift */,
|
||||||
37737785276F9858000521C1 /* Windows.swift */,
|
37737785276F9858000521C1 /* Windows.swift */,
|
||||||
@ -1879,6 +1909,7 @@
|
|||||||
377ABC47286E5887009C986F /* Sequence+Unique.swift */,
|
377ABC47286E5887009C986F /* Sequence+Unique.swift */,
|
||||||
3782B9512755667600990149 /* String+Format.swift */,
|
3782B9512755667600990149 /* String+Format.swift */,
|
||||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
||||||
|
37F7AB4C28A9361F00FB46B5 /* UIDevice+Cellular.swift */,
|
||||||
370B79CB286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift */,
|
370B79CB286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift */,
|
||||||
3743CA51270F284F00E4D32B /* View+Borders.swift */,
|
3743CA51270F284F00E4D32B /* View+Borders.swift */,
|
||||||
);
|
);
|
||||||
@ -2021,6 +2052,8 @@
|
|||||||
37130A5E277657300033018A /* PersistenceController.swift */,
|
37130A5E277657300033018A /* PersistenceController.swift */,
|
||||||
376578882685471400D4EA09 /* Playlist.swift */,
|
376578882685471400D4EA09 /* Playlist.swift */,
|
||||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||||
|
375EC95C289EEEE000751258 /* QualityProfile.swift */,
|
||||||
|
375EC969289F232600751258 /* QualityProfilesModel.swift */,
|
||||||
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
||||||
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
||||||
37F0F4E9286F397E00C06C2E /* SettingsModel.swift */,
|
37F0F4E9286F397E00C06C2E /* SettingsModel.swift */,
|
||||||
@ -2233,6 +2266,7 @@
|
|||||||
37CF8B8328535E4F00B71E37 /* SDWebImage */,
|
37CF8B8328535E4F00B71E37 /* SDWebImage */,
|
||||||
37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */,
|
37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */,
|
||||||
372AA40F286D067B0000B1DC /* Repeat */,
|
372AA40F286D067B0000B1DC /* Repeat */,
|
||||||
|
37EE6DC428A305AD00BFD632 /* Reachability */,
|
||||||
);
|
);
|
||||||
productName = "Yattee (iOS)";
|
productName = "Yattee (iOS)";
|
||||||
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
||||||
@ -2442,6 +2476,7 @@
|
|||||||
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */,
|
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */,
|
||||||
37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
|
37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
|
||||||
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */,
|
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */,
|
||||||
|
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -2710,6 +2745,7 @@
|
|||||||
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
|
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
|
||||||
37E80F40287B472300561799 /* ScrollContentBackground+Backport.swift in Sources */,
|
37E80F40287B472300561799 /* ScrollContentBackground+Backport.swift in Sources */,
|
||||||
|
375EC959289EEB8200751258 /* QualityProfileForm.swift in Sources */,
|
||||||
3727B74A27872A920021C15E /* VisualEffectBlur-iOS.swift in Sources */,
|
3727B74A27872A920021C15E /* VisualEffectBlur-iOS.swift in Sources */,
|
||||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||||
37130A5F277657300033018A /* PersistenceController.swift in Sources */,
|
37130A5F277657300033018A /* PersistenceController.swift in Sources */,
|
||||||
@ -2731,6 +2767,7 @@
|
|||||||
37B4E805277D0AB4004BF56A /* Orientation.swift in Sources */,
|
37B4E805277D0AB4004BF56A /* Orientation.swift in Sources */,
|
||||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||||
37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
|
37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
|
||||||
|
375EC95D289EEEE000751258 /* QualityProfile.swift in Sources */,
|
||||||
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */,
|
37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */,
|
||||||
37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
||||||
@ -2746,6 +2783,7 @@
|
|||||||
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||||
3752069D285E910600CA655F /* ChaptersView.swift in Sources */,
|
3752069D285E910600CA655F /* ChaptersView.swift in Sources */,
|
||||||
|
375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */,
|
||||||
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
||||||
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||||
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||||
@ -2799,6 +2837,7 @@
|
|||||||
374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
||||||
375F7410289DC35A00747050 /* PlayerBackendView.swift in Sources */,
|
375F7410289DC35A00747050 /* PlayerBackendView.swift in Sources */,
|
||||||
37FB28412721B22200A57617 /* ContentItem.swift in Sources */,
|
37FB28412721B22200A57617 /* ContentItem.swift in Sources */,
|
||||||
|
379F141F289ECE7F00DE48B5 /* QualitySettings.swift in Sources */,
|
||||||
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
||||||
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
|
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
|
||||||
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
|
||||||
@ -2835,6 +2874,7 @@
|
|||||||
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
|
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
|
||||||
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||||
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
|
37F7AB4D28A9361F00FB46B5 /* UIDevice+Cellular.swift in Sources */,
|
||||||
37141673267A8E10006CA35D /* Country.swift in Sources */,
|
37141673267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */,
|
||||||
37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
|
||||||
@ -2903,7 +2943,9 @@
|
|||||||
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
3703100327B0713600ECDDAA /* PlayerGestures.swift in Sources */,
|
3703100327B0713600ECDDAA /* PlayerGestures.swift in Sources */,
|
||||||
374C0540272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
374C0540272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
||||||
|
379F1420289ECE7F00DE48B5 /* QualitySettings.swift in Sources */,
|
||||||
3751BA8027E64244007B1A60 /* VideoLayer.swift in Sources */,
|
3751BA8027E64244007B1A60 /* VideoLayer.swift in Sources */,
|
||||||
|
375EC96B289F232600751258 /* QualityProfilesModel.swift in Sources */,
|
||||||
374C053627242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
374C053627242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
||||||
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||||
37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
@ -2918,6 +2960,7 @@
|
|||||||
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
|
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
|
||||||
|
375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */,
|
||||||
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
|
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
|
||||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
@ -2952,6 +2995,7 @@
|
|||||||
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||||
377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */,
|
377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */,
|
||||||
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||||
|
37F7AB5528A951B200FB46B5 /* Power.swift in Sources */,
|
||||||
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
||||||
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||||
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||||
@ -3029,6 +3073,7 @@
|
|||||||
37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
||||||
|
375EC95E289EEEE000751258 /* QualityProfile.swift in Sources */,
|
||||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
3703100027B04DCC00ECDDAA /* PlayerControls.swift in Sources */,
|
3703100027B04DCC00ECDDAA /* PlayerControls.swift in Sources */,
|
||||||
37130A5C277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
37130A5C277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
||||||
@ -3147,9 +3192,11 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
37579D5F27864F5F00FD0B98 /* Help.swift in Sources */,
|
37579D5F27864F5F00FD0B98 /* Help.swift in Sources */,
|
||||||
|
375EC95F289EEEE000751258 /* QualityProfile.swift in Sources */,
|
||||||
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */,
|
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||||
37DD9DBC2785D60300539416 /* FramePreferenceKey.swift in Sources */,
|
37DD9DBC2785D60300539416 /* FramePreferenceKey.swift in Sources */,
|
||||||
|
375EC974289F2ABF00751258 /* MultiselectRow.swift in Sources */,
|
||||||
37EBD8C827AF26B300F1C24B /* AVPlayerBackend.swift in Sources */,
|
37EBD8C827AF26B300F1C24B /* AVPlayerBackend.swift in Sources */,
|
||||||
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
|
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||||
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||||
@ -3159,6 +3206,7 @@
|
|||||||
374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
||||||
37BE0BD426A1D47D0092E2DB /* AppleAVPlayerView.swift in Sources */,
|
37BE0BD426A1D47D0092E2DB /* AppleAVPlayerView.swift in Sources */,
|
||||||
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||||
|
3769537928A877C4005D87C3 /* StreamControl.swift in Sources */,
|
||||||
3700155D271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
3700155D271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
||||||
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */,
|
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
3769C0302779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
3769C0302779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||||
@ -3168,6 +3216,7 @@
|
|||||||
3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
|
3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||||
3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */,
|
3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */,
|
||||||
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
|
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
|
||||||
|
379F1421289ECE7F00DE48B5 /* QualitySettings.swift in Sources */,
|
||||||
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||||
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
||||||
3776ADD8287381240078EBC4 /* Captions.swift in Sources */,
|
3776ADD8287381240078EBC4 /* Captions.swift in Sources */,
|
||||||
@ -3222,6 +3271,7 @@
|
|||||||
37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||||
37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */,
|
37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */,
|
||||||
375E45F627B1976B00BA7902 /* MPVOGLView.swift in Sources */,
|
375E45F627B1976B00BA7902 /* MPVOGLView.swift in Sources */,
|
||||||
|
375EC95B289EEB8200751258 /* QualityProfileForm.swift in Sources */,
|
||||||
371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||||
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
||||||
@ -3260,6 +3310,7 @@
|
|||||||
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||||
37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */,
|
37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */,
|
||||||
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */,
|
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||||
|
375EC96C289F232600751258 /* QualityProfilesModel.swift in Sources */,
|
||||||
372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */,
|
372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */,
|
||||||
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */,
|
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */,
|
||||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
@ -4297,6 +4348,14 @@
|
|||||||
minimumVersion = 5.0.0;
|
minimumVersion = 5.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 5.1.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
37FB28442722054B00A57617 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
|
37FB28442722054B00A57617 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git";
|
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git";
|
||||||
@ -4504,6 +4563,11 @@
|
|||||||
package = 37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;
|
package = 37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;
|
||||||
productName = SwiftUIPager;
|
productName = SwiftUIPager;
|
||||||
};
|
};
|
||||||
|
37EE6DC428A305AD00BFD632 /* Reachability */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */;
|
||||||
|
productName = Reachability;
|
||||||
|
};
|
||||||
37FB28452722054C00A57617 /* SDWebImageSwiftUI */ = {
|
37FB28452722054C00A57617 /* SDWebImageSwiftUI */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 37FB28442722054B00A57617 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
package = 37FB28442722054B00A57617 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
||||||
|
@ -45,6 +45,15 @@
|
|||||||
"version" : "1.2.1"
|
"version" : "1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "reachability.swift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2",
|
||||||
|
"version" : "5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "repeat",
|
"identity" : "repeat",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#import <ifaddrs.h>
|
||||||
#import <CoreFoundation/CoreFoundation.h>
|
#import <CoreFoundation/CoreFoundation.h>
|
||||||
#import "../Vendor/mpv/include/client.h"
|
#import "../Vendor/mpv/include/client.h"
|
||||||
#import "../Vendor/mpv/include/render.h"
|
#import "../Vendor/mpv/include/render.h"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#import <CoreFoundation/CoreFoundation.h>
|
#import <CoreFoundation/CoreFoundation.h>
|
||||||
|
#import <IOKit/ps/IOPowerSources.h>
|
||||||
#import "../Vendor/mpv/include/client.h"
|
#import "../Vendor/mpv/include/client.h"
|
||||||
#import "../Vendor/mpv/include/render.h"
|
#import "../Vendor/mpv/include/render.h"
|
||||||
#import "../Vendor/mpv/include/render_gl.h"
|
#import "../Vendor/mpv/include/render_gl.h"
|
||||||
|
38
macOS/Power.swift
Normal file
38
macOS/Power.swift
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Power {
|
||||||
|
static var hasInternalBattery: Bool {
|
||||||
|
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||||
|
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
|
||||||
|
|
||||||
|
for ps in psList {
|
||||||
|
if let psDesc = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? [String: Any] {
|
||||||
|
if let type = psDesc[kIOPSTypeKey] as? String {
|
||||||
|
if type == "InternalBattery" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
static var isConnectedToPower: Bool {
|
||||||
|
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||||
|
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
|
||||||
|
|
||||||
|
for ps in psList {
|
||||||
|
if let psDesc = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? [String: Any] {
|
||||||
|
if let type = psDesc[kIOPSTypeKey] as? String,
|
||||||
|
type == "InternalBattery",
|
||||||
|
let powerSourceState = (psDesc[kIOPSPowerSourceStateKey] as? String)
|
||||||
|
{
|
||||||
|
return powerSourceState == kIOPSACPowerValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user