mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Switching accounts was buried in Subscriptions settings behind a picker and a confirmation dialog, and it wrote two stores that could disagree. One shared switcher now sits in the Subscriptions and Playlists toolbars, the macOS sidebar footer and the Settings root, and every path goes through AppEnvironment.switchDataContext(to:) so a switch behaves the same wherever it is made. Switching is one tap: it is reversible, so there is nothing to confirm. Rows come from DataContextValidator.availableContexts, name themselves through displayName(for:), and carry a status suffix when the account is not signed in — plus the instance host when two accounts share a name. tvOS gets the same rows as a full-screen picker. Subscriptions also explains itself when the selected account cannot answer: a thin banner names the cause and says the data below is local. This is the first consumer of the validator's unavailability, and correctIfContextRemoved() now runs on foreground activation, where SubscriptionAccountValidator used to auto-correct any selection it could not immediately verify. With the switcher reading the context directly, the legacy subscriptionAccount setting has no readers left: the setting, its validator, the mirror that kept it in step and the feed cache's instance-keyed fallbacks all go. The cache invalidation the mirror also carried stays, under its own name. AccountsManager still reads the raw legacy key for the one-way context migration.
560 lines
20 KiB
Swift
560 lines
20 KiB
Swift
//
|
|
// SettingsManager.swift
|
|
// Yattee
|
|
//
|
|
// Manages user settings with iCloud sync via NSUbiquitousKeyValueStore.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
#if os(iOS)
|
|
import CoreHaptics
|
|
import UIKit
|
|
#endif
|
|
|
|
/// Manages application settings with platform-specific keys and iCloud sync.
|
|
@MainActor
|
|
@Observable
|
|
final class SettingsManager {
|
|
// MARK: - Storage
|
|
|
|
let ubiquitousStore = NSUbiquitousKeyValueStore.default
|
|
let localDefaults = UserDefaults.standard
|
|
|
|
// MARK: - Backing Storage for @Observable
|
|
// These stored properties trigger observation when modified.
|
|
// Internal access for extension use.
|
|
|
|
// Theme
|
|
var _theme: AppTheme?
|
|
var _accentColor: AccentColor?
|
|
var _customAccentColor: String?
|
|
var _accentColorDark: AccentColor?
|
|
var _customAccentColorDark: String?
|
|
var _useSeparateDarkAccentColor: Bool?
|
|
var _showWatchedCheckmark: Bool?
|
|
|
|
// Playback
|
|
var _preferredQuality: VideoQuality?
|
|
var _cellularQuality: VideoQuality?
|
|
var _backgroundPlaybackEnabled: Bool?
|
|
var _preferredAudioLanguage: String?
|
|
var _preferredSubtitlesLanguage: String?
|
|
var _playerVolume: Float?
|
|
var _resumeAction: ResumeAction?
|
|
var _tvOSMenuButtonClosesVideo: Bool?
|
|
var _allowSoftwareDecodedFormats: Bool?
|
|
var _audioOnlyModeEnabled: Bool?
|
|
|
|
// SponsorBlock
|
|
var _sponsorBlockEnabled: Bool?
|
|
var _sponsorBlockCategories: Set<SponsorBlockCategory>?
|
|
var _sponsorBlockAPIURL: String?
|
|
|
|
// Return YouTube Dislike & DeArrow
|
|
var _returnYouTubeDislikeEnabled: Bool?
|
|
var _deArrowEnabled: Bool?
|
|
var _deArrowReplaceTitles: Bool?
|
|
var _deArrowReplaceThumbnails: Bool?
|
|
var _deArrowAPIURL: String?
|
|
var _deArrowThumbnailAPIURL: String?
|
|
|
|
// Short link resolution
|
|
var _resolveShortLinksEnabled: Bool?
|
|
|
|
// User Agent
|
|
var _customUserAgent: String?
|
|
var _randomizeUserAgentPerRequest: Bool?
|
|
|
|
// Feed
|
|
var _feedCacheValidityMinutes: Int?
|
|
|
|
// Player
|
|
var _keepPlayerPinnedEnabled: Bool?
|
|
#if os(iOS)
|
|
var _inAppOrientationLock: Bool?
|
|
var _rotateToMatchAspectRatio: Bool?
|
|
var _preferPortraitBrowsing: Bool?
|
|
#endif
|
|
#if os(macOS)
|
|
var _macPlayerSeparateWindow: Bool?
|
|
var _macPlayerFloating: Bool?
|
|
var _macControlsBarOffsetX: Double?
|
|
var _macControlsBarOffsetY: Double?
|
|
var _playerSheetAutoResize: Bool?
|
|
#endif
|
|
|
|
// Mini Player Minimize Behavior is kept as it's not part of the preset
|
|
|
|
// Mini Player Minimize Behavior (iOS 26+)
|
|
#if os(iOS)
|
|
var _miniPlayerMinimizeBehavior: (any RawRepresentable)?
|
|
#endif
|
|
|
|
// Haptics (iOS)
|
|
#if os(iOS)
|
|
var _hapticFeedbackEnabled: Bool?
|
|
var _hapticFeedbackIntensity: HapticFeedbackIntensity?
|
|
#endif
|
|
|
|
// iCloud sync
|
|
var _iCloudSyncEnabled: Bool?
|
|
var _lastSyncTime: Date?
|
|
var _syncInstances: Bool?
|
|
var _syncSubscriptions: Bool?
|
|
var _syncBookmarks: Bool?
|
|
var _syncPlaybackHistory: Bool?
|
|
var _syncPlaylists: Bool?
|
|
var _syncSettings: Bool?
|
|
var _syncMediaSources: Bool?
|
|
var _syncSearchHistory: Bool?
|
|
|
|
// Search history
|
|
var _searchHistoryLimit: Int?
|
|
|
|
// Home settings
|
|
var _homeShortcutOrder: [HomeShortcutItem]?
|
|
var _homeShortcutVisibility: [HomeShortcutItem: Bool]?
|
|
var _homeShortcutLayout: HomeShortcutLayout?
|
|
var _homeShortcutCardStyle: HomeShortcutCardStyle?
|
|
var _homeShortcutCardColor: HomeShortcutCardColor?
|
|
var _homeShortcutColorfulPalette: HomeShortcutColorfulPalette?
|
|
var _homeShortcutCustomPaletteColors: [String]?
|
|
var _homeSectionOrder: [HomeSectionItem]?
|
|
var _homeSectionVisibility: [HomeSectionItem: Bool]?
|
|
var _homeSectionItemsLimit: Int?
|
|
var _homeSectionLayout: HomeSectionLayout?
|
|
|
|
// Top Shelf (tvOS)
|
|
var _topShelfSections: [TopShelfSection]?
|
|
|
|
// Custom MPV options (local-only)
|
|
var _customMPVOptions: [String: String]?
|
|
|
|
// Tab bar settings (compact size class only - iOS)
|
|
var _tabBarItemOrder: [TabBarItem]?
|
|
var _tabBarItemVisibility: [TabBarItem: Bool]?
|
|
|
|
// Sidebar settings
|
|
var _sidebarMainItemOrder: [SidebarMainItem]?
|
|
var _sidebarMainItemVisibility: [SidebarMainItem: Bool]?
|
|
var _sidebarStartupTab: SidebarMainItem?
|
|
|
|
// Tab bar startup
|
|
var _tabBarStartupTab: SidebarMainItem?
|
|
var _sidebarSourcesEnabled: Bool?
|
|
var _sidebarSourceSort: SidebarSourceSort?
|
|
var _sidebarSourcesLimitEnabled: Bool?
|
|
var _sidebarMaxSources: Int?
|
|
var _sidebarChannelsEnabled: Bool?
|
|
var _sidebarMaxChannels: Int?
|
|
var _sidebarChannelSort: SidebarChannelSort?
|
|
var _sidebarChannelsLimitEnabled: Bool?
|
|
var _sidebarPlaylistsEnabled: Bool?
|
|
var _sidebarMaxPlaylists: Int?
|
|
var _sidebarPlaylistSort: SidebarPlaylistSort?
|
|
var _sidebarPlaylistsLimitEnabled: Bool?
|
|
|
|
// iCloud startup sync protection
|
|
/// When true, suppresses iCloud writes from set() methods to prevent
|
|
/// stale local values from overwriting newer iCloud data during app startup.
|
|
var isInitialSyncPending = false
|
|
|
|
// Advanced settings
|
|
var _showAdvancedStreamDetails: Bool?
|
|
var _showPlayerAreaDebug: Bool?
|
|
var _showTVDebugButton: Bool?
|
|
var _verboseMPVLogging: Bool?
|
|
var _verboseRemoteControlLogging: Bool?
|
|
var _mpvBufferSeconds: Double?
|
|
var _mpvUseEDLStreams: Bool?
|
|
var _zoomTransitionsEnabled: Bool?
|
|
var _tvMatchDisplayFrameRate: Bool?
|
|
var _tvMatchDisplayDynamicRange: Bool?
|
|
var _tvAudioDelayMs: Double?
|
|
var _tvVideoSyncMode: TVVideoSyncMode?
|
|
|
|
// Details panel settings
|
|
var _floatingDetailsPanelSide: FloatingPanelSide?
|
|
var _floatingDetailsPanelWidth: CGFloat?
|
|
var _landscapeDetailsPanelVisible: Bool?
|
|
var _landscapeDetailsPanelPinned: Bool?
|
|
|
|
// Notification settings
|
|
var _backgroundNotificationsEnabled: Bool?
|
|
var _defaultNotificationsForNewChannels: Bool?
|
|
var _lastBackgroundCheck: Date?
|
|
var _clipboardURLDetectionEnabled: Bool?
|
|
var _incognitoModeEnabled: Bool?
|
|
var _historyRetentionDays: Int?
|
|
var _saveWatchHistory: Bool?
|
|
var _saveRecentSearches: Bool?
|
|
var _saveRecentChannels: Bool?
|
|
var _saveRecentPlaylists: Bool?
|
|
|
|
// Queue settings
|
|
var _queueEnabled: Bool?
|
|
var _queueAutoPlayNext: Bool?
|
|
var _queueAutoPlayCountdown: Int?
|
|
|
|
// Handoff settings
|
|
var _handoffEnabled: Bool?
|
|
|
|
// Remote Control settings
|
|
var _remoteControlCustomDeviceName: String?
|
|
var _remoteControlHideWhenBackgrounded: Bool?
|
|
|
|
// Link action settings
|
|
var _defaultLinkAction: DefaultLinkAction?
|
|
|
|
// Video tap actions (iOS/macOS only)
|
|
#if !os(tvOS)
|
|
var _thumbnailTapAction: VideoTapAction?
|
|
var _textAreaTapAction: VideoTapAction?
|
|
#endif
|
|
|
|
// Video tap action (tvOS only)
|
|
#if os(tvOS)
|
|
var _tvOSVideoTapAction: VideoTapAction?
|
|
#endif
|
|
|
|
// Player Controls settings (controlsButtonSize moved to preset)
|
|
|
|
// Appearance settings
|
|
var _listStyle: VideoListStyle?
|
|
var _appIcon: AppIcon?
|
|
|
|
// Video Swipe Actions
|
|
#if !os(tvOS)
|
|
var _videoSwipeActionOrder: [VideoSwipeAction]?
|
|
var _videoSwipeActionVisibility: [VideoSwipeAction: Bool]?
|
|
#endif
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Logs KVStore change reason for debugging iCloud account switches.
|
|
private func logKVStoreChangeReason(_ reason: Int) {
|
|
let reasonDescription: String
|
|
switch reason {
|
|
case NSUbiquitousKeyValueStoreServerChange:
|
|
reasonDescription = "ServerChange"
|
|
case NSUbiquitousKeyValueStoreInitialSyncChange:
|
|
reasonDescription = "InitialSyncChange"
|
|
case NSUbiquitousKeyValueStoreQuotaViolationChange:
|
|
reasonDescription = "QuotaViolationChange"
|
|
case NSUbiquitousKeyValueStoreAccountChange:
|
|
reasonDescription = "AccountChange (iCloud account switched!)"
|
|
LoggingService.shared.logCloudKit("SettingsManager: iCloud account changed - settings will sync with new account")
|
|
default:
|
|
reasonDescription = "Unknown(\(reason))"
|
|
}
|
|
|
|
LoggingService.shared.logCloudKit("SettingsManager KVStore change: \(reasonDescription)")
|
|
}
|
|
|
|
init() {
|
|
// Listen for external changes from iCloud
|
|
NotificationCenter.default.addObserver(
|
|
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
|
|
object: ubiquitousStore,
|
|
queue: .main
|
|
) { [weak self] notification in
|
|
let changeReason = notification.userInfo?[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int
|
|
let changedKeys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String]
|
|
|
|
// Only process iCloud changes if sync is enabled
|
|
Task { @MainActor [weak self] in
|
|
// Log change reason for debugging account switches
|
|
if let changeReason {
|
|
self?.logKVStoreChangeReason(changeReason)
|
|
}
|
|
|
|
if let changedKeys {
|
|
LoggingService.shared.logCloudKit("SettingsManager KVStore changed keys: \(changedKeys)")
|
|
}
|
|
|
|
guard let self, self.iCloudSyncEnabled else { return }
|
|
let keySet = changedKeys.map { Set($0) }
|
|
self.refreshFromiCloud(changedKeys: keySet)
|
|
self.updateLastSyncTime()
|
|
}
|
|
}
|
|
|
|
// One-shot migration: move legacy unprefixed values for keys that became
|
|
// platform-specific into their new iOS./macOS./tvOS. slots. Must run before
|
|
// the initial iCloud refresh so it can seed the prefixed iCloud slot from the
|
|
// current local value before any remote data is read.
|
|
migrateLayoutKeysToPlatformPrefixed()
|
|
|
|
// Initial sync from iCloud to local storage (async to avoid blocking app launch)
|
|
// This ensures local defaults have the latest iCloud values before any reads.
|
|
// While sync is pending, suppress iCloud writes from set() to prevent stale
|
|
// local values from overwriting newer iCloud data.
|
|
if localDefaults.bool(forKey: "iCloudSyncEnabled") {
|
|
isInitialSyncPending = true
|
|
LoggingService.shared.logCloudKit("SettingsManager.init: isInitialSyncPending = true, suppressing iCloud writes until refresh completes")
|
|
Task { @MainActor [weak self] in
|
|
defer {
|
|
self?.isInitialSyncPending = false
|
|
LoggingService.shared.logCloudKit("SettingsManager.init: isInitialSyncPending = false, iCloud writes re-enabled")
|
|
}
|
|
self?.ubiquitousStore.synchronize()
|
|
self?.refreshFromiCloud()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Storage Helpers
|
|
// Internal access for extension use.
|
|
|
|
func platformKey(_ key: SettingsKey) -> String {
|
|
let baseKey = key.rawValue
|
|
#if os(iOS)
|
|
return key.isPlatformSpecific ? "iOS.\(baseKey)" : baseKey
|
|
#elseif os(macOS)
|
|
return key.isPlatformSpecific ? "macOS.\(baseKey)" : baseKey
|
|
#elseif os(tvOS)
|
|
return key.isPlatformSpecific ? "tvOS.\(baseKey)" : baseKey
|
|
#endif
|
|
}
|
|
|
|
/// Returns the companion key used to store the last-modified timestamp for a protected setting.
|
|
func modifiedAtKey(for key: SettingsKey) -> String {
|
|
"\(platformKey(key))_modifiedAt"
|
|
}
|
|
|
|
func string(for key: SettingsKey) -> String? {
|
|
// Always read from local storage to avoid blocking on iCloud XPC calls
|
|
return localDefaults.string(forKey: platformKey(key))
|
|
}
|
|
|
|
func bool(for key: SettingsKey, default defaultValue: Bool = false) -> Bool {
|
|
// Always read from local storage to avoid blocking on iCloud XPC calls
|
|
let pKey = platformKey(key)
|
|
if localDefaults.object(forKey: pKey) != nil {
|
|
return localDefaults.bool(forKey: pKey)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func data(for key: SettingsKey) -> Data? {
|
|
// Always read from local storage to avoid blocking on iCloud XPC calls
|
|
return localDefaults.data(forKey: platformKey(key))
|
|
}
|
|
|
|
func integer(for key: SettingsKey, default defaultValue: Int) -> Int {
|
|
// Always read from local storage to avoid blocking on iCloud XPC calls
|
|
let pKey = platformKey(key)
|
|
if localDefaults.object(forKey: pKey) != nil {
|
|
return localDefaults.integer(forKey: pKey)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func double(for key: SettingsKey) -> Double {
|
|
// Always read from local storage to avoid blocking on iCloud XPC calls
|
|
return localDefaults.double(forKey: platformKey(key))
|
|
}
|
|
|
|
func set(_ value: String, for key: SettingsKey) {
|
|
let pKey = platformKey(key)
|
|
localDefaults.set(value, forKey: pKey)
|
|
|
|
// Only write to iCloud if sync is enabled, settings sync is enabled, key is not local-only,
|
|
// and initial sync has completed (to prevent stale values overwriting iCloud during startup)
|
|
if iCloudSyncEnabled && syncSettings && !key.isLocalOnly && !isInitialSyncPending {
|
|
ubiquitousStore.set(value, forKey: pKey)
|
|
} else if isInitialSyncPending && !key.isLocalOnly {
|
|
LoggingService.shared.logCloudKit("set(String): suppressed iCloud write for \(pKey) (initial sync pending)")
|
|
}
|
|
}
|
|
|
|
func set(_ value: Bool, for key: SettingsKey) {
|
|
let pKey = platformKey(key)
|
|
localDefaults.set(value, forKey: pKey)
|
|
|
|
if iCloudSyncEnabled && syncSettings && !key.isLocalOnly && !isInitialSyncPending {
|
|
ubiquitousStore.set(value, forKey: pKey)
|
|
} else if isInitialSyncPending && !key.isLocalOnly {
|
|
LoggingService.shared.logCloudKit("set(Bool): suppressed iCloud write for \(pKey) (initial sync pending)")
|
|
}
|
|
}
|
|
|
|
func set(_ value: Data, for key: SettingsKey) {
|
|
let pKey = platformKey(key)
|
|
localDefaults.set(value, forKey: pKey)
|
|
|
|
if iCloudSyncEnabled && syncSettings && !key.isLocalOnly && !isInitialSyncPending {
|
|
ubiquitousStore.set(value, forKey: pKey)
|
|
} else if isInitialSyncPending && !key.isLocalOnly {
|
|
LoggingService.shared.logCloudKit("set(Data): suppressed iCloud write for \(pKey) (initial sync pending)")
|
|
}
|
|
}
|
|
|
|
func set(_ value: Int, for key: SettingsKey) {
|
|
let pKey = platformKey(key)
|
|
localDefaults.set(value, forKey: pKey)
|
|
|
|
if iCloudSyncEnabled && syncSettings && !key.isLocalOnly && !isInitialSyncPending {
|
|
ubiquitousStore.set(value, forKey: pKey)
|
|
} else if isInitialSyncPending && !key.isLocalOnly {
|
|
LoggingService.shared.logCloudKit("set(Int): suppressed iCloud write for \(pKey) (initial sync pending)")
|
|
}
|
|
}
|
|
|
|
func set(_ value: Double, for key: SettingsKey) {
|
|
let pKey = platformKey(key)
|
|
localDefaults.set(value, forKey: pKey)
|
|
|
|
if iCloudSyncEnabled && syncSettings && !key.isLocalOnly && !isInitialSyncPending {
|
|
ubiquitousStore.set(value, forKey: pKey)
|
|
} else if isInitialSyncPending && !key.isLocalOnly {
|
|
LoggingService.shared.logCloudKit("set(Double): suppressed iCloud write for \(pKey) (initial sync pending)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Cache Management
|
|
|
|
/// Clears cached values to force re-read from storage
|
|
func clearCache() {
|
|
_theme = nil
|
|
_accentColor = nil
|
|
_customAccentColor = nil
|
|
_accentColorDark = nil
|
|
_customAccentColorDark = nil
|
|
_useSeparateDarkAccentColor = nil
|
|
_showWatchedCheckmark = nil
|
|
_preferredQuality = nil
|
|
_cellularQuality = nil
|
|
_backgroundPlaybackEnabled = nil
|
|
_preferredAudioLanguage = nil
|
|
_preferredSubtitlesLanguage = nil
|
|
_playerVolume = nil
|
|
_resumeAction = nil
|
|
_tvOSMenuButtonClosesVideo = nil
|
|
_allowSoftwareDecodedFormats = nil
|
|
_audioOnlyModeEnabled = nil
|
|
_sponsorBlockEnabled = nil
|
|
_sponsorBlockCategories = nil
|
|
_sponsorBlockAPIURL = nil
|
|
_returnYouTubeDislikeEnabled = nil
|
|
_deArrowEnabled = nil
|
|
_deArrowReplaceTitles = nil
|
|
_deArrowReplaceThumbnails = nil
|
|
_deArrowAPIURL = nil
|
|
_deArrowThumbnailAPIURL = nil
|
|
_resolveShortLinksEnabled = nil
|
|
_customUserAgent = nil
|
|
_randomizeUserAgentPerRequest = nil
|
|
_feedCacheValidityMinutes = nil
|
|
_keepPlayerPinnedEnabled = nil
|
|
#if os(iOS)
|
|
_hapticFeedbackEnabled = nil
|
|
_hapticFeedbackIntensity = nil
|
|
_inAppOrientationLock = nil
|
|
_rotateToMatchAspectRatio = nil
|
|
_preferPortraitBrowsing = nil
|
|
#endif
|
|
_iCloudSyncEnabled = nil
|
|
_lastSyncTime = nil
|
|
_syncInstances = nil
|
|
_syncSubscriptions = nil
|
|
_syncBookmarks = nil
|
|
_syncPlaybackHistory = nil
|
|
_syncPlaylists = nil
|
|
_syncSettings = nil
|
|
_syncMediaSources = nil
|
|
_syncSearchHistory = nil
|
|
_searchHistoryLimit = nil
|
|
#if os(macOS)
|
|
_macPlayerSeparateWindow = nil
|
|
_macPlayerFloating = nil
|
|
_macControlsBarOffsetX = nil
|
|
_macControlsBarOffsetY = nil
|
|
_playerSheetAutoResize = nil
|
|
#endif
|
|
// miniPlayerShowVideo and miniPlayerVideoTapAction moved to preset
|
|
#if os(iOS)
|
|
_miniPlayerMinimizeBehavior = nil
|
|
#endif
|
|
_homeShortcutOrder = nil
|
|
_homeShortcutVisibility = nil
|
|
_homeShortcutLayout = nil
|
|
_homeShortcutCardStyle = nil
|
|
_homeShortcutCardColor = nil
|
|
_homeShortcutColorfulPalette = nil
|
|
_homeShortcutCustomPaletteColors = nil
|
|
_homeSectionOrder = nil
|
|
_homeSectionVisibility = nil
|
|
_homeSectionItemsLimit = nil
|
|
_homeSectionLayout = nil
|
|
_topShelfSections = nil
|
|
_tabBarItemOrder = nil
|
|
_tabBarItemVisibility = nil
|
|
_sidebarMainItemOrder = nil
|
|
_sidebarMainItemVisibility = nil
|
|
_sidebarStartupTab = nil
|
|
_tabBarStartupTab = nil
|
|
_sidebarSourcesEnabled = nil
|
|
_sidebarSourceSort = nil
|
|
_sidebarSourcesLimitEnabled = nil
|
|
_sidebarMaxSources = nil
|
|
_sidebarChannelsEnabled = nil
|
|
_sidebarMaxChannels = nil
|
|
_sidebarChannelSort = nil
|
|
_sidebarChannelsLimitEnabled = nil
|
|
_sidebarPlaylistsEnabled = nil
|
|
_sidebarMaxPlaylists = nil
|
|
_sidebarPlaylistSort = nil
|
|
_sidebarPlaylistsLimitEnabled = nil
|
|
_showAdvancedStreamDetails = nil
|
|
_showPlayerAreaDebug = nil
|
|
_showTVDebugButton = nil
|
|
_verboseMPVLogging = nil
|
|
_verboseRemoteControlLogging = nil
|
|
_mpvBufferSeconds = nil
|
|
_mpvUseEDLStreams = nil
|
|
_zoomTransitionsEnabled = nil
|
|
_tvMatchDisplayFrameRate = nil
|
|
_tvMatchDisplayDynamicRange = nil
|
|
_tvAudioDelayMs = nil
|
|
_tvVideoSyncMode = nil
|
|
_floatingDetailsPanelSide = nil
|
|
_floatingDetailsPanelWidth = nil
|
|
_landscapeDetailsPanelVisible = nil
|
|
_landscapeDetailsPanelPinned = nil
|
|
_backgroundNotificationsEnabled = nil
|
|
_defaultNotificationsForNewChannels = nil
|
|
_lastBackgroundCheck = nil
|
|
_clipboardURLDetectionEnabled = nil
|
|
_incognitoModeEnabled = nil
|
|
_historyRetentionDays = nil
|
|
_saveWatchHistory = nil
|
|
_saveRecentSearches = nil
|
|
_saveRecentChannels = nil
|
|
_saveRecentPlaylists = nil
|
|
_queueEnabled = nil
|
|
_queueAutoPlayNext = nil
|
|
_queueAutoPlayCountdown = nil
|
|
_handoffEnabled = nil
|
|
_defaultLinkAction = nil
|
|
_remoteControlCustomDeviceName = nil
|
|
_remoteControlHideWhenBackgrounded = nil
|
|
#if !os(tvOS)
|
|
_thumbnailTapAction = nil
|
|
_textAreaTapAction = nil
|
|
#endif
|
|
#if os(tvOS)
|
|
_tvOSVideoTapAction = nil
|
|
#endif
|
|
_listStyle = nil
|
|
#if os(iOS)
|
|
_appIcon = nil
|
|
#endif
|
|
#if !os(tvOS)
|
|
_videoSwipeActionOrder = nil
|
|
_videoSwipeActionVisibility = nil
|
|
#endif
|
|
}
|
|
}
|