Files
yattee/Shared/Settings/QualitySettings.swift
Arkadiusz Fal ce7ba207ea Fix API availability issues for macOS 11.0 and tvOS 15.0
This commit resolves multiple build errors caused by using APIs that
require newer OS versions than the deployment targets (macOS 11.0 and
tvOS 15.0).

macOS fixes:
- Add missing init(frame:) initializer to PlayerLayerView
- Add availability checks for textSelection modifier (macOS 12.0+)
- Add availability checks for AttributedString (macOS 12.0+)
- Add availability checks for listStyle.inset(alternatesRowBackgrounds:) (macOS 12.0+)
- Add availability checks for focusScope modifier (macOS 12.0+)
- Correct listRowSeparator availability from macOS 12.0 to 13.0

tvOS fixes:
- Use older onChange(of:) signature compatible with tvOS 15.0
- Add availability check for Menu with primaryAction (tvOS 17.0+)

All changes include appropriate fallbacks for older OS versions to
maintain backward compatibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 18:53:06 +01:00

216 lines
7.6 KiB
Swift

import Defaults
import SwiftUI
struct QualitySettings: View {
@State private var presentingProfileForm = false
@State private var editedProfileID: QualityProfile.ID?
@ObservedObject private var settings = SettingsModel.shared
@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
@Default(.forceAVPlayerForLiveStreams) private var forceAVPlayerForLiveStreams
var body: some View {
VStack(alignment: .leading) {
#if os(macOS)
sections
Spacer()
#else
List {
sections
}
#if os(tvOS)
.listStyle(.plain)
#elseif os(iOS)
.listStyle(.insetGrouped)
#endif
#endif
}
.sheet(isPresented: $presentingProfileForm) {
QualityProfileForm(qualityProfileID: $editedProfileID)
}
#if os(tvOS)
.buttonStyle(.plain)
.toggleStyle(TVOSPlainToggleStyle())
.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
forceAVPlayerForLiveStreamsToggle
}
.disabled(qualityProfiles.isEmpty)
Section(header: SettingsHeader(text: "Profiles".localized()), footer: profilesFooter) {
profilesList
Button {
editedProfileID = nil
presentingProfileForm = true
} label: {
Label("Add profile...", systemImage: "plus")
}
}
.frame(maxWidth: .infinity, alignment: .leading)
HStack {
Button {
settings.presentAlert(
Alert(
title: Text("Are you sure you want to restore default quality profiles?"),
message: Text("This will remove all your custom profiles and return their default values. This cannot be reverted."),
primaryButton: .destructive(Text("Reset")) {
QualityProfilesModel.shared.reset()
},
secondaryButton: .cancel()
)
)
} label: {
Text("Restore default profiles...")
.foregroundColor(.red)
}
Spacer()
}
}
}
@ViewBuilder var nonCellularBatteryDevicesProfilesPickers: some View {
Picker("Battery", selection: $batteryNonCellularProfile) { profilePickerOptions }
Picker("Charging", selection: $chargingNonCellularProfile) { profilePickerOptions }
}
@ViewBuilder var forceAVPlayerForLiveStreamsToggle: some View {
Toggle("Always use AVPlayer for live videos", isOn: $forceAVPlayerForLiveStreams)
}
@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 {
editedProfileID = qualityProfile.id
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 os(macOS)
if #available(macOS 12.0, *) {
List {
list
}
.listStyle(.inset(alternatesRowBackgrounds: true))
} else {
List {
list
}
.listStyle(.inset)
}
#else
list
#endif
}
}
struct QualitySettings_Previews: PreviewProvider {
static var previews: some View {
#if os(macOS)
QualitySettings()
#else
NavigationView {
EmptyView()
QualitySettings()
}
.navigationViewStyle(.stack)
#endif
}
}