diff --git a/Shared/Player/Controls/ControlsOverlay.swift b/Shared/Player/Controls/ControlsOverlay.swift index 728b22b0..b5846fae 100644 --- a/Shared/Player/Controls/ControlsOverlay.swift +++ b/Shared/Player/Controls/ControlsOverlay.swift @@ -21,7 +21,10 @@ struct ControlsOverlay: View { } @FocusState private var focusedField: Field? - @State private var presentingButtonHintAlert = false + @State private var presentingQualityProfileMenu = false + @State private var presentingStreamMenu = false + @State private var presentingCaptionsMenu = false + @State private var presentingAudioTrackMenu = false #endif var body: some View { @@ -102,19 +105,9 @@ struct ControlsOverlay: View { #if os(tvOS) .padding(.horizontal, 40) #endif - - #if os(tvOS) - Text("Press and hold remote button to open captions and quality menus") - .frame(maxWidth: 400) - .font(.caption) - .foregroundColor(.secondary) - #endif } - .frame(maxHeight: overlayHeight) + .frame(maxHeight: contentSize.height) #if os(tvOS) - .alert(isPresented: $presentingButtonHintAlert) { - Alert(title: Text("Press and hold to open this menu")) - } .onAppear { focusedField = .qualityProfile } @@ -125,14 +118,6 @@ struct ControlsOverlay: View { player.activeBackend == .mpv ? "Rate & Captions" : "Playback Rate" } - private var overlayHeight: Double { - #if os(tvOS) - contentSize.height + 80.0 - #else - contentSize.height - #endif - } - private func controlsHeader(_ text: String) -> some View { Text(text) .font(.system(.caption)) @@ -277,23 +262,25 @@ struct ControlsOverlay: View { .modifier(ControlBackgroundModifier()) .mask(RoundedRectangle(cornerRadius: 3)) #else - ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) { + ControlsOverlayButton( + focusedField: $focusedField, + field: .qualityProfile, + onSelect: { presentingQualityProfileMenu = true } + ) { Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) .lineLimit(1) .frame(maxWidth: 320) } - .contextMenu { + .alert("Quality Profile", isPresented: $presentingQualityProfileMenu) { Button("Automatic") { player.qualityProfileSelection = nil } ForEach(qualityProfiles) { qualityProfile in - Button { + Button(qualityProfile.description) { player.qualityProfileSelection = qualityProfile - } label: { - Text(qualityProfile.description) } - - Button("Cancel", role: .cancel) {} } + + Button("Cancel", role: .cancel) {} } #endif } @@ -328,7 +315,7 @@ struct ControlsOverlay: View { .modifier(ControlBackgroundModifier()) .mask(RoundedRectangle(cornerRadius: 3)) #else - StreamControl(focusedField: $focusedField) + StreamControl(focusedField: $focusedField, presentingStreamMenu: $presentingStreamMenu) #endif } @@ -367,7 +354,11 @@ struct ControlsOverlay: View { .modifier(ControlBackgroundModifier()) .mask(RoundedRectangle(cornerRadius: 3)) #else - ControlsOverlayButton(focusedField: $focusedField, field: .captions) { + ControlsOverlayButton( + focusedField: $focusedField, + field: .captions, + onSelect: { presentingCaptionsMenu = true } + ) { HStack(spacing: 8) { Image(systemName: "text.bubble") if let captions = captionsBinding.wrappedValue, @@ -386,7 +377,7 @@ struct ControlsOverlay: View { } .frame(maxWidth: 320) } - .contextMenu { + .alert("Captions", isPresented: $presentingCaptionsMenu) { Button("Disabled") { captionsBinding.wrappedValue = nil } ForEach(player.currentVideo?.captions ?? []) { caption in @@ -440,11 +431,15 @@ struct ControlsOverlay: View { .frame(maxWidth: 240, alignment: .trailing) .frame(height: 40) #else - ControlsOverlayButton(focusedField: $focusedField, field: .audioTrack) { + ControlsOverlayButton( + focusedField: $focusedField, + field: .audioTrack, + onSelect: { presentingAudioTrackMenu = true } + ) { Text(player.selectedAudioTrack?.displayLanguage ?? "Original") .frame(maxWidth: 320) } - .contextMenu { + .alert("Audio Track", isPresented: $presentingAudioTrackMenu) { ForEach(Array(player.availableAudioTracks.enumerated()), id: \.offset) { index, track in Button(track.description) { player.selectedAudioTrackIndex = index } } diff --git a/Shared/Player/StreamControl.swift b/Shared/Player/StreamControl.swift index 659bb5e4..17d6fcc1 100644 --- a/Shared/Player/StreamControl.swift +++ b/Shared/Player/StreamControl.swift @@ -3,9 +3,11 @@ import SwiftUI struct StreamControl: View { #if os(tvOS) var focusedField: FocusState.Binding? + @Binding var presentingStreamMenu: Bool - init(focusedField: FocusState.Binding?) { + init(focusedField: FocusState.Binding?, presentingStreamMenu: Binding) { self.focusedField = focusedField + _presentingStreamMenu = presentingStreamMenu } #endif @@ -45,16 +47,20 @@ struct StreamControl: View { .fixedSize() #endif #else - ControlsOverlayButton(focusedField: focusedField!, field: .stream) { + ControlsOverlayButton( + focusedField: focusedField!, + field: .stream, + onSelect: { presentingStreamMenu = true } + ) { Text(player.streamSelection?.shortQuality ?? "loading") .frame(maxWidth: 320) } - .contextMenu { + .alert("Stream Quality", isPresented: $presentingStreamMenu) { ForEach(streams) { stream in Button(stream.description) { player.streamSelection = stream } } - Button("Close", role: .cancel) {} + Button("Cancel", role: .cancel) {} } #endif } @@ -79,7 +85,7 @@ struct StreamControl: View { struct StreamControl_Previews: PreviewProvider { static var previews: some View { #if os(tvOS) - StreamControl(focusedField: .none) + StreamControl(focusedField: .none, presentingStreamMenu: .constant(false)) .injectFixtureEnvironmentObjects() #else StreamControl() diff --git a/tvOS/ControlsOverlayButton.swift b/tvOS/ControlsOverlayButton.swift index 1bc2ac6e..3ce643bf 100644 --- a/tvOS/ControlsOverlayButton.swift +++ b/tvOS/ControlsOverlayButton.swift @@ -4,25 +4,52 @@ struct ControlsOverlayButton: View { var focusedField: FocusState.Binding var field: ControlsOverlay.Field let label: LabelView + var onSelect: (() -> Void)? init( focusedField: FocusState.Binding, field: ControlsOverlay.Field, + onSelect: (() -> Void)? = nil, @ViewBuilder label: @escaping () -> LabelView ) { self.focusedField = focusedField self.field = field + self.onSelect = onSelect self.label = label() } var body: some View { - label - .padding() - .frame(width: 400) - .focusable() + let isFocused = focusedField.wrappedValue == field + + if let onSelect { + Button(action: onSelect) { + label + .padding() + .frame(width: 400) + } + .buttonStyle(TVButtonStyle(isFocused: isFocused)) .focused(focusedField, equals: field) - .background(focusedField.wrappedValue == field ? Color.white : Color.secondary) - .foregroundColor(focusedField.wrappedValue == field ? Color.black : Color.white) - .clipShape(RoundedRectangle(cornerRadius: 4)) + } else { + label + .padding() + .frame(width: 400) + .focusable() + .focused(focusedField, equals: field) + .background(isFocused ? Color.white : Color.secondary) + .foregroundColor(isFocused ? Color.black : Color.white) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + } +} + +struct TVButtonStyle: ButtonStyle { + let isFocused: Bool + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .background(isFocused ? Color.white : Color.secondary) + .foregroundColor(isFocused ? Color.black : Color.white) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) } }