mirror of
https://github.com/yattee/yattee.git
synced 2026-07-19 22:02:10 +00:00
Add a Custom swatch to the appearance settings accent color grid, backed by a hex value stored in settings (synced via iCloud like the rest). iOS uses the native ColorPicker wheel; macOS uses a circular swatch that opens NSColorPanel, since the SwiftUI color well looks out of place among the circles. All accent consumers now read the resolved color through SettingsManager.resolvedAccentColor. The indigo preset is retired from the grid but kept in the enum, so users who selected it keep their color until they pick another one. Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
556 lines
20 KiB
Swift
556 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 _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?
|
|
|
|
// 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?
|
|
|
|
// Subscription account settings
|
|
var _subscriptionAccount: SubscriptionAccount?
|
|
|
|
// 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
|
|
_showWatchedCheckmark = nil
|
|
_preferredQuality = nil
|
|
_cellularQuality = nil
|
|
_backgroundPlaybackEnabled = nil
|
|
_preferredAudioLanguage = nil
|
|
_preferredSubtitlesLanguage = nil
|
|
_playerVolume = nil
|
|
_resumeAction = nil
|
|
_tvOSMenuButtonClosesVideo = nil
|
|
_allowSoftwareDecodedFormats = 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
|
|
_subscriptionAccount = 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
|
|
}
|
|
}
|