Merge pull request #684 from stonerl/better-caption-handling

Improved Captions handling
This commit is contained in:
Arkadiusz Fal 2024-05-23 11:37:03 +02:00 committed by GitHub
commit 2f4fb9fc67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 471 additions and 130 deletions

View File

@ -217,9 +217,22 @@ final class MPVBackend: PlayerBackend {
#endif
var captions: Captions?
if let captionsLanguageCode = Defaults[.captionsLanguageCode] {
captions = video.captions.first { $0.code == captionsLanguageCode } ??
video.captions.first { $0.code.contains(captionsLanguageCode) }
if Defaults[.captionsAutoShow] == true {
let captionsDefaultLanguageCode = Defaults[.captionsDefaultLanguageCode],
captionsFallbackLanguageCode = Defaults[.captionsFallbackLanguageCode]
// Try to get captions with the default language code first
captions = video.captions.first { $0.code == captionsDefaultLanguageCode } ??
video.captions.first { $0.code.contains(captionsDefaultLanguageCode) }
// If there are still no captions, try to get captions with the fallback language code
if captions.isNil && !captionsFallbackLanguageCode.isEmpty {
captions = video.captions.first { $0.code == captionsFallbackLanguageCode } ??
video.captions.first { $0.code.contains(captionsFallbackLanguageCode) }
}
} else {
captions = nil
}
let updateCurrentStream = {
@ -254,9 +267,8 @@ final class MPVBackend: PlayerBackend {
self.startClientUpdates()
// Captions should only be displayed when selected by the user,
// not when the video starts. So, we remove them.
self.client?.removeSubs()
if Defaults[.captionsAutoShow] { self.client?.setSubToAuto() } else { self.client?.setSubToNo() }
PlayerModel.shared.captions = self.captions
if !preservingTime,
!upgrading,

View File

@ -68,6 +68,8 @@ final class MPVClient: ObservableObject {
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
checkError(mpv_set_option_string(mpv, "deinterlace", Defaults[.mpvDeinterlace] ? "yes" : "no"))
checkError(mpv_set_option_string(mpv, "sub-scale", Defaults[.captionsFontScaleSize]))
checkError(mpv_set_option_string(mpv, "sub-color", Defaults[.captionsFontColor]))
checkError(mpv_initialize(mpv))
@ -405,6 +407,22 @@ final class MPVClient: ObservableObject {
setString("video", "no")
}
func setSubToAuto() {
setString("sub", "auto")
}
func setSubToNo() {
setString("sub", "no")
}
func setSubFontSize(scaleSize: String) {
setString("sub-scale", scaleSize)
}
func setSubFontColor(color: String) {
setString("sub-color", color)
}
var tracksCount: Int {
Int(getString("track-list/count") ?? "-1") ?? -1
}

View File

@ -301,7 +301,12 @@ extension Defaults.Keys {
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
static let captionsAutoShow = Key<Bool>("captionsAutoShow", default: false)
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
static let captionsDefaultLanguageCode = Key<String>("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue)
static let captionsFallbackLanguageCode = Key<String>("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue)
static let captionsFontScaleSize = Key<String>("captionsFontScale", default: "1.0")
static let captionsFontColor = Key<String>("captionsFontColor", default: "#FFFFFF")
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)

109
Shared/LanguageCodes.swift Normal file
View File

@ -0,0 +1,109 @@
enum LanguageCodes: String, CaseIterable {
case Afrikaans = "af"
case Arabic = "ar"
case Azerbaijani = "az"
case Bengali = "bn"
case Catalan = "ca"
case Czech = "cs"
case Welsh = "cy"
case Danish = "da"
case German = "de"
case Greek = "el"
case English = "en"
case English_GB = "en-GB"
case Spanish = "es"
case Persian = "fa"
case Finnish = "fi"
case Filipino = "fil"
case French = "fr"
case Irish = "ga"
case Hebrew = "he"
case Hindi = "hi"
case Hungarian = "hu"
case Indonesian = "id"
case Italian = "it"
case Japanese = "ja"
case Javanese = "jv"
case Korean = "ko"
case Lithuanian = "lt"
case Malay = "ms"
case Maltese = "mt"
case Dutch = "nl"
case Norwegian = "no"
case Polish = "pl"
case Portuguese = "pt"
case Romanian = "ro"
case Russian = "ru"
case Slovak = "sk"
case Slovene = "sl"
case Swedish = "sv"
case Swahili = "sw"
case Thai = "th"
case Tagalog = "tl"
case Turkish = "tr"
case Ukrainian = "uk"
case Urdu = "ur"
case Uzbek = "uz"
case Vietnamese = "vi"
case Xhosa = "xh"
case Chinese = "zh"
case Zulu = "zu"
var description: String {
switch self {
case .Afrikaans: return "Afrikaans"
case .Arabic: return "Arabic"
case .Azerbaijani: return "Azerbaijani"
case .Bengali: return "Bengali"
case .Catalan: return "Catalan"
case .Czech: return "Czech"
case .Welsh: return "Welsh"
case .Danish: return "Danish"
case .German: return "German"
case .Greek: return "Greek"
case .English: return "English"
case .English_GB: return "English (United Kingdom)"
case .Spanish: return "Spanish"
case .Persian: return "Persian"
case .Finnish: return "Finnish"
case .Filipino: return "Filipino"
case .French: return "French"
case .Irish: return "Irish"
case .Hebrew: return "Hebrew"
case .Hindi: return "Hindi"
case .Hungarian: return "Hungarian"
case .Indonesian: return "Indonesian"
case .Italian: return "Italian"
case .Japanese: return "Japanese"
case .Javanese: return "Javanese"
case .Korean: return "Korean"
case .Lithuanian: return "Lithuanian"
case .Malay: return "Malay"
case .Maltese: return "Maltese"
case .Dutch: return "Dutch"
case .Norwegian: return "Norwegian"
case .Polish: return "Polish"
case .Portuguese: return "Portuguese"
case .Romanian: return "Romanian"
case .Russian: return "Russian"
case .Slovak: return "Slovak"
case .Slovene: return "Slovene"
case .Swedish: return "Swedish"
case .Swahili: return "Swahili"
case .Thai: return "Thai"
case .Tagalog: return "Tagalog"
case .Turkish: return "Turkish"
case .Ukrainian: return "Ukrainian"
case .Urdu: return "Urdu"
case .Uzbek: return "Uzbek"
case .Vietnamese: return "Vietnamese"
case .Xhosa: return "Xhosa"
case .Chinese: return "Chinese"
case .Zulu: return "Zulu"
}
}
static func languageName(for code: String) -> String {
return LanguageCodes(rawValue: code)?.description ?? "Unknown"
}
}

View File

@ -11,16 +11,16 @@ struct ControlsOverlay: View {
@Default(.qualityProfiles) private var qualityProfiles
#if os(tvOS)
enum Field: Hashable {
case qualityProfile
case stream
case increaseRate
case decreaseRate
case captions
}
enum Field: Hashable {
case qualityProfile
case stream
case increaseRate
case decreaseRate
case captions
}
@FocusState private var focusedField: Field?
@State private var presentingButtonHintAlert = false
@FocusState private var focusedField: Field?
@State private var presentingButtonHintAlert = false
#endif
var body: some View {
@ -94,10 +94,10 @@ struct ControlsOverlay: View {
#endif
#if os(tvOS)
Text("Press and hold remote button to open captions and quality menus")
.frame(maxWidth: 400)
.font(.caption)
.foregroundColor(.secondary)
Text("Press and hold remote button to open captions and quality menus")
.frame(maxWidth: 400)
.font(.caption)
.foregroundColor(.secondary)
#endif
}
.frame(maxHeight: overlayHeight)
@ -117,9 +117,9 @@ struct ControlsOverlay: View {
private var overlayHeight: Double {
#if os(tvOS)
contentSize.height + 80.0
contentSize.height + 80.0
#else
contentSize.height
contentSize.height
#endif
}
@ -160,26 +160,26 @@ struct ControlsOverlay: View {
@ViewBuilder private var rateButton: some View {
#if os(macOS)
ratePicker
.labelsHidden()
.frame(maxWidth: 100)
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
Menu {
ratePicker
} label: {
Text(player.rateLabel(player.currentRate))
.frame(minWidth: 120)
.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
}
@ -241,50 +241,50 @@ struct ControlsOverlay: View {
private var rateButtonsSpacing: Double {
#if os(tvOS)
10
10
#else
8
8
#endif
}
@ViewBuilder private var qualityProfileButton: some View {
#if os(macOS)
qualityProfilePicker
.labelsHidden()
.frame(maxWidth: 300)
qualityProfilePicker
.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))
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))
#else
ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) {
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
.lineLimit(1)
.frame(maxWidth: 320)
}
.contextMenu {
Button("Automatic") { player.qualityProfileSelection = nil }
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) {}
ForEach(qualityProfiles) { qualityProfile in
Button {
player.qualityProfileSelection = qualityProfile
} label: {
Text(qualityProfile.description)
}
Button("Cancel", role: .cancel) {}
}
}
#endif
}
@ -300,71 +300,91 @@ struct ControlsOverlay: View {
@ViewBuilder private var qualityButton: some View {
#if os(macOS)
StreamControl()
.labelsHidden()
.frame(maxWidth: 300)
StreamControl()
.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))
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))
#else
StreamControl(focusedField: $focusedField)
StreamControl(focusedField: $focusedField)
#endif
}
@ViewBuilder private var captionsButton: some View {
#if os(macOS)
captionsPicker
.labelsHidden()
.frame(maxWidth: 300)
captionsPicker
.labelsHidden()
.frame(maxWidth: 300)
#elseif os(iOS)
Menu {
captionsPicker
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue {
Text(captions.code)
.foregroundColor(.primary)
}
}
.frame(width: 240)
.frame(height: 40)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)
.foregroundColor(.primary)
.frame(width: 240)
.modifier(ControlBackgroundModifier())
.mask(RoundedRectangle(cornerRadius: 3))
#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 }
Menu {
captionsPicker
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
ForEach(player.currentVideo?.captions ?? []) { caption in
Button(caption.description) { captionsBinding.wrappedValue = caption }
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
}
Button("Cancel", role: .cancel) {}
}
.frame(width: 240)
.frame(height: 40)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)
.foregroundColor(.primary)
.frame(width: 240)
.modifier(ControlBackgroundModifier())
.mask(RoundedRectangle(cornerRadius: 3))
#else
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))")
.foregroundColor(.accentColor)
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
}
}
.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
}

