1
0
mirror of https://github.com/yattee/yattee.git synced 2024-12-15 06:40:32 +05:30
yattee/Shared/Player/Controls/ControlsOverlay.swift

448 lines
15 KiB
Swift
Raw Normal View History

import Defaults
import SwiftUI
struct ControlsOverlay: View {
@ObservedObject private var player = PlayerModel.shared
private var model = PlayerControlsModel.shared
@State private var availableCaptions: [Captions] = []
@State private var isLoadingCaptions = true
2022-08-14 22:36:22 +05:30
@State private var contentSize: CGSize = .zero
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
2022-08-14 22:36:22 +05:30
@Default(.qualityProfiles) private var qualityProfiles
#if os(tvOS)
2024-05-23 15:14:58 +05:30
enum Field: Hashable {
case qualityProfile
case stream
case increaseRate
case decreaseRate
case captions
}
2022-08-14 22:36:22 +05:30
2024-05-23 15:14:58 +05:30
@FocusState private var focusedField: Field?
@State private var presentingButtonHintAlert = false
2022-08-14 22:36:22 +05:30
#endif
var body: some View {
2022-07-05 22:50:25 +05:30
ScrollView {
2022-08-14 22:36:22 +05:30
VStack {
2022-11-18 03:19:08 +05:30
Section(header: controlsHeader(rateAndCaptionsLabel.localized())) {
2022-08-14 22:36:22 +05:30
HStack(spacing: rateButtonsSpacing) {
decreaseRateButton
#if os(tvOS)
.focused($focusedField, equals: .decreaseRate)
#endif
rateButton
increaseRateButton
#if os(tvOS)
.focused($focusedField, equals: .increaseRate)
#endif
}
2022-07-11 22:00:12 +05:30
2022-11-18 03:19:08 +05:30
if player.activeBackend == .mpv {
captionsButton
#if os(tvOS)
.focused($focusedField, equals: .captions)
#endif
2022-08-14 22:36:22 +05:30
2022-11-18 03:19:08 +05:30
#if os(iOS)
2022-08-14 22:36:22 +05:30
.foregroundColor(.white)
2022-11-18 03:19:08 +05:30
#endif
}
2022-08-14 22:36:22 +05:30
}
2022-09-27 18:52:40 +05:30
Section(header: controlsHeader("Quality Profile".localized())) {
2022-08-14 22:36:22 +05:30
qualityProfileButton
#if os(tvOS)
.focused($focusedField, equals: .qualityProfile)
#endif
2022-07-11 22:00:12 +05:30
}
2022-09-27 18:52:40 +05:30
Section(header: controlsHeader("Stream & Player".localized())) {
2022-08-14 22:36:22 +05:30
qualityButton
#if os(tvOS)
.focused($focusedField, equals: .stream)
#endif
2022-08-21 02:35:40 +05:30
HStack(spacing: 8) {
backendButtons
}
2022-07-05 22:50:25 +05:30
}
2022-07-05 22:50:25 +05:30
if player.activeBackend == .mpv,
showMPVPlaybackStats
{
2022-09-27 18:52:40 +05:30
Section(header: controlsHeader("Statistics".localized())) {
2022-09-02 04:49:15 +05:30
PlaybackStatsView()
2022-08-14 22:36:22 +05:30
}
#if os(tvOS)
.frame(width: 400)
#else
.frame(width: 240)
#endif
2022-07-05 22:50:25 +05:30
}
}
2022-08-14 22:36:22 +05:30
.overlay(
GeometryReader { geometry in
Color.clear.onAppear {
contentSize = geometry.size
}
}
)
#if os(tvOS)
.padding(.horizontal, 40)
#endif
2022-09-02 05:36:33 +05:30
#if os(tvOS)
2024-05-23 15:14:58 +05:30
Text("Press and hold remote button to open captions and quality menus")
.frame(maxWidth: 400)
.font(.caption)
.foregroundColor(.secondary)
2022-09-02 05:36:33 +05:30
#endif
}
2022-08-14 22:36:22 +05:30
.frame(maxHeight: overlayHeight)
#if os(tvOS)
2022-08-15 03:47:00 +05:30
.alert(isPresented: $presentingButtonHintAlert) {
Alert(title: Text("Press and hold to open this menu"))
}
2022-08-14 22:36:22 +05:30
.onAppear {
focusedField = .qualityProfile
}
#endif
}
2022-11-18 03:19:08 +05:30
private var rateAndCaptionsLabel: String {
player.activeBackend == .mpv ? "Rate & Captions" : "Playback Rate"
}
2022-08-14 22:36:22 +05:30
private var overlayHeight: Double {
#if os(tvOS)
2024-05-23 15:14:58 +05:30
contentSize.height + 80.0
2022-08-14 22:36:22 +05:30
#else
2024-05-23 15:14:58 +05:30
contentSize.height
2022-08-14 22:36:22 +05:30
#endif
}
private func controlsHeader(_ text: String) -> some View {
Text(text)
.font(.system(.caption))
.foregroundColor(.secondary)
}
private var backendButtons: some View {
ForEach(PlayerBackendType.allCases, id: \.self) { backend in
backendButton(backend)
2022-08-21 02:35:40 +05:30
#if !os(tvOS)
2022-08-14 22:36:22 +05:30
.frame(height: 40)
2022-08-21 02:35:40 +05:30
#endif
2022-08-14 22:36:22 +05:30
#if os(iOS)
2022-08-21 02:35:40 +05:30
.frame(maxWidth: 115)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 4))
2022-08-14 22:36:22 +05:30
#endif
}
}
private func backendButton(_ backend: PlayerBackendType) -> some View {
Button {
player.saveTime {
player.changeActiveBackend(from: player.activeBackend, to: backend)
model.resetTimer()
}
} label: {
Text(backend.label)
.foregroundColor(player.activeBackend == backend ? .accentColor : .secondary)
}
2022-08-14 22:36:22 +05:30
#if os(macOS)
.buttonStyle(.bordered)
#endif
}
@ViewBuilder private var rateButton: some View {
#if os(macOS)
2024-05-20 17:50:08 +05:30
ratePicker
2024-05-23 15:14:58 +05:30
.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))
2024-05-20 17:50:08 +05:30
#else
2024-05-23 15:14:58 +05:30
Text(player.rateLabel(player.currentRate))
.frame(minWidth: 120)
2022-08-14 22:36:22 +05:30
#endif
}
var ratePicker: some View {
Picker("Rate", selection: $player.currentRate) {
2022-11-11 03:49:34 +05:30
ForEach(player.backend.suggestedPlaybackRates, id: \.self) { rate in
2022-08-14 22:36:22 +05:30
Text(player.rateLabel(rate)).tag(rate)
}
}
.transaction { t in t.animation = .none }
}
private var increaseRateButton: some View {
2022-11-11 03:49:34 +05:30
let increasedRate = player.backend.suggestedPlaybackRates.first { $0 > player.currentRate }
return Button {
if let rate = increasedRate {
player.currentRate = rate
}
} label: {
Label("Increase rate", systemImage: "plus")
2022-08-06 19:52:41 +05:30
.foregroundColor(.primary)
.labelStyle(.iconOnly)
2022-06-25 22:35:08 +05:30
.padding(8)
2022-08-14 22:36:22 +05:30
.frame(width: 50, height: 40)
.contentShape(Rectangle())
}
#if os(macOS)
.buttonStyle(.bordered)
2022-08-14 22:36:22 +05:30
#elseif os(iOS)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 4))
#endif
.disabled(increasedRate.isNil)
}
private var decreaseRateButton: some View {
2022-11-11 03:49:34 +05:30
let decreasedRate = player.backend.suggestedPlaybackRates.last { $0 < player.currentRate }
return Button {
if let rate = decreasedRate {
player.currentRate = rate
}
} label: {
Label("Decrease rate", systemImage: "minus")
2022-08-06 19:52:41 +05:30
.foregroundColor(.primary)
.labelStyle(.iconOnly)
2022-06-25 22:35:08 +05:30
.padding(8)
2022-08-14 22:36:22 +05:30
.frame(width: 50, height: 40)
.contentShape(Rectangle())
}
#if os(macOS)
.buttonStyle(.bordered)
2022-08-14 22:36:22 +05:30
#elseif os(iOS)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 4))
#endif
.disabled(decreasedRate.isNil)
}
2022-08-14 22:36:22 +05:30
private var rateButtonsSpacing: Double {
#if os(tvOS)
2024-05-23 15:14:58 +05:30
10
2022-08-14 22:36:22 +05:30
#else
2024-05-23 15:14:58 +05:30
8
2022-08-14 22:36:22 +05:30
#endif
}
@ViewBuilder private var qualityProfileButton: some View {
#if os(macOS)
2024-05-20 17:50:08 +05:30
qualityProfilePicker
2024-05-23 15:14:58 +05:30
.labelsHidden()
.frame(maxWidth: 300)
#elseif os(iOS)
Menu {
qualityProfilePicker
} label: {
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
.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))
2022-08-14 22:36:22 +05:30
#else
2024-05-23 15:14:58 +05:30
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)
}
2022-08-14 22:36:22 +05:30
2024-05-23 15:14:58 +05:30
Button("Cancel", role: .cancel) {}
2022-08-14 22:36:22 +05:30
}
}
#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 {
#if os(macOS)
2024-05-20 17:50:08 +05:30
StreamControl()
2024-05-23 15:14:58 +05:30
.labelsHidden()
.frame(maxWidth: 300)
#elseif os(iOS)
Menu {
StreamControl()
} label: {
Text(player.streamSelection?.resolutionAndFormat ?? "loading")
.frame(width: 140, height: 40)
.foregroundColor(.primary)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)
.foregroundColor(.primary)
.frame(width: 240, height: 40)
.modifier(ControlBackgroundModifier())
.mask(RoundedRectangle(cornerRadius: 3))
2022-08-14 22:36:22 +05:30
#else
2024-05-23 15:14:58 +05:30
StreamControl(focusedField: $focusedField)
#endif
}
2022-07-05 22:50:25 +05:30
@ViewBuilder private var captionsButton: some View {
#if os(macOS)
2024-05-20 17:50:08 +05:30
captionsPicker
2024-05-23 15:14:58 +05:30
.labelsHidden()
.frame(maxWidth: 300)
#elseif os(iOS)
Menu {
captionsPicker
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
2024-05-20 17:50:08 +05:30
.foregroundColor(.accentColor)
2024-05-23 15:14:58 +05:30
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
2022-07-05 22:50:25 +05:30
}
}
2024-05-23 15:14:58 +05:30
.frame(width: 240)
.frame(height: 40)
2022-07-05 22:50:25 +05:30
}
2024-05-23 15:14:58 +05:30
.transaction { t in t.animation = .none }
.buttonStyle(.plain)
.foregroundColor(.primary)
2022-08-14 22:36:22 +05:30
.frame(width: 240)
2024-05-23 15:14:58 +05:30
.modifier(ControlBackgroundModifier())
.mask(RoundedRectangle(cornerRadius: 3))
2022-08-14 22:36:22 +05:30
#else
2024-05-23 15:14:58 +05:30
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
HStack(spacing: 8) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
2024-05-20 17:50:08 +05:30
.foregroundColor(.accentColor)
2024-05-23 15:14:58 +05:30
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
2022-08-14 22:36:22 +05:30
}
}
2024-05-23 15:14:58 +05:30
.frame(maxWidth: 320)
2022-08-14 22:36:22 +05:30
}
2024-05-23 15:14:58 +05:30
.contextMenu {
Button("Disabled") { captionsBinding.wrappedValue = nil }
2022-09-11 22:15:43 +05:30
ForEach(availableCaptions) { caption in
2024-05-23 15:14:58 +05:30
Button(caption.description) { captionsBinding.wrappedValue = caption }
}
Button("Cancel", role: .cancel) {}
2022-08-14 22:36:22 +05:30
}
2022-07-05 22:50:25 +05:30
#endif
}
@ViewBuilder private var captionsPicker: some View {
let captions = availableCaptions
2022-07-05 22:50:25 +05:30
Picker("Captions", selection: captionsBinding) {
if captions.isEmpty {
Text("Not available").tag(Captions?.none)
2022-07-05 22:50:25 +05:30
} else {
Text("Disabled").tag(Captions?.none)
ForEach(captions) { caption in
Text(caption.description).tag(Optional(caption))
}
2022-07-05 22:50:25 +05:30
}
}
.disabled(captions.isEmpty)
.onAppear {
loadCaptions()
}
}
private func loadCaptions() {
isLoadingCaptions = true
// Fetch captions asynchronously
Task {
let fetchedCaptions = await fetchCaptions()
await MainActor.run {
// Update state on the main thread
self.availableCaptions = fetchedCaptions
self.isLoadingCaptions = false
}
}
}
private func fetchCaptions() async -> [Captions] {
// Access currentVideo from the main actor context
await MainActor.run {
// Safely access the main actor-isolated currentVideo property
player.currentVideo?.captions ?? []
}
2022-07-05 22:50:25 +05:30
}
private var captionsBinding: Binding<Captions?> {
.init(
get: { player.mpvBackend.captions },
set: {
player.mpvBackend.captions = $0
Defaults[.captionsLanguageCode] = $0?.code
}
)
}
}
struct ControlsOverlay_Previews: PreviewProvider {
static var previews: some View {
ControlsOverlay()
}
}