diff --git a/Model/NavigationModel.swift b/Model/NavigationModel.swift index bb91903c..7ba899ec 100644 --- a/Model/NavigationModel.swift +++ b/Model/NavigationModel.swift @@ -78,6 +78,7 @@ final class NavigationModel: ObservableObject { @Published var presentingPlaylist = false @Published var sidebarSectionChanged = false + @Published var presentingPlaybackSettings = false @Published var presentingOpenVideos = false @Published var presentingSettings = false @Published var presentingAccounts = false diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 2c5ea8a1..cbbd1c01 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -95,6 +95,13 @@ final class PlayerModel: ObservableObject { @Published var availableStreams = [Stream]() { didSet { handleAvailableStreamsChange() } } @Published var streamSelection: Stream? { didSet { rebuildTVMenu() } } + @Published var captions: Captions? { didSet { + mpvBackend.captions = captions + if let code = captions?.code { + Defaults[.captionsLanguageCode] = code + } + }} + @Published var queue = [PlayerQueueItem]() { didSet { handleQueueChange() } } @Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } } @Published var videoBeingOpened: Video? { didSet { seek.reset() } } @@ -666,6 +673,7 @@ final class PlayerModel: ObservableObject { func handleCurrentItemChange() { if currentItem == nil { + captions = nil FeedModel.shared.calculateUnwatchedFeed() } diff --git a/Model/Player/PlayerStreams.swift b/Model/Player/PlayerStreams.swift index 4504b499..d66b23c3 100644 --- a/Model/Player/PlayerStreams.swift +++ b/Model/Player/PlayerStreams.swift @@ -16,6 +16,7 @@ extension PlayerModel { } func loadAvailableStreams(_ video: Video, onCompletion: @escaping (ResponseInfo) -> Void = { _ in }) { + captions = nil availableStreams = [] guard let playerInstance else { return } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 0fea31d7..a0cd1488 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -104,6 +104,13 @@ struct ContentView: View { } ) #endif + #if os(iOS) + .background( + EmptyView().sheet(isPresented: $navigation.presentingPlaybackSettings) { + PlaybackSettings() + } + ) + #endif .background( EmptyView().sheet(isPresented: $navigation.presentingOpenVideos) { OpenVideosView() diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 731b878f..b2bc4981 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -44,6 +44,7 @@ struct PlayerControls: View { @Default(.playerControlsMusicModeEnabled) private var playerControlsMusicModeEnabled private let controlsOverlayModel = ControlOverlaysModel.shared + private var navigation = NavigationModel.shared var playerControlsLayout: PlayerControlsLayout { player.playingFullScreen ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout @@ -345,7 +346,11 @@ struct PlayerControls: View { private var settingsButton: some View { button("settings", systemImage: "gearshape") { withAnimation(Self.animation) { - controlsOverlayModel.toggle() + #if os(tvOS) + controlsOverlayModel.toggle() + #else + navigation.presentingPlaybackSettings = true + #endif } } #if os(tvOS) diff --git a/Shared/Player/PlaybackSettings.swift b/Shared/Player/PlaybackSettings.swift new file mode 100644 index 00000000..51183381 --- /dev/null +++ b/Shared/Player/PlaybackSettings.swift @@ -0,0 +1,417 @@ +import Defaults +import SwiftUI + +struct PlaybackSettings: View { + @ObservedObject private var player = PlayerModel.shared + private var model = PlayerControlsModel.shared + + @State private var contentSize: CGSize = .zero + + @Default(.showMPVPlaybackStats) private var showMPVPlaybackStats + @Default(.qualityProfiles) private var qualityProfiles + + #if os(iOS) + @Environment(\.verticalSizeClass) private var verticalSizeClass + #endif + + #if os(tvOS) + enum Field: Hashable { + case qualityProfile + case stream + case increaseRate + case decreaseRate + case captions + } + + @FocusState private var focusedField: Field? + @State private var presentingButtonHintAlert = false + #endif + + var body: some View { + #if DEBUG + // TODO: remove + if #available(iOS 15.0, macOS 12.0, *) { + Self._printChanges() + } + #endif + return ScrollView { + VStack(alignment: .leading, spacing: 10) { + HStack { + Button { + withAnimation(ControlOverlaysModel.animation) { + NavigationModel.shared.presentingPlaybackSettings = false + } + } label: { + Label("Close", systemImage: "xmark") + .padding(.vertical) + + #if os(iOS) + .labelStyle(.iconOnly) + .frame(maxWidth: 50, alignment: .leading) + #endif + } + .keyboardShortcut(.cancelAction) + + Spacer() + Text("Playback Settings") + .font(.headline) + .frame(maxWidth: .infinity) + + Spacer() + .frame(maxWidth: 50, alignment: .trailing) + } + + HStack { + controlsHeader("Rate") + Spacer() + HStack(spacing: rateButtonsSpacing) { + decreaseRateButton + #if os(tvOS) + .focused($focusedField, equals: .decreaseRate) + #endif + rateButton + increaseRateButton + #if os(tvOS) + .focused($focusedField, equals: .increaseRate) + #endif + } + } + + if player.activeBackend == .mpv { + HStack { + controlsHeader("Captions") + Spacer() + captionsButton + #if os(tvOS) + .focused($focusedField, equals: .captions) + #endif + + #if os(iOS) + .foregroundColor(.white) + #endif + } + } + + HStack { + controlsHeader("Quality Profile".localized()) + Spacer() + qualityProfileButton + #if os(tvOS) + .focused($focusedField, equals: .qualityProfile) + #endif + } + + HStack { + controlsHeader("Stream".localized()) + Spacer() + streamButton + #if os(tvOS) + .focused($focusedField, equals: .stream) + #endif + } + + HStack(spacing: 8) { + controlsHeader("Backend".localized()) + Spacer() + backendButtons + } + + if player.activeBackend == .mpv, + showMPVPlaybackStats + { + Section(header: controlsHeader("Statistics".localized()).padding(.top, 15)) { + PlaybackStatsView() + } + } + } + #if os(iOS) + .padding(.top, verticalSizeClass == .regular ? 10 : 0) + .padding(.bottom, 15) + #else + .padding(.top) + #endif + .padding(.horizontal) + .overlay( + GeometryReader { geometry in + Color.clear.onAppear { + contentSize = geometry.size + } + } + ) + } + .animation(nil, value: player.activeBackend) + .frame(alignment: .topLeading) + + .ignoresSafeArea(.all, edges: .bottom) + .backport + .playbackSettingsPresentationDetents() + #if os(macOS) + .frame(width: 500) + .frame(minHeight: 350, maxHeight: 450) + #endif + } + + private func controlsHeader(_ text: String) -> some View { + Text(text) + } + + private var backendButtons: some View { + ForEach(PlayerBackendType.allCases, id: \.self) { backend in + backendButton(backend) + .frame(height: 40) + #if os(iOS) + .padding(12) + .frame(height: 50) + .background(RoundedRectangle(cornerRadius: 4).foregroundColor(player.activeBackend == backend ? Color.accentColor : Color.clear)) + .contentShape(Rectangle()) + #endif + } + } + + private func backendButton(_ backend: PlayerBackendType) -> some View { + Button { + player.saveTime { + player.changeActiveBackend(from: player.activeBackend, to: backend) + model.resetTimer() + } + } label: { + Text(backend.label) + .fontWeight(player.activeBackend == backend ? .bold : .regular) + #if os(iOS) + .foregroundColor(player.activeBackend == backend ? .white : .secondary) + #else + .foregroundColor(player.activeBackend == backend ? .accentColor : .secondary) + #endif + } + #if os(macOS) + .buttonStyle(.bordered) + #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: 70) + } + .transaction { t in t.animation = .none } + .buttonStyle(.plain) + .foregroundColor(.primary) + .frame(width: 70, height: 40) + #else + Text(player.rateLabel(player.currentRate)) + .frame(minWidth: 120) + #endif + } + + var ratePicker: some View { + Picker("Rate", selection: $player.currentRate) { + ForEach(player.backend.suggestedPlaybackRates, id: \.self) { rate in + Text(player.rateLabel(rate)).tag(rate) + } + } + .transaction { t in t.animation = .none } + } + + private var increaseRateButton: some View { + let increasedRate = player.backend.suggestedPlaybackRates.first { $0 > player.currentRate } + return Button { + if let rate = increasedRate { + player.currentRate = rate + } + } label: { + Label("Increase rate", systemImage: "plus") + .foregroundColor(.accentColor) + .imageScale(.large) + .labelStyle(.iconOnly) + #if os(iOS) + .padding(12) + .frame(width: 40, height: 40) + + .background(RoundedRectangle(cornerRadius: 4).strokeBorder(Color.accentColor, lineWidth: 1)) + .contentShape(Rectangle()) + #endif + } + #if os(macOS) + .buttonStyle(.bordered) + #endif + .disabled(increasedRate.isNil) + } + + private var decreaseRateButton: some View { + let decreasedRate = player.backend.suggestedPlaybackRates.last { $0 < player.currentRate } + + return Button { + if let rate = decreasedRate { + player.currentRate = rate + } + } label: { + Label("Decrease rate", systemImage: "minus") + .foregroundColor(.accentColor) + .imageScale(.large) + .labelStyle(.iconOnly) + #if os(iOS) + .padding(12) + .frame(width: 40, height: 40) + + .background(RoundedRectangle(cornerRadius: 4).strokeBorder(Color.accentColor, lineWidth: 1)) + .contentShape(Rectangle()) + #endif + } + #if os(macOS) + .buttonStyle(.bordered) + #elseif os(iOS) + #endif + .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 ?? "Automatic".localized()) + .frame(maxWidth: 240, alignment: .trailing) + } + .transaction { t in t.animation = .none } + .buttonStyle(.plain) + .foregroundColor(.accentColor) + .frame(maxWidth: 240, alignment: .trailing) + .frame(height: 40) + #else + ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) { + Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) + .lineLimit(1) + .frame(maxWidth: 320) + } + .contextMenu { + Button("Automatic") { player.qualityProfileSelection = nil } + + ForEach(qualityProfiles) { qualityProfile in + 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 streamButton: some View { + #if os(macOS) + StreamControl() + .labelsHidden() + .frame(maxWidth: 300) + #elseif os(iOS) + Menu { + StreamControl() + } label: { + Text(player.streamSelection?.resolutionAndFormat ?? "loading...") + .frame(width: 140, height: 40, alignment: .trailing) + .foregroundColor(player.streamSelection == nil ? .secondary : .accentColor) + } + .transaction { t in t.animation = .none } + .buttonStyle(.plain) + .frame(height: 40, alignment: .trailing) + #else + StreamControl(focusedField: $focusedField) + #endif + } + + @ViewBuilder private var captionsButton: some View { + #if os(macOS) + captionsPicker + .labelsHidden() + .frame(maxWidth: 300) + #elseif os(iOS) + Menu { + captionsPicker + } label: { + HStack(spacing: 4) { + Image(systemName: "text.bubble") + if let captions = player.captions { + Text(captions.code) + .foregroundColor(.accentColor) + } + } + .frame(alignment: .trailing) + .frame(height: 40) + } + .transaction { t in t.animation = .none } + .buttonStyle(.plain) + .foregroundColor(.accentColor) + #else + ControlsOverlayButton(focusedField: $focusedField, field: .captions) { + HStack(spacing: 8) { + Image(systemName: "text.bubble") + if let captions = captionsBinding.wrappedValue { + Text(captions.code) + } + } + .frame(maxWidth: 320) + } + .contextMenu { + Button("Disabled") { captionsBinding.wrappedValue = nil } + + ForEach(player.currentVideo?.captions ?? []) { caption in + Button(caption.description) { captionsBinding.wrappedValue = caption } + } + Button("Cancel", role: .cancel) {} + } + + #endif + } + + @ViewBuilder private var captionsPicker: some View { + let captions = player.currentVideo?.captions ?? [] + Picker("Captions", selection: $player.captions) { + if captions.isEmpty { + Text("Not available") + } else { + Text("Disabled").tag(Captions?.none) + } + ForEach(captions) { caption in + Text(caption.description).tag(Optional(caption)) + } + } + .disabled(captions.isEmpty) + } +} + +struct PlaybackSettings_Previews: PreviewProvider { + static var previews: some View { + PlaybackSettings() + } +} diff --git a/Shared/Player/PlaybackSettingsPresentationDetents+Backport.swift b/Shared/Player/PlaybackSettingsPresentationDetents+Backport.swift new file mode 100644 index 00000000..e4d8e0dc --- /dev/null +++ b/Shared/Player/PlaybackSettingsPresentationDetents+Backport.swift @@ -0,0 +1,13 @@ +import Foundation +import SwiftUI + +extension Backport where Content: View { + @ViewBuilder func playbackSettingsPresentationDetents() -> some View { + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) { + content + .presentationDetents([.height(350), .large]) + } else { + content + } + } +} diff --git a/Shared/Player/Video Details/VideoActions.swift b/Shared/Player/Video Details/VideoActions.swift index accac590..9bda40da 100644 --- a/Shared/Player/Video Details/VideoActions.swift +++ b/Shared/Player/Video Details/VideoActions.swift @@ -119,7 +119,11 @@ struct VideoActions: View { case .settings: actionButton("Settings", systemImage: "gear") { withAnimation(ControlOverlaysModel.animation) { - ControlOverlaysModel.shared.show() + #if os(tvOS) + ControlOverlaysModel.shared.show() + #else + navigation.presentingPlaybackSettings = true + #endif } } case .next: diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 5155f271..94122028 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -207,7 +207,7 @@ struct VideoDetails: View { .zIndex(1) #if !os(tvOS) - if #available(iOS 15, macOS 12, *) { + if #available(iOS 16, macOS 13, *) { Rectangle() .fill( LinearGradient( diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 19a5b5aa..afdf2b45 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -406,6 +406,13 @@ struct VideoPlayerView: View { #if os(iOS) .statusBar(hidden: fullScreenPlayer) #endif + #if os(macOS) + .background( + EmptyView().sheet(isPresented: $navigation.presentingPlaybackSettings) { + PlaybackSettings() + } + ) + #endif } var detailsNeedBottomPadding: Bool { diff --git a/Shared/Settings/AdvancedSettings.swift b/Shared/Settings/AdvancedSettings.swift index 7a30c6d4..484e4a4c 100644 --- a/Shared/Settings/AdvancedSettings.swift +++ b/Shared/Settings/AdvancedSettings.swift @@ -67,6 +67,9 @@ struct AdvancedSettings: View { Text("cache-secs") .frame(minWidth: 140, alignment: .leading) TextField("cache-secs", text: $mpvCacheSecs) + #if !os(macOS) + .keyboardType(.URL) + #endif } .multilineTextAlignment(.trailing) @@ -74,6 +77,9 @@ struct AdvancedSettings: View { Text("cache-pause-wait") .frame(minWidth: 140, alignment: .leading) TextField("cache-pause-wait", text: $mpvCachePauseWait) + #if !os(macOS) + .keyboardType(.URL) + #endif } .multilineTextAlignment(.trailing) diff --git a/Shared/Settings/PlayerControlsSettings.swift b/Shared/Settings/PlayerControlsSettings.swift index 613c65b3..e35f6a62 100644 --- a/Shared/Settings/PlayerControlsSettings.swift +++ b/Shared/Settings/PlayerControlsSettings.swift @@ -45,10 +45,6 @@ struct PlayerControlsSettings: View { List { sections } - #if !os(tvOS) - .backport - .scrollDismissesKeyboard() - #endif #endif } #if os(tvOS) @@ -215,23 +211,23 @@ struct PlayerControlsSettings: View { HStack { #if !os(tvOS) - Button { - var intValue = Int(value.wrappedValue) ?? 10 - intValue += 5 - if intValue <= 0 { - intValue = 5 + Label("Plus", systemImage: "plus") + .imageScale(.large) + .labelStyle(.iconOnly) + .padding(7) + .foregroundColor(.accentColor) + #if os(iOS) + .background(RoundedRectangle(cornerRadius: 4).strokeBorder(lineWidth: 1).foregroundColor(.accentColor)) + #endif + .contentShape(Rectangle()) + .onTapGesture { + var intValue = Int(value.wrappedValue) ?? 10 + intValue += 5 + if intValue <= 0 { + intValue = 5 + } + value.wrappedValue = String(intValue) } - value.wrappedValue = String(intValue) - } label: { - Label("Plus", systemImage: "plus") - .imageScale(.large) - .padding(7) - .labelStyle(.iconOnly) - .frame(minHeight: 35) - .foregroundColor(.primary) - .contentShape(Rectangle()) - } - .background(RoundedRectangle(cornerRadius: 4).stroke(lineWidth: 1)) #endif #if os(tvOS) @@ -250,24 +246,24 @@ struct PlayerControlsSettings: View { #endif #if !os(tvOS) - Button { - var intValue = Int(value.wrappedValue) ?? 10 - intValue -= 5 - if intValue <= 0 { - intValue = 5 + Label("Minus", systemImage: "minus") + .imageScale(.large) + .labelStyle(.iconOnly) + .padding(7) + .foregroundColor(.accentColor) + #if os(iOS) + .frame(minHeight: 35) + .background(RoundedRectangle(cornerRadius: 4).strokeBorder(lineWidth: 1).foregroundColor(.accentColor)) + #endif + .contentShape(Rectangle()) + .onTapGesture { + var intValue = Int(value.wrappedValue) ?? 10 + intValue -= 5 + if intValue <= 0 { + intValue = 5 + } + value.wrappedValue = String(intValue) } - value.wrappedValue = String(intValue) - } label: { - Label("Minus", systemImage: "minus") - .imageScale(.large) - .padding(7) - .labelStyle(.iconOnly) - .frame(minHeight: 35) - .foregroundColor(.primary) - .contentShape(Rectangle()) - } - .background(RoundedRectangle(cornerRadius: 4).stroke(lineWidth: 1)) - .buttonStyle(.plain) #endif } } diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index bd3cf3e0..f086e2f7 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -243,7 +243,7 @@ struct SettingsView: View { case .player: return 450 case .controls: - return 800 + return 850 case .quality: return 420 case .history: diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index ffbbb331..ed25f8db 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -687,6 +687,11 @@ 37A362BA2953707F00BDF328 /* ClearQueueButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A362B92953707F00BDF328 /* ClearQueueButton.swift */; }; 37A362BB2953707F00BDF328 /* ClearQueueButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A362B92953707F00BDF328 /* ClearQueueButton.swift */; }; 37A362BC2953707F00BDF328 /* ClearQueueButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A362B92953707F00BDF328 /* ClearQueueButton.swift */; }; + 37A362BE29537AAA00BDF328 /* PlaybackSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */; }; + 37A362BF29537AAA00BDF328 /* PlaybackSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */; }; + 37A362C229537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */; }; + 37A362C329537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */; }; + 37A362C429537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */; }; 37A5DBC4285DFF5400CA4DD1 /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = 37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */; }; 37A5DBC6285E06B100CA4DD1 /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = 37A5DBC5285E06B100CA4DD1 /* SwiftUIPager */; }; 37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; }; @@ -1363,6 +1368,8 @@ 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettings.swift; sourceTree = ""; }; 37A2B345294723850050933E /* CacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheModel.swift; sourceTree = ""; }; 37A362B92953707F00BDF328 /* ClearQueueButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearQueueButton.swift; sourceTree = ""; }; + 37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettings.swift; sourceTree = ""; }; + 37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaybackSettingsPresentationDetents+Backport.swift"; sourceTree = ""; }; 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlBackgroundModifier.swift; sourceTree = ""; }; 37A81BF8294BD1440081D322 /* WatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchView.swift; sourceTree = ""; }; 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCells.swift; sourceTree = ""; }; @@ -1803,6 +1810,8 @@ 37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */, 37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */, 37BA221029526A18000DAD1F /* ControlsGradientView.swift */, + 37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */, + 37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */, 375F740F289DC35A00747050 /* PlayerBackendView.swift */, 374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */, 3703100127B0713600ECDDAA /* PlayerGestures.swift */, @@ -3057,6 +3066,7 @@ 37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37C2211D27ADA33300305B41 /* MPVViewController.swift in Sources */, + 37A362BE29537AAA00BDF328 /* PlaybackSettings.swift in Sources */, 371B7E612759706A00D21217 /* CommentsView.swift in Sources */, 37D9BA0629507F69002586BD /* PlayerControlsSettings.swift in Sources */, 379DC3D128BA4EB400B09677 /* Seek.swift in Sources */, @@ -3262,6 +3272,7 @@ 37141673267A8E10006CA35D /* Country.swift in Sources */, 37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */, 37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */, + 37A362C229537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift in Sources */, 3748186E26A769D60084E870 /* DetailBadge.swift in Sources */, 3744A96028B99ADD005DE0A7 /* PlayerControlsLayout.swift in Sources */, 376BE50B27349108009AD608 /* BrowsingSettings.swift in Sources */, @@ -3451,6 +3462,7 @@ 3751B4B327836902000B7DF4 /* SearchPage.swift in Sources */, 3782B9532755667600990149 /* String+Format.swift in Sources */, 37635FE5291EA6CF00C11E79 /* AccentButton.swift in Sources */, + 37A362C329537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift in Sources */, 378E9C3D2945565500B2D696 /* SubscriptionsView.swift in Sources */, 3776ADD7287381240078EBC4 /* Captions.swift in Sources */, 37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */, @@ -3579,6 +3591,7 @@ 371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */, 3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */, 37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */, + 37A362BF29537AAA00BDF328 /* PlaybackSettings.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3728,6 +3741,7 @@ 37A5DBCA285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */, 37E80F46287B7AEC00561799 /* PlayerQueueView.swift in Sources */, 3786D060294C737300D23E82 /* RequestErrorButton.swift in Sources */, + 37A362C429537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift in Sources */, 37BDFF1929487B99000C6404 /* PlaylistVideosView.swift in Sources */, 37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 374924DC2921050B0017D862 /* LocationsSettings.swift in Sources */,