View File

@ -1,3 +1,4 @@
import Combine
import Defaults
import SwiftUI
@ -383,23 +384,35 @@ struct PlaybackSettings: View {
}
@ViewBuilder private var captionsButton: some View {
let videoCaptions = player.currentVideo?.captions
#if os(macOS)
captionsPicker
.labelsHidden()
.frame(maxWidth: 300)
#elseif os(iOS)
Menu {
captionsPicker
if videoCaptions?.isEmpty == false {
captionsPicker
}
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = player.captions {
Text(captions.code)
if let captions = player.captions,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if videoCaptions?.isEmpty == true {
Text("Not available")
} else {
Text("Disabled")
}
}
}
.frame(alignment: .trailing)
.frame(height: 40)
.disabled(videoCaptions?.isEmpty == true)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)

View File

@ -30,12 +30,19 @@ struct PlayerSettings: View {
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
@Default(.showRelated) private var showRelated
@Default(.showInspector) private var showInspector
@Default(.showChapters) private var showChapters
@Default(.showChapterThumbnails) private var showThumbnails
@Default(.showChapterThumbnailsOnlyWhenDifferent) private var showThumbnailsOnlyWhenDifferent
@Default(.expandChapters) private var expandChapters
@Default(.showRelated) private var showRelated
@Default(.captionsAutoShow) private var captionsAutoShow
@Default(.captionsDefaultLanguageCode) private var captionsDefaultLanguageCode
@Default(.captionsFallbackLanguageCode) private var captionsFallbackLanguageCode
@Default(.captionsFontScaleSize) private var captionsFontScaleSize
@Default(.captionsFontColor) private var captionsFontColor
@ObservedObject private var accounts = AccountsModel.shared
@ -45,6 +52,11 @@ struct PlayerSettings: View {
}
#endif
#if os(tvOS)
@State private var isShowingDefaultLanguagePicker = false
@State private var isShowingFallbackLanguagePicker = false
#endif
var body: some View {
Group {
#if os(macOS)
@ -93,7 +105,54 @@ struct PlayerSettings: View {
inspectorVisibilityPicker
#endif
}
#endif
Section(header: SettingsHeader(text: "Captions".localized())) {
#if os(tvOS)
Text("Size").font(.subheadline)
#endif
captionsFontScaleSizePicker
#if os(tvOS)
Text("Color").font(.subheadline)
#endif
captionsFontColorPicker
showCaptionsAutoShowToggle
#if !os(tvOS)
captionDefaultLanguagePicker
captionFallbackLanguagePicker
#else
Button(action: { isShowingDefaultLanguagePicker = true }) {
HStack {
Text("Default language")
Spacer()
Text("\(LanguageCodes(rawValue: captionsDefaultLanguageCode)!.description.capitalized) (\(captionsDefaultLanguageCode))").foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) {
defaultLanguagePickerTVOS(
selectedLanguage: $captionsDefaultLanguageCode,
isShowing: $isShowingDefaultLanguagePicker
)
}
Button(action: { isShowingFallbackLanguagePicker = true }) {
HStack {
Text("Fallback language")
Spacer()
Text("\(LanguageCodes(rawValue: captionsFallbackLanguageCode)!.description.capitalized) (\(captionsFallbackLanguageCode))").foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) {
fallbackLanguagePickerTVOS(
selectedLanguage: $captionsFallbackLanguageCode,
isShowing: $isShowingFallbackLanguagePicker
)
}
#endif
}
#if !os(tvOS)
Section(header: SettingsHeader(text: "Chapters".localized())) {
showChaptersToggle
showThumbnailsToggle
@ -279,6 +338,103 @@ struct PlayerSettings: View {
}
#endif
private var showCaptionsAutoShowToggle: some View {
Toggle("Always show captions", isOn: $captionsAutoShow)
}
private var captionsFontScaleSizePicker: some View {
Picker("Size", selection: $captionsFontScaleSize) {
Text("Small").tag(String("0.5"))
Text("Medium").tag(String("1.0"))
Text("Large").tag(String("2.0"))
}
.onChange(of: captionsFontScaleSize) { _ in
PlayerModel.shared.mpvBackend.client.setSubFontSize(scaleSize: captionsFontScaleSize)
}
#if os(macOS)
.labelsHidden()
#endif
}
private var captionsFontColorPicker: some View {
Picker("Color", selection: $captionsFontColor) {
Text("White").tag(String("#FFFFFF"))
Text("Yellow").tag(String("#FFFF00"))
Text("Red").tag(String("#FF0000"))
Text("Orange").tag(String("#FFA500"))
Text("Green").tag(String("#008000"))
Text("Blue").tag(String("#0000FF"))
}
.onChange(of: captionsFontColor) { _ in
PlayerModel.shared.mpvBackend.client.setSubFontColor(color: captionsFontColor)
}
#if os(macOS)
.labelsHidden()
#endif
}
#if !os(tvOS)
private var captionDefaultLanguagePicker: some View {
Picker("Default language", selection: $captionsDefaultLanguageCode) {
ForEach(LanguageCodes.allCases, id: \.self) { language in
Text("\(language.description.capitalized) (\(language.rawValue))").tag(language.rawValue)
}
}
#if os(macOS)
.labelsHidden()
#endif
}
private var captionFallbackLanguagePicker: some View {
Picker("Fallback language", selection: $captionsFallbackLanguageCode) {
ForEach(LanguageCodes.allCases, id: \.self) { language in
Text("\(language.description.capitalized) (\(language.rawValue))").tag(language.rawValue)
}
}
#if os(macOS)
.labelsHidden()
#endif
}
#else
struct defaultLanguagePickerTVOS: View {
@Binding var selectedLanguage: String
@Binding var isShowing: Bool
var body: some View {
NavigationView {
List(LanguageCodes.allCases, id: \.self) { language in
Button(action: {
selectedLanguage = language.rawValue
isShowing = false
}) {
Text("\(language.description.capitalized) (\(language.rawValue))")
}
}
.navigationTitle("Select Default Language")
}
}
}
struct fallbackLanguagePickerTVOS: View {
@Binding var selectedLanguage: String
@Binding var isShowing: Bool
var body: some View {
NavigationView {
List(LanguageCodes.allCases, id: \.self) { language in
Button(action: {
selectedLanguage = language.rawValue
isShowing = false
}) {
Text("\(language.description.capitalized) (\(language.rawValue))")
}
}
.navigationTitle("Select Fallback Language")
}
}
}
#endif
#if !os(tvOS)
private var inspectorVisibilityPicker: some View {
Picker("Inspector", selection: $showInspector) {

View File

@ -1076,6 +1076,9 @@
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
FA97174C2A494700001FF53D /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA97174B2A494700001FF53D /* MPVKit */; };
/* End PBXBuildFile section */
@ -1547,6 +1550,7 @@
3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; };
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatus.swift; sourceTree = "<group>"; };
E258F3892BF61BD2005B8C28 /* URLTester.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTester.swift; sourceTree = "<group>"; };
E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageCodes.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -2295,6 +2299,7 @@
37D2E0D328B67EFC00F64D52 /* Delay.swift */,
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */,
E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */,
375B537828DF6CBB004C1D19 /* Localizable.strings */,
3729037D2739E47400EA99F6 /* MenuCommands.swift */,
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */,
@ -3225,6 +3230,7 @@
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
@ -3405,6 +3411,7 @@
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */,
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
@ -4000,6 +4007,7 @@
37FB28432721B22200A57617 /* ContentItem.swift in Sources */,
37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */,
37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */,
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */,
37AAF2A226741C97007FC770 /* FeedView.swift in Sources */,
37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */,