mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 20:24:06 +00:00
Merge branch 'main' into add-user-agent-to-header
This commit is contained in:
@@ -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
109
Shared/LanguageCodes.swift
Normal 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"
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -65,7 +65,7 @@ import SwiftUI
|
||||
}
|
||||
|
||||
static var thumbnailHeight: Double {
|
||||
thumbnailWidth / 1.7777
|
||||
thumbnailWidth / (16 / 9)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -154,7 +154,12 @@ struct YatteeApp: App {
|
||||
#if DEBUG
|
||||
SiestaLog.Category.enabled = .common
|
||||
#endif
|
||||
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
|
||||
#if os(tvOS)
|
||||
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
|
||||
#else
|
||||
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
|
||||
#endif
|
||||
|
||||
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
|
||||
|
||||
if !Defaults[.lastAccountIsPublic] {
|
||||
|
@@ -387,7 +387,7 @@
|
||||
"Backend" = "الواجهة الخلفية";
|
||||
"Badge" = "الشارة";
|
||||
"Close PiP when starting playing other video" = "غلق الفيديو المصغر عند بدء تشغيل فيديو آخر";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).\n";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).";
|
||||
"Filter" = " عامل التصفية";
|
||||
"Frontend URL" = "عنوان URL للواجهة الأمامية";
|
||||
"Fullscreen size" = "حجم ملء الشاشة";
|
||||
@@ -628,4 +628,4 @@
|
||||
"Password required to import" = "كلمة المرور مطلوبة للإستيراد";
|
||||
"Password saved in import file" = "كلمة المرور محفوظة في ملف الإستيراد";
|
||||
"Export in progress..." = "جارِ التصدير...";
|
||||
"In progress..." = "في تَقَدم…";
|
||||
"In progress..." = "في طور الأجراء…";
|
||||
|
@@ -406,7 +406,7 @@
|
||||
"Country" = "País";
|
||||
"Clear All" = "Limpar Tudo";
|
||||
"Clear All Recents" = "Limpar Todos os Recentes";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).\n";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).";
|
||||
"Duration" = "Duração";
|
||||
"Edit Quality Profile" = "Editar Perfil de Qualidade";
|
||||
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussões acontecem no Discord e no Matrix. É um bom lugar para perguntas gerais.";
|
||||
|
Reference in New Issue
Block a user