From 2be6f04e37864c5b9ca289b870a242b3ef9c44f6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 1 Feb 2024 23:54:16 +0100 Subject: [PATCH] Import export settings --- Model/Accounts/AccountsModel.swift | 8 +- Model/Accounts/Instance.swift | 4 + Model/Accounts/InstancesModel.swift | 12 +- Model/Favorites/FavoritesModel.swift | 9 + .../AdvancedSettingsGroupExporter.swift | 16 + .../BrowsingSettingsGroupExporter.swift | 54 +++ .../ControlsSettingsGroupExporter.swift | 40 ++ .../HistorySettingsGroupExporter.swift | 21 ++ .../LocationsSettingsGroupExporter.swift | 56 +++ .../OtherDataSettingsGroupExporter.swift | 27 ++ .../PlayerSettingsGroupExporter.swift | 44 +++ .../QualitySettingsGroupExporter.swift | 21 ++ .../Exporters/RecentlyOpenedExporter.swift | 16 + .../Exporters/SettingsGroupExporter.swift | 32 ++ .../SponsorBlockSettingsGroupExporter.swift | 11 + .../ImportExportSettingsModel.swift | 193 ++++++++++ .../ImportSettingsFileModel.swift | 135 +++++++ .../AdvancedSettingsGroupImporter.swift | 36 ++ .../BrowsingSettingsGroupImporter.swift | 144 ++++++++ .../ControlsSettingsGroupImporter.swift | 140 +++++++ .../HistorySettingsGroupImporter.swift | 54 +++ .../LocationsSettingsGroupImporter.swift | 84 +++++ .../OtherDataSettingsGroupImporter.swift | 70 ++++ .../PlayerSettingsGroupImporter.swift | 100 +++++ .../QualitySettingsGroupImporter.swift | 37 ++ .../Importers/RecentlyOpenedImporter.swift | 17 + .../SponsorBlockSettingsGroupImporter.swift | 16 + Model/NavigationModel.swift | 26 +- Model/SettingsModel.swift | 8 + Shared/Constants.swift | 20 + Shared/Defaults.swift | 345 ++++++++++-------- Shared/Home/HomeView.swift | 1 - Shared/Navigation/ContentView.swift | 1 + Shared/OpenURLHandler.swift | 5 + Shared/Settings/AccountForm.swift | 2 +- Shared/Settings/ExportSettings.swift | 165 +++++++++ .../Import/ImportSettingsAccountRow.swift | 187 ++++++++++ ...portSettingsFileImporterViewModifier.swift | 30 ++ .../Import/ImportSettingsSheetView.swift | 260 +++++++++++++ .../Import/ImportSettingsSheetViewModel.swift | 77 ++++ .../ImportSettingsSheetViewModifier.swift | 25 ++ Shared/Settings/SettingsView.swift | 57 ++- Shared/YatteeApp.swift | 8 + Yattee.xcodeproj/project.pbxproj | 262 +++++++++++++ iOS/Info.plist | 36 ++ macOS/Info.plist | 58 +++ 46 files changed, 2801 insertions(+), 169 deletions(-) create mode 100644 Model/Import Export Settings/Exporters/AdvancedSettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/BrowsingSettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/ControlsSettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/HistorySettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/LocationsSettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/OtherDataSettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/PlayerSettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/QualitySettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/RecentlyOpenedExporter.swift create mode 100644 Model/Import Export Settings/Exporters/SettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/Exporters/SponsorBlockSettingsGroupExporter.swift create mode 100644 Model/Import Export Settings/ImportExportSettingsModel.swift create mode 100644 Model/Import Export Settings/ImportSettingsFileModel.swift create mode 100644 Model/Import Export Settings/Importers/AdvancedSettingsGroupImporter.swift create mode 100644 Model/Import Export Settings/Importers/BrowsingSettingsGroupImporter.swift create mode 100644 Model/Import Export Settings/Importers/ControlsSettingsGroupImporter.swift create mode 100644 Model/Import Export Settings/Importers/HistorySettingsGroupImporter.swift create mode 100644 Model/Import Export Settings/Importers/LocationsSettingsGroupImporter.swift create mode 100644 Model/Import Export Settings/Importers/OtherDataSettingsGroupImporter.swift create mode 100644 Model/Import Export Settings/Importers/PlayerSettingsGroupImporter.swift create mode 100644 Model/Import Export Settings/Importers/QualitySettingsGroupImporter.swift create mode 100644 Model/Import Export Settings/Importers/RecentlyOpenedImporter.swift create mode 100644 Model/Import Export Settings/Importers/SponsorBlockSettingsGroupImporter.swift create mode 100644 Shared/Settings/ExportSettings.swift create mode 100644 Shared/Settings/Import/ImportSettingsAccountRow.swift create mode 100644 Shared/Settings/Import/ImportSettingsFileImporterViewModifier.swift create mode 100644 Shared/Settings/Import/ImportSettingsSheetView.swift create mode 100644 Shared/Settings/Import/ImportSettingsSheetViewModel.swift create mode 100644 Shared/Settings/Import/ImportSettingsSheetViewModifier.swift diff --git a/Model/Accounts/AccountsModel.swift b/Model/Accounts/AccountsModel.swift index f16c2b50..45f05f70 100644 --- a/Model/Accounts/AccountsModel.swift +++ b/Model/Accounts/AccountsModel.swift @@ -64,6 +64,10 @@ final class AccountsModel: ObservableObject { ) } + func find(_ id: Account.ID) -> Account? { + all.first { $0.id == id } + } + func configureAccount() { if let account = lastUsed ?? InstancesModel.shared.lastUsed?.anonymousAccount ?? @@ -108,8 +112,8 @@ final class AccountsModel: ObservableObject { Defaults[.accounts].first { $0.id == id } } - static func add(instance: Instance, name: String, username: String, password: String) -> Account { - let account = Account(instanceID: instance.id, name: name, urlString: instance.apiURLString) + static func add(instance: Instance, id: String? = UUID().uuidString, name: String, username: String, password: String) -> Account { + let account = Account(id: id, instanceID: instance.id, name: name, urlString: instance.apiURLString) Defaults[.accounts].append(account) setCredentials(account, username: username, password: password) diff --git a/Model/Accounts/Instance.swift b/Model/Accounts/Instance.swift index 9a191ece..4aef5792 100644 --- a/Model/Accounts/Instance.swift +++ b/Model/Accounts/Instance.swift @@ -68,4 +68,8 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { func hash(into hasher: inout Hasher) { hasher.combine(apiURL) } + + var accounts: [Account] { + AccountsModel.shared.all.filter { $0.instanceID == id } + } } diff --git a/Model/Accounts/InstancesModel.swift b/Model/Accounts/InstancesModel.swift index 90eb7117..af9e1b01 100644 --- a/Model/Accounts/InstancesModel.swift +++ b/Model/Accounts/InstancesModel.swift @@ -42,15 +42,23 @@ final class InstancesModel: ObservableObject { Defaults[.accounts].filter { $0.instanceID == id } } - func add(app: VideosApp, name: String, url: String) -> Instance { + func add(id: String? = UUID().uuidString, app: VideosApp, name: String, url: String) -> Instance { let instance = Instance( - app: app, id: UUID().uuidString, name: name, apiURLString: standardizedURL(url) + app: app, id: id, name: name, apiURLString: standardizedURL(url) ) Defaults[.instances].append(instance) return instance } + func insert(id: String? = UUID().uuidString, app: VideosApp, name: String, url: String) -> Instance { + if let instance = Defaults[.instances].first(where: { $0.apiURL.absoluteString == standardizedURL(url) }) { + return instance + } + + return add(id: id, app: app, name: name, url: url) + } + func setFrontendURL(_ instance: Instance, _ url: String) { if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) { var instance = Defaults[.instances][index] diff --git a/Model/Favorites/FavoritesModel.swift b/Model/Favorites/FavoritesModel.swift index ee918976..8798cb34 100644 --- a/Model/Favorites/FavoritesModel.swift +++ b/Model/Favorites/FavoritesModel.swift @@ -25,6 +25,7 @@ struct FavoritesModel { } func add(_ item: FavoriteItem) { + if contains(item) { return } all.append(item) } @@ -122,4 +123,12 @@ struct FavoritesModel { func widgetSettings(_ item: FavoriteItem) -> WidgetSettings { widgetsSettings.first { $0.id == item.widgetSettingsKey } ?? WidgetSettings(id: item.widgetSettingsKey) } + + func updateWidgetSettings(_ settings: WidgetSettings) { + if let index = widgetsSettings.firstIndex(where: { $0.id == settings.id }) { + widgetsSettings[index] = settings + } else { + widgetsSettings.append(settings) + } + } } diff --git a/Model/Import Export Settings/Exporters/AdvancedSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/AdvancedSettingsGroupExporter.swift new file mode 100644 index 00000000..17e7fa7d --- /dev/null +++ b/Model/Import Export Settings/Exporters/AdvancedSettingsGroupExporter.swift @@ -0,0 +1,16 @@ +import Defaults +import SwiftyJSON + +final class AdvancedSettingsGroupExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu], + "showMPVPlaybackStats": Defaults[.showMPVPlaybackStats], + "mpvEnableLogging": Defaults[.mpvEnableLogging], + "mpvCacheSecs": Defaults[.mpvCacheSecs], + "mpvCachePauseWait": Defaults[.mpvCachePauseWait], + "showCacheStatus": Defaults[.showCacheStatus], + "feedCacheSize": Defaults[.feedCacheSize] + ] + } +} diff --git a/Model/Import Export Settings/Exporters/BrowsingSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/BrowsingSettingsGroupExporter.swift new file mode 100644 index 00000000..5b2ba2b5 --- /dev/null +++ b/Model/Import Export Settings/Exporters/BrowsingSettingsGroupExporter.swift @@ -0,0 +1,54 @@ +import Defaults +import SwiftyJSON + +final class BrowsingSettingsGroupExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "showHome": Defaults[.showHome], + "showOpenActionsInHome": Defaults[.showOpenActionsInHome], + "showQueueInHome": Defaults[.showQueueInHome], + "showFavoritesInHome": Defaults[.showFavoritesInHome], + "favorites": Defaults[.favorites].compactMap { jsonFromString(FavoriteItem.bridge.serialize($0)) }, + "widgetsSettings": Defaults[.widgetsSettings].compactMap { widgetSettingsJSON($0) }, + "startupSection": Defaults[.startupSection].rawValue, + "visibleSections": Defaults[.visibleSections].compactMap { $0.rawValue }, + "showOpenActionsToolbarItem": Defaults[.showOpenActionsToolbarItem], + "accountPickerDisplaysAnonymousAccounts": Defaults[.accountPickerDisplaysAnonymousAccounts], + "showUnwatchedFeedBadges": Defaults[.showUnwatchedFeedBadges], + "expandChannelDescription": Defaults[.expandChannelDescription], + "keepChannelsWithUnwatchedFeedOnTop": Defaults[.keepChannelsWithUnwatchedFeedOnTop], + "showChannelAvatarInChannelsLists": Defaults[.showChannelAvatarInChannelsLists], + "showChannelAvatarInVideosListing": Defaults[.showChannelAvatarInVideosListing], + "playerButtonSingleTapGesture": Defaults[.playerButtonSingleTapGesture].rawValue, + "playerButtonDoubleTapGesture": Defaults[.playerButtonDoubleTapGesture].rawValue, + "playerButtonShowsControlButtonsWhenMinimized": Defaults[.playerButtonShowsControlButtonsWhenMinimized], + "playerButtonIsExpanded": Defaults[.playerButtonIsExpanded], + "playerBarMaxWidth": Defaults[.playerBarMaxWidth], + "channelOnThumbnail": Defaults[.channelOnThumbnail], + "timeOnThumbnail": Defaults[.timeOnThumbnail], + "roundedThumbnails": Defaults[.roundedThumbnails], + "thumbnailsQuality": Defaults[.thumbnailsQuality].rawValue + ] + } + + override var platformJSON: JSON { + var export = JSON() + + #if os(iOS) + export["showDocuments"].bool = Defaults[.showDocuments] + export["lockPortraitWhenBrowsing"].bool = Defaults[.lockPortraitWhenBrowsing] + #endif + + #if !os(tvOS) + export["accountPickerDisplaysUsername"].bool = Defaults[.accountPickerDisplaysUsername] + #endif + + return export + } + + private func widgetSettingsJSON(_ settings: WidgetSettings) -> JSON { + var json = JSON() + json.dictionaryObject = WidgetSettingsBridge().serialize(settings) + return json + } +} diff --git a/Model/Import Export Settings/Exporters/ControlsSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/ControlsSettingsGroupExporter.swift new file mode 100644 index 00000000..9654b6e8 --- /dev/null +++ b/Model/Import Export Settings/Exporters/ControlsSettingsGroupExporter.swift @@ -0,0 +1,40 @@ +import Defaults +import SwiftyJSON + +final class ConstrolsSettingsGroupExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "avPlayerUsesSystemControls": Defaults[.avPlayerUsesSystemControls], + "horizontalPlayerGestureEnabled": Defaults[.horizontalPlayerGestureEnabled], + "seekGestureSensitivity": Defaults[.seekGestureSensitivity], + "seekGestureSpeed": Defaults[.seekGestureSpeed], + "playerControlsLayout": Defaults[.playerControlsLayout].rawValue, + "fullScreenPlayerControlsLayout": Defaults[.fullScreenPlayerControlsLayout].rawValue, + "systemControlsCommands": Defaults[.systemControlsCommands].rawValue, + "buttonBackwardSeekDuration": Defaults[.buttonBackwardSeekDuration], + "buttonForwardSeekDuration": Defaults[.buttonForwardSeekDuration], + "gestureBackwardSeekDuration": Defaults[.gestureBackwardSeekDuration], + "gestureForwardSeekDuration": Defaults[.gestureForwardSeekDuration], + "systemControlsSeekDuration": Defaults[.systemControlsSeekDuration], + "playerControlsSettingsEnabled": Defaults[.playerControlsSettingsEnabled], + "playerControlsCloseEnabled": Defaults[.playerControlsCloseEnabled], + "playerControlsRestartEnabled": Defaults[.playerControlsRestartEnabled], + "playerControlsAdvanceToNextEnabled": Defaults[.playerControlsAdvanceToNextEnabled], + "playerControlsPlaybackModeEnabled": Defaults[.playerControlsPlaybackModeEnabled], + "playerControlsMusicModeEnabled": Defaults[.playerControlsMusicModeEnabled], + "playerActionsButtonLabelStyle": Defaults[.playerActionsButtonLabelStyle].rawValue, + "actionButtonShareEnabled": Defaults[.actionButtonShareEnabled], + "actionButtonAddToPlaylistEnabled": Defaults[.actionButtonAddToPlaylistEnabled], + "actionButtonSubscribeEnabled": Defaults[.actionButtonSubscribeEnabled], + "actionButtonSettingsEnabled": Defaults[.actionButtonSettingsEnabled], + "actionButtonHideEnabled": Defaults[.actionButtonHideEnabled], + "actionButtonCloseEnabled": Defaults[.actionButtonCloseEnabled], + "actionButtonFullScreenEnabled": Defaults[.actionButtonFullScreenEnabled], + "actionButtonPipEnabled": Defaults[.actionButtonPipEnabled], + "actionButtonLockOrientationEnabled": Defaults[.actionButtonLockOrientationEnabled], + "actionButtonRestartEnabled": Defaults[.actionButtonRestartEnabled], + "actionButtonAdvanceToNextItemEnabled": Defaults[.actionButtonAdvanceToNextItemEnabled], + "actionButtonMusicModeEnabled": Defaults[.actionButtonMusicModeEnabled] + ] + } +} diff --git a/Model/Import Export Settings/Exporters/HistorySettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/HistorySettingsGroupExporter.swift new file mode 100644 index 00000000..08add25a --- /dev/null +++ b/Model/Import Export Settings/Exporters/HistorySettingsGroupExporter.swift @@ -0,0 +1,21 @@ +import Defaults +import SwiftyJSON + +final class HistorySettingsGroupExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "saveRecents": Defaults[.saveRecents], + "saveHistory": Defaults[.saveHistory], + "showWatchingProgress": Defaults[.showWatchingProgress], + "saveLastPlayed": Defaults[.saveLastPlayed], + + "watchedVideoPlayNowBehavior": Defaults[.watchedVideoPlayNowBehavior].rawValue, + "watchedThreshold": Defaults[.watchedThreshold], + "resetWatchedStatusOnPlaying": Defaults[.resetWatchedStatusOnPlaying], + + "watchedVideoStyle": Defaults[.watchedVideoStyle].rawValue, + "watchedVideoBadgeColor": Defaults[.watchedVideoBadgeColor].rawValue, + "showToggleWatchedStatusButton": Defaults[.showToggleWatchedStatusButton] + ] + } +} diff --git a/Model/Import Export Settings/Exporters/LocationsSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/LocationsSettingsGroupExporter.swift new file mode 100644 index 00000000..97355f01 --- /dev/null +++ b/Model/Import Export Settings/Exporters/LocationsSettingsGroupExporter.swift @@ -0,0 +1,56 @@ +import Defaults +import SwiftyJSON + +final class LocationsSettingsGroupExporter: SettingsGroupExporter { + var includePublicInstances = true + var includeInstances = true + var includeAccounts = true + var includeAccountsUnencryptedPasswords = false + + init(includePublicInstances: Bool = true, includeInstances: Bool = true, includeAccounts: Bool = true, includeAccountsUnencryptedPasswords: Bool = false) { + self.includePublicInstances = includePublicInstances + self.includeInstances = includeInstances + self.includeAccounts = includeAccounts + self.includeAccountsUnencryptedPasswords = includeAccountsUnencryptedPasswords + } + + override var globalJSON: JSON { + var json = JSON() + + if includePublicInstances { + json["instancesManifest"].string = Defaults[.instancesManifest] + json["countryOfPublicInstances"].string = Defaults[.countryOfPublicInstances] ?? "" + } + + if includeInstances { + json["instances"].arrayObject = Defaults[.instances].compactMap { instanceJSON($0) } + } + + if includeAccounts { + json["accounts"].arrayObject = Defaults[.accounts].compactMap { account in + var account = account + let (username, password) = AccountsModel.getCredentials(account) + account.username = username ?? "" + if includeAccountsUnencryptedPasswords { + account.password = password ?? "" + } + + return accountJSON(account).dictionaryObject + } + } + + return json + } + + private func instanceJSON(_ instance: Instance) -> JSON { + var json = JSON() + json.dictionaryObject = InstancesBridge().serialize(instance) + return json + } + + private func accountJSON(_ account: Account) -> JSON { + var json = JSON() + json.dictionaryObject = AccountsBridge().serialize(account) + return json + } +} diff --git a/Model/Import Export Settings/Exporters/OtherDataSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/OtherDataSettingsGroupExporter.swift new file mode 100644 index 00000000..2e19f9f8 --- /dev/null +++ b/Model/Import Export Settings/Exporters/OtherDataSettingsGroupExporter.swift @@ -0,0 +1,27 @@ +import Defaults +import SwiftyJSON + +final class OtherDataSettingsGroupExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "lastAccountID": Defaults[.lastAccountID] ?? "", + "lastInstanceID": Defaults[.lastInstanceID] ?? "", + + "playerRate": Defaults[.playerRate], + + "trendingCategory": Defaults[.trendingCategory].rawValue, + "trendingCountry": Defaults[.trendingCountry].rawValue, + + "subscriptionsViewPage": Defaults[.subscriptionsViewPage].rawValue, + "subscriptionsListingStyle": Defaults[.subscriptionsListingStyle].rawValue, + "popularListingStyle": Defaults[.popularListingStyle].rawValue, + "trendingListingStyle": Defaults[.trendingListingStyle].rawValue, + "playlistListingStyle": Defaults[.playlistListingStyle].rawValue, + "channelPlaylistListingStyle": Defaults[.channelPlaylistListingStyle].rawValue, + "searchListingStyle": Defaults[.searchListingStyle].rawValue, + + "hideShorts": Defaults[.hideShorts], + "hideWatched": Defaults[.hideWatched] + ] + } +} diff --git a/Model/Import Export Settings/Exporters/PlayerSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/PlayerSettingsGroupExporter.swift new file mode 100644 index 00000000..a70e93da --- /dev/null +++ b/Model/Import Export Settings/Exporters/PlayerSettingsGroupExporter.swift @@ -0,0 +1,44 @@ +import Defaults +import SwiftyJSON + +final class PlayerSettingsGroupExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "playerInstanceID": Defaults[.playerInstanceID] ?? "", + "pauseOnHidingPlayer": Defaults[.pauseOnHidingPlayer], + "closeVideoOnEOF": Defaults[.closeVideoOnEOF], + "expandVideoDescription": Defaults[.expandVideoDescription], + "collapsedLinesDescription": Defaults[.collapsedLinesDescription], + "showChapters": Defaults[.showChapters], + "expandChapters": Defaults[.expandChapters], + "showRelated": Defaults[.showRelated], + "showInspector": Defaults[.showInspector].rawValue, + "playerSidebar": Defaults[.playerSidebar].rawValue, + "showKeywords": Defaults[.showKeywords], + "enableReturnYouTubeDislike": Defaults[.enableReturnYouTubeDislike], + "closePiPOnNavigation": Defaults[.closePiPOnNavigation], + "closePiPOnOpeningPlayer": Defaults[.closePiPOnOpeningPlayer], + "closePlayerOnOpeningPiP": Defaults[.closePlayerOnOpeningPiP] + ] + } + + override var platformJSON: JSON { + var export = JSON() + + #if !os(macOS) + export["pauseOnEnteringBackground"].bool = Defaults[.pauseOnEnteringBackground] + #endif + + #if !os(tvOS) + export["showScrollToTopInComments"].bool = Defaults[.showScrollToTopInComments] + #endif + + #if os(iOS) + export["honorSystemOrientationLock"].bool = Defaults[.honorSystemOrientationLock] + export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape] + export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue + #endif + + return export + } +} diff --git a/Model/Import Export Settings/Exporters/QualitySettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/QualitySettingsGroupExporter.swift new file mode 100644 index 00000000..1ebe6119 --- /dev/null +++ b/Model/Import Export Settings/Exporters/QualitySettingsGroupExporter.swift @@ -0,0 +1,21 @@ +import Defaults +import SwiftyJSON + +final class QualitySettingsGroupExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "batteryCellularProfile": Defaults[.batteryCellularProfile], + "batteryNonCellularProfile": Defaults[.batteryNonCellularProfile], + "chargingCellularProfile": Defaults[.chargingCellularProfile], + "chargingNonCellularProfile": Defaults[.chargingNonCellularProfile], + "forceAVPlayerForLiveStreams": Defaults[.forceAVPlayerForLiveStreams], + "qualityProfiles": Defaults[.qualityProfiles].compactMap { qualityProfileJSON($0) } + ] + } + + func qualityProfileJSON(_ profile: QualityProfile) -> JSON { + var json = JSON() + json.dictionaryObject = QualityProfileBridge().serialize(profile) + return json + } +} diff --git a/Model/Import Export Settings/Exporters/RecentlyOpenedExporter.swift b/Model/Import Export Settings/Exporters/RecentlyOpenedExporter.swift new file mode 100644 index 00000000..dba25ee3 --- /dev/null +++ b/Model/Import Export Settings/Exporters/RecentlyOpenedExporter.swift @@ -0,0 +1,16 @@ +import Defaults +import SwiftyJSON + +final class RecentlyOpenedExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "recentlyOpened": Defaults[.recentlyOpened].compactMap { recentItemJSON($0) } + ] + } + + private func recentItemJSON(_ recentItem: RecentItem) -> JSON { + var json = JSON() + json.dictionaryObject = RecentItemBridge().serialize(recentItem) + return json + } +} diff --git a/Model/Import Export Settings/Exporters/SettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/SettingsGroupExporter.swift new file mode 100644 index 00000000..7495e3f3 --- /dev/null +++ b/Model/Import Export Settings/Exporters/SettingsGroupExporter.swift @@ -0,0 +1,32 @@ +import Foundation +import SwiftyJSON + +class SettingsGroupExporter { // swiftlint:disable:this final_class + var globalJSON: JSON { + [] + } + + var platformJSON: JSON { + [] + } + + var exportJSON: JSON { + var json = globalJSON + + if !platformJSON.isEmpty { + try? json.merge(with: platformJSON) + } + + return json + } + + func jsonFromString(_ string: String?) -> JSON? { + if let data = string?.data(using: .utf8, allowLossyConversion: false), + let json = try? JSON(data: data) + { + return json + } + + return nil + } +} diff --git a/Model/Import Export Settings/Exporters/SponsorBlockSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/SponsorBlockSettingsGroupExporter.swift new file mode 100644 index 00000000..e2f9d3bd --- /dev/null +++ b/Model/Import Export Settings/Exporters/SponsorBlockSettingsGroupExporter.swift @@ -0,0 +1,11 @@ +import Defaults +import SwiftyJSON + +final class SponsorBlockSettingsGroupExporter: SettingsGroupExporter { + override var globalJSON: JSON { + [ + "sponsorBlockInstance": Defaults[.sponsorBlockInstance], + "sponsorBlockCategories": Array(Defaults[.sponsorBlockCategories]) + ] + } +} diff --git a/Model/Import Export Settings/ImportExportSettingsModel.swift b/Model/Import Export Settings/ImportExportSettingsModel.swift new file mode 100644 index 00000000..33b6dd60 --- /dev/null +++ b/Model/Import Export Settings/ImportExportSettingsModel.swift @@ -0,0 +1,193 @@ +import Defaults +import Foundation +import SwiftUI +import SwiftyJSON + +final class ImportExportSettingsModel: ObservableObject { + static let shared = ImportExportSettingsModel() + + static var exportFile: URL { + YatteeApp.settingsExportDirectory + .appendingPathComponent("Yattee Settings from \(Constants.deviceName).\(settingsExtension)") + } + + static var settingsExtension: String { + "yatteesettings" + } + + enum ExportGroup: String, Identifiable, CaseIterable { + case browsingSettings + case playerSettings + case controlsSettings + case qualitySettings + case historySettings + case sponsorBlockSettings + case advancedSettings + + case locationsSettings + case instances + case accounts + case accountsUnencryptedPasswords + + case recentlyOpened + case otherData + + static var settingsGroups: [Self] { + [.browsingSettings, .playerSettings, .controlsSettings, .qualitySettings, .historySettings, .sponsorBlockSettings, .advancedSettings] + } + + static var locationsGroups: [Self] { + [.locationsSettings, .instances, .accounts, .accountsUnencryptedPasswords] + } + + static var otherGroups: [Self] { + [.recentlyOpened, .otherData] + } + + var id: RawValue { + rawValue + } + + var label: String { + switch self { + case .browsingSettings: + "Browsing" + case .playerSettings: + "Player" + case .controlsSettings: + "Controls" + case .qualitySettings: + "Quality" + case .historySettings: + "History" + case .sponsorBlockSettings: + "SponsorBlock" + case .locationsSettings: + "Public Locations" + case .instances: + "Custom Locations" + case .accounts: + "Accounts" + case .accountsUnencryptedPasswords: + "Accounts passwords (unencrypted)" + case .advancedSettings: + "Advanced" + case .recentlyOpened: + "Recents" + case .otherData: + "Other data" + } + } + } + + @Published var selectedExportGroups = Set() + static var defaultExportGroups = Set([ + .browsingSettings, + .playerSettings, + .controlsSettings, + .qualitySettings, + .historySettings, + .sponsorBlockSettings, + .locationsSettings, + .instances, + .accounts, + .advancedSettings + ]) + + @Published var isExportInProgress = false + + private var navigation = NavigationModel.shared + private var settings = SettingsModel.shared + + func toggleExportGroupSelection(_ group: ExportGroup) { + if isGroupSelected(group) { + selectedExportGroups.remove(group) + } else { + selectedExportGroups.insert(group) + } + + removeNotEnabledSelectedGroups() + } + + func reset() { + isExportInProgress = false + selectedExportGroups = Self.defaultExportGroups + } + + func reset(_ model: ImportSettingsFileModel? = nil) { + reset() + + guard let model else { return } + + selectedExportGroups = selectedExportGroups.filter { model.isGroupIncludedInFile($0) } + } + + func exportAction() { + DispatchQueue.global(qos: .background).async { [weak self] in + var writingOptions: JSONSerialization.WritingOptions = [] + #if DEBUG + writingOptions.insert(.prettyPrinted) + writingOptions.insert(.sortedKeys) + #endif + try? self?.jsonForExport?.rawString(options: writingOptions)?.write(to: Self.exportFile, atomically: true, encoding: String.Encoding.utf8) + #if os(macOS) + DispatchQueue.main.async { [weak self] in + self?.isExportInProgress = false + } + NSWorkspace.shared.selectFile(Self.exportFile.path, inFileViewerRootedAtPath: YatteeApp.settingsExportDirectory.path) + #endif + } + } + + private var jsonForExport: JSON? { + [ + "metadata": metadataJSON, + "browsingSettings": selectedExportGroups.contains(.browsingSettings) ? BrowsingSettingsGroupExporter().exportJSON : JSON(), + "playerSettings": selectedExportGroups.contains(.playerSettings) ? PlayerSettingsGroupExporter().exportJSON : JSON(), + "controlsSettings": selectedExportGroups.contains(.controlsSettings) ? ConstrolsSettingsGroupExporter().exportJSON : JSON(), + "qualitySettings": selectedExportGroups.contains(.qualitySettings) ? QualitySettingsGroupExporter().exportJSON : JSON(), + "historySettings": selectedExportGroups.contains(.historySettings) ? HistorySettingsGroupExporter().exportJSON : JSON(), + "sponsorBlockSettings": selectedExportGroups.contains(.sponsorBlockSettings) ? SponsorBlockSettingsGroupExporter().exportJSON : JSON(), + "locationsSettings": LocationsSettingsGroupExporter( + includePublicInstances: isGroupSelected(.locationsSettings), + includeInstances: isGroupSelected(.instances), + includeAccounts: isGroupSelected(.accounts), + includeAccountsUnencryptedPasswords: isGroupSelected(.accountsUnencryptedPasswords) + ).exportJSON, + "advancedSettings": selectedExportGroups.contains(.advancedSettings) ? AdvancedSettingsGroupExporter().exportJSON : JSON(), + "recentlyOpened": selectedExportGroups.contains(.recentlyOpened) ? RecentlyOpenedExporter().exportJSON : JSON(), + "otherData": selectedExportGroups.contains(.otherData) ? OtherDataSettingsGroupExporter().exportJSON : JSON() + ] + } + + private var metadataJSON: JSON { + [ + "build": YatteeApp.build, + "timestamp": "\(Date().timeIntervalSince1970)", + "platform": Constants.platform + ] + } + + func isGroupSelected(_ group: ExportGroup) -> Bool { + selectedExportGroups.contains(group) + } + + func isGroupEnabled(_ group: ExportGroup) -> Bool { + switch group { + case .accounts: + return selectedExportGroups.contains(.instances) + case .accountsUnencryptedPasswords: + return selectedExportGroups.contains(.instances) && selectedExportGroups.contains(.accounts) + default: + return true + } + } + + func removeNotEnabledSelectedGroups() { + selectedExportGroups = selectedExportGroups.filter { isGroupEnabled($0) } + } + + var isExportAvailable: Bool { + !selectedExportGroups.isEmpty && !isExportInProgress + } +} diff --git a/Model/Import Export Settings/ImportSettingsFileModel.swift b/Model/Import Export Settings/ImportSettingsFileModel.swift new file mode 100644 index 00000000..157e9975 --- /dev/null +++ b/Model/Import Export Settings/ImportSettingsFileModel.swift @@ -0,0 +1,135 @@ +import Defaults +import Foundation +import SwiftyJSON + +struct ImportSettingsFileModel { + let url: URL + + var filename: String { + String(url.lastPathComponent.dropLast(ImportExportSettingsModel.settingsExtension.count + 1)) + } + + var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? { + if let locationsSettings = json?.dictionaryValue["locationsSettings"] { + return LocationsSettingsGroupImporter( + json: locationsSettings, + includePublicLocations: importExportModel.isGroupEnabled(.locationsSettings), + includedInstancesIDs: sheetViewModel.selectedInstances, + includedAccountsIDs: sheetViewModel.selectedAccounts, + includedAccountsPasswords: sheetViewModel.importableAccountsPasswords + ) + } + return nil + } + + var importExportModel = ImportExportSettingsModel.shared + var sheetViewModel = ImportSettingsSheetViewModel.shared + + func isGroupIncludedInFile(_ group: ImportExportSettingsModel.ExportGroup) -> Bool { + switch group { + case .locationsSettings: + return isPublicInstancesSettingsGroupInFile || instancesOrAccountsInFile + default: + return !groupJSON(group).isEmpty + } + } + + var isPublicInstancesSettingsGroupInFile: Bool { + guard let dict = groupJSON(.locationsSettings).dictionary else { return false } + + return dict.keys.contains("instancesManifest") || dict.keys.contains("countryOfPublicInstances") + } + + var instancesOrAccountsInFile: Bool { + guard let dict = groupJSON(.locationsSettings).dictionary else { return false } + + return (dict.keys.contains("instances") && !(dict["instances"]?.arrayValue.isEmpty ?? true)) || + (dict.keys.contains("accounts") && !(dict["accounts"]?.arrayValue.isEmpty ?? true)) + } + + func groupJSON(_ group: ImportExportSettingsModel.ExportGroup) -> JSON { + json?.dictionaryValue[group.rawValue] ?? .init() + } + + func performImport() { + if importExportModel.isGroupSelected(.browsingSettings), isGroupIncludedInFile(.browsingSettings) { + BrowsingSettingsGroupImporter(json: groupJSON(.browsingSettings)).performImport() + } + + if importExportModel.isGroupSelected(.playerSettings), isGroupIncludedInFile(.playerSettings) { + PlayerSettingsGroupImporter(json: groupJSON(.playerSettings)).performImport() + } + + if importExportModel.isGroupSelected(.controlsSettings), isGroupIncludedInFile(.controlsSettings) { + ConstrolsSettingsGroupImporter(json: groupJSON(.controlsSettings)).performImport() + } + + if importExportModel.isGroupSelected(.qualitySettings), isGroupIncludedInFile(.qualitySettings) { + QualitySettingsGroupImporter(json: groupJSON(.qualitySettings)).performImport() + } + + if importExportModel.isGroupSelected(.historySettings), isGroupIncludedInFile(.historySettings) { + HistorySettingsGroupImporter(json: groupJSON(.historySettings)).performImport() + } + + if importExportModel.isGroupSelected(.sponsorBlockSettings), isGroupIncludedInFile(.sponsorBlockSettings) { + SponsorBlockSettingsGroupImporter(json: groupJSON(.sponsorBlockSettings)).performImport() + } + + locationsSettingsGroupImporter?.performImport() + + if importExportModel.isGroupSelected(.advancedSettings), isGroupIncludedInFile(.advancedSettings) { + AdvancedSettingsGroupImporter(json: groupJSON(.advancedSettings)).performImport() + } + + if importExportModel.isGroupSelected(.recentlyOpened), isGroupIncludedInFile(.recentlyOpened) { + RecentlyOpenedImporter(json: groupJSON(.recentlyOpened)).performImport() + } + + if importExportModel.isGroupSelected(.otherData), isGroupIncludedInFile(.otherData) { + OtherDataSettingsGroupImporter(json: groupJSON(.otherData)).performImport() + } + } + + var json: JSON? { + if let fileContents = try? Data(contentsOf: url), + let json = try? JSON(data: fileContents) + { + return json + } + return nil + } + + var metadataBuild: String? { + if let build = json?.dictionaryValue["metadata"]?.dictionaryValue["build"]?.string { + return build + } + + return nil + } + + var metadataPlatform: String? { + if let platform = json?.dictionaryValue["metadata"]?.dictionaryValue["platform"]?.string { + return platform + } + + return nil + } + + var metadataDate: String? { + if let timestamp = json?.dictionaryValue["metadata"]?.dictionaryValue["timestamp"]?.doubleValue { + let date = Date(timeIntervalSince1970: timestamp) + return dateFormatter.string(from: date) + } + + return nil + } + + var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .medium + + return formatter + } +} diff --git a/Model/Import Export Settings/Importers/AdvancedSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/AdvancedSettingsGroupImporter.swift new file mode 100644 index 00000000..0ca6aa98 --- /dev/null +++ b/Model/Import Export Settings/Importers/AdvancedSettingsGroupImporter.swift @@ -0,0 +1,36 @@ +import Defaults +import SwiftyJSON + +struct AdvancedSettingsGroupImporter { + var json: JSON + + func performImport() { + if let showPlayNowInBackendContextMenu = json["showPlayNowInBackendContextMenu"].bool { + Defaults[.showPlayNowInBackendContextMenu] = showPlayNowInBackendContextMenu + } + + if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool { + Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats + } + + if let mpvEnableLogging = json["mpvEnableLogging"].bool { + Defaults[.mpvEnableLogging] = mpvEnableLogging + } + + if let mpvCacheSecs = json["mpvCacheSecs"].string { + Defaults[.mpvCacheSecs] = mpvCacheSecs + } + + if let mpvCachePauseWait = json["mpvCachePauseWait"].string { + Defaults[.mpvCachePauseWait] = mpvCachePauseWait + } + + if let showCacheStatus = json["showCacheStatus"].bool { + Defaults[.showCacheStatus] = showCacheStatus + } + + if let feedCacheSize = json["feedCacheSize"].string { + Defaults[.feedCacheSize] = feedCacheSize + } + } +} diff --git a/Model/Import Export Settings/Importers/BrowsingSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/BrowsingSettingsGroupImporter.swift new file mode 100644 index 00000000..aff8f654 --- /dev/null +++ b/Model/Import Export Settings/Importers/BrowsingSettingsGroupImporter.swift @@ -0,0 +1,144 @@ +import Defaults +import SwiftyJSON + +struct BrowsingSettingsGroupImporter { + var json: JSON + + func performImport() { + if let showHome = json["showHome"].bool { + Defaults[.showHome] = showHome + } + + if let showOpenActionsInHome = json["showOpenActionsInHome"].bool { + Defaults[.showOpenActionsInHome] = showOpenActionsInHome + } + + if let showQueueInHome = json["showQueueInHome"].bool { + Defaults[.showQueueInHome] = showQueueInHome + } + + if let showFavoritesInHome = json["showFavoritesInHome"].bool { + Defaults[.showFavoritesInHome] = showFavoritesInHome + } + + if let favorites = json["favorites"].array { + favorites.forEach { favoriteJSON in + if let jsonString = favoriteJSON.rawString(options: []), + let item = FavoriteItem.bridge.deserialize(jsonString) + { + FavoritesModel.shared.add(item) + } + } + } + + if let widgetsFavorites = json["widgetsSettings"].array { + widgetsFavorites.forEach { widgetJSON in + let dict = widgetJSON.dictionaryValue.mapValues { json in json.stringValue } + if let item = WidgetSettingsBridge().deserialize(dict) { + FavoritesModel.shared.updateWidgetSettings(item) + } + } + } + + if let startupSectionString = json["startupSection"].string, + let startupSection = StartupSection(rawValue: startupSectionString) + { + Defaults[.startupSection] = startupSection + } + + if let visibleSections = json["visibleSections"].array { + let sections = visibleSections.compactMap { visibleSectionJSON in + if let visibleSectionString = visibleSectionJSON.rawString(options: []), + let section = VisibleSection(rawValue: visibleSectionString) + { + return section + } + return nil + } + + Defaults[.visibleSections] = Set(sections) + } + + #if os(iOS) + if let showOpenActionsToolbarItem = json["showOpenActionsToolbarItem"].bool { + Defaults[.showOpenActionsToolbarItem] = showOpenActionsToolbarItem + } + + if let lockPortraitWhenBrowsing = json["lockPortraitWhenBrowsing"].bool { + Defaults[.lockPortraitWhenBrowsing] = lockPortraitWhenBrowsing + } + #endif + + #if !os(tvOS) + if let accountPickerDisplaysUsername = json["accountPickerDisplaysUsername"].bool { + Defaults[.accountPickerDisplaysUsername] = accountPickerDisplaysUsername + } + #endif + + if let accountPickerDisplaysAnonymousAccounts = json["accountPickerDisplaysAnonymousAccounts"].bool { + Defaults[.accountPickerDisplaysAnonymousAccounts] = accountPickerDisplaysAnonymousAccounts + } + + if let showUnwatchedFeedBadges = json["showUnwatchedFeedBadges"].bool { + Defaults[.showUnwatchedFeedBadges] = showUnwatchedFeedBadges + } + + if let expandChannelDescription = json["expandChannelDescription"].bool { + Defaults[.expandChannelDescription] = expandChannelDescription + } + + if let keepChannelsWithUnwatchedFeedOnTop = json["keepChannelsWithUnwatchedFeedOnTop"].bool { + Defaults[.keepChannelsWithUnwatchedFeedOnTop] = keepChannelsWithUnwatchedFeedOnTop + } + + if let showChannelAvatarInChannelsLists = json["showChannelAvatarInChannelsLists"].bool { + Defaults[.showChannelAvatarInChannelsLists] = showChannelAvatarInChannelsLists + } + + if let showChannelAvatarInVideosListing = json["showChannelAvatarInVideosListing"].bool { + Defaults[.showChannelAvatarInVideosListing] = showChannelAvatarInVideosListing + } + + if let playerButtonSingleTapGestureString = json["playerButtonSingleTapGesture"].string, + let playerButtonSingleTapGesture = PlayerTapGestureAction(rawValue: playerButtonSingleTapGestureString) + { + Defaults[.playerButtonSingleTapGesture] = playerButtonSingleTapGesture + } + + if let playerButtonDoubleTapGestureString = json["playerButtonDoubleTapGesture"].string, + let playerButtonDoubleTapGesture = PlayerTapGestureAction(rawValue: playerButtonDoubleTapGestureString) + { + Defaults[.playerButtonDoubleTapGesture] = playerButtonDoubleTapGesture + } + + if let playerButtonShowsControlButtonsWhenMinimized = json["playerButtonShowsControlButtonsWhenMinimized"].bool { + Defaults[.playerButtonShowsControlButtonsWhenMinimized] = playerButtonShowsControlButtonsWhenMinimized + } + + if let playerButtonIsExpanded = json["playerButtonIsExpanded"].bool { + Defaults[.playerButtonIsExpanded] = playerButtonIsExpanded + } + + if let playerBarMaxWidth = json["playerBarMaxWidth"].string { + Defaults[.playerBarMaxWidth] = playerBarMaxWidth + } + + if let channelOnThumbnail = json["channelOnThumbnail"].bool { + Defaults[.channelOnThumbnail] = channelOnThumbnail + } + + if let timeOnThumbnail = json["timeOnThumbnail"].bool { + Defaults[.timeOnThumbnail] = timeOnThumbnail + } + + if let roundedThumbnails = json["roundedThumbnails"].bool { + Defaults[.roundedThumbnails] = roundedThumbnails + } + + if let thumbnailsQualityString = json["thumbnailsQuality"].string, + let thumbnailsQuality = ThumbnailsQuality(rawValue: thumbnailsQualityString) + { + Defaults[.thumbnailsQuality] = thumbnailsQuality + } + } +} diff --git a/Model/Import Export Settings/Importers/ControlsSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/ControlsSettingsGroupImporter.swift new file mode 100644 index 00000000..a3359cca --- /dev/null +++ b/Model/Import Export Settings/Importers/ControlsSettingsGroupImporter.swift @@ -0,0 +1,140 @@ +import Defaults +import SwiftyJSON + +struct ConstrolsSettingsGroupImporter { + var json: JSON + + func performImport() { + if let avPlayerUsesSystemControls = json["avPlayerUsesSystemControls"].bool { + Defaults[.avPlayerUsesSystemControls] = avPlayerUsesSystemControls + } + + if let horizontalPlayerGestureEnabled = json["horizontalPlayerGestureEnabled"].bool { + Defaults[.horizontalPlayerGestureEnabled] = horizontalPlayerGestureEnabled + } + + if let seekGestureSensitivity = json["seekGestureSensitivity"].double { + Defaults[.seekGestureSensitivity] = seekGestureSensitivity + } + + if let seekGestureSpeed = json["seekGestureSpeed"].double { + Defaults[.seekGestureSpeed] = seekGestureSpeed + } + + if let playerControlsLayoutString = json["playerControlsLayout"].string, + let playerControlsLayout = PlayerControlsLayout(rawValue: playerControlsLayoutString) + { + Defaults[.playerControlsLayout] = playerControlsLayout + } + + if let fullScreenPlayerControlsLayoutString = json["fullScreenPlayerControlsLayout"].string, + let fullScreenPlayerControlsLayout = PlayerControlsLayout(rawValue: fullScreenPlayerControlsLayoutString) + { + Defaults[.fullScreenPlayerControlsLayout] = fullScreenPlayerControlsLayout + } + + if let systemControlsCommandsString = json["systemControlsCommands"].string, + let systemControlsCommands = SystemControlsCommands(rawValue: systemControlsCommandsString) + { + Defaults[.systemControlsCommands] = systemControlsCommands + } + + if let buttonBackwardSeekDuration = json["buttonBackwardSeekDuration"].string { + Defaults[.buttonBackwardSeekDuration] = buttonBackwardSeekDuration + } + + if let buttonForwardSeekDuration = json["buttonForwardSeekDuration"].string { + Defaults[.buttonForwardSeekDuration] = buttonForwardSeekDuration + } + + if let gestureBackwardSeekDuration = json["gestureBackwardSeekDuration"].string { + Defaults[.gestureBackwardSeekDuration] = gestureBackwardSeekDuration + } + + if let gestureForwardSeekDuration = json["gestureForwardSeekDuration"].string { + Defaults[.gestureForwardSeekDuration] = gestureForwardSeekDuration + } + + if let systemControlsSeekDuration = json["systemControlsSeekDuration"].string { + Defaults[.systemControlsSeekDuration] = systemControlsSeekDuration + } + + if let playerControlsSettingsEnabled = json["playerControlsSettingsEnabled"].bool { + Defaults[.playerControlsSettingsEnabled] = playerControlsSettingsEnabled + } + + if let playerControlsCloseEnabled = json["playerControlsCloseEnabled"].bool { + Defaults[.playerControlsCloseEnabled] = playerControlsCloseEnabled + } + + if let playerControlsRestartEnabled = json["playerControlsRestartEnabled"].bool { + Defaults[.playerControlsRestartEnabled] = playerControlsRestartEnabled + } + + if let playerControlsAdvanceToNextEnabled = json["playerControlsAdvanceToNextEnabled"].bool { + Defaults[.playerControlsAdvanceToNextEnabled] = playerControlsAdvanceToNextEnabled + } + + if let playerControlsPlaybackModeEnabled = json["playerControlsPlaybackModeEnabled"].bool { + Defaults[.playerControlsPlaybackModeEnabled] = playerControlsPlaybackModeEnabled + } + + if let playerControlsMusicModeEnabled = json["playerControlsMusicModeEnabled"].bool { + Defaults[.playerControlsMusicModeEnabled] = playerControlsMusicModeEnabled + } + + if let playerActionsButtonLabelStyleString = json["playerActionsButtonLabelStyle"].string, + let playerActionsButtonLabelStyle = ButtonLabelStyle(rawValue: playerActionsButtonLabelStyleString) + { + Defaults[.playerActionsButtonLabelStyle] = playerActionsButtonLabelStyle + } + + if let actionButtonShareEnabled = json["actionButtonShareEnabled"].bool { + Defaults[.actionButtonShareEnabled] = actionButtonShareEnabled + } + + if let actionButtonAddToPlaylistEnabled = json["actionButtonAddToPlaylistEnabled"].bool { + Defaults[.actionButtonAddToPlaylistEnabled] = actionButtonAddToPlaylistEnabled + } + + if let actionButtonSubscribeEnabled = json["actionButtonSubscribeEnabled"].bool { + Defaults[.actionButtonSubscribeEnabled] = actionButtonSubscribeEnabled + } + + if let actionButtonSettingsEnabled = json["actionButtonSettingsEnabled"].bool { + Defaults[.actionButtonSettingsEnabled] = actionButtonSettingsEnabled + } + + if let actionButtonHideEnabled = json["actionButtonHideEnabled"].bool { + Defaults[.actionButtonHideEnabled] = actionButtonHideEnabled + } + + if let actionButtonCloseEnabled = json["actionButtonCloseEnabled"].bool { + Defaults[.actionButtonCloseEnabled] = actionButtonCloseEnabled + } + + if let actionButtonFullScreenEnabled = json["actionButtonFullScreenEnabled"].bool { + Defaults[.actionButtonFullScreenEnabled] = actionButtonFullScreenEnabled + } + + if let actionButtonPipEnabled = json["actionButtonPipEnabled"].bool { + Defaults[.actionButtonPipEnabled] = actionButtonPipEnabled + } + + if let actionButtonLockOrientationEnabled = json["actionButtonLockOrientationEnabled"].bool { + Defaults[.actionButtonLockOrientationEnabled] = actionButtonLockOrientationEnabled + } + + if let actionButtonRestartEnabled = json["actionButtonRestartEnabled"].bool { + Defaults[.actionButtonRestartEnabled] = actionButtonRestartEnabled + } + + if let actionButtonAdvanceToNextItemEnabled = json["actionButtonAdvanceToNextItemEnabled"].bool { + Defaults[.actionButtonAdvanceToNextItemEnabled] = actionButtonAdvanceToNextItemEnabled + } + + if let actionButtonMusicModeEnabled = json["actionButtonMusicModeEnabled"].bool { + Defaults[.actionButtonMusicModeEnabled] = actionButtonMusicModeEnabled + } + } +} diff --git a/Model/Import Export Settings/Importers/HistorySettingsGroupImporter.swift b/Model/Import Export Settings/Importers/HistorySettingsGroupImporter.swift new file mode 100644 index 00000000..24385cd8 --- /dev/null +++ b/Model/Import Export Settings/Importers/HistorySettingsGroupImporter.swift @@ -0,0 +1,54 @@ +import Defaults +import SwiftyJSON + +struct HistorySettingsGroupImporter { + var json: JSON + + func performImport() { + if let saveRecents = json["saveRecents"].bool { + Defaults[.saveRecents] = saveRecents + } + + if let saveHistory = json["saveHistory"].bool { + Defaults[.saveHistory] = saveHistory + } + + if let showWatchingProgress = json["showWatchingProgress"].bool { + Defaults[.showWatchingProgress] = showWatchingProgress + } + + if let saveLastPlayed = json["saveLastPlayed"].bool { + Defaults[.saveLastPlayed] = saveLastPlayed + } + + if let watchedVideoPlayNowBehaviorString = json["watchedVideoPlayNowBehavior"].string, + let watchedVideoPlayNowBehavior = WatchedVideoPlayNowBehavior(rawValue: watchedVideoPlayNowBehaviorString) + { + Defaults[.watchedVideoPlayNowBehavior] = watchedVideoPlayNowBehavior + } + + if let watchedThreshold = json["watchedThreshold"].int { + Defaults[.watchedThreshold] = watchedThreshold + } + + if let resetWatchedStatusOnPlaying = json["resetWatchedStatusOnPlaying"].bool { + Defaults[.resetWatchedStatusOnPlaying] = resetWatchedStatusOnPlaying + } + + if let watchedVideoStyleString = json["watchedVideoStyle"].string, + let watchedVideoStyle = WatchedVideoStyle(rawValue: watchedVideoStyleString) + { + Defaults[.watchedVideoStyle] = watchedVideoStyle + } + + if let watchedVideoBadgeColorString = json["watchedVideoBadgeColor"].string, + let watchedVideoBadgeColor = WatchedVideoBadgeColor(rawValue: watchedVideoBadgeColorString) + { + Defaults[.watchedVideoBadgeColor] = watchedVideoBadgeColor + } + + if let showToggleWatchedStatusButton = json["showToggleWatchedStatusButton"].bool { + Defaults[.showToggleWatchedStatusButton] = showToggleWatchedStatusButton + } + } +} diff --git a/Model/Import Export Settings/Importers/LocationsSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/LocationsSettingsGroupImporter.swift new file mode 100644 index 00000000..9c2e0c9c --- /dev/null +++ b/Model/Import Export Settings/Importers/LocationsSettingsGroupImporter.swift @@ -0,0 +1,84 @@ +import Defaults +import SwiftyJSON + +struct LocationsSettingsGroupImporter { + var json: JSON + + var includePublicLocations = true + var includedInstancesIDs = Set() + var includedAccountsIDs = Set() + var includedAccountsPasswords = [Account.ID: String]() + + init( + json: JSON, + includePublicLocations: Bool = true, + includedInstancesIDs: Set = [], + includedAccountsIDs: Set = [], + includedAccountsPasswords: [Account.ID: String] = [:] + ) { + self.json = json + self.includePublicLocations = includePublicLocations + self.includedInstancesIDs = includedInstancesIDs + self.includedAccountsIDs = includedAccountsIDs + self.includedAccountsPasswords = includedAccountsPasswords + } + + var instances: [Instance] { + if let instances = json["instances"].array { + return instances.compactMap { instanceJSON in + let dict = instanceJSON.dictionaryValue.mapValues { json in json.stringValue } + return InstancesBridge().deserialize(dict) + } + } + + return [] + } + + var accounts: [Account] { + if let accounts = json["accounts"].array { + return accounts.compactMap { accountJSON in + let dict = accountJSON.dictionaryValue.mapValues { json in json.stringValue } + return AccountsBridge().deserialize(dict) + } + } + + return [] + } + + func performImport() { + if includePublicLocations { + Defaults[.instancesManifest] = json["instancesManifest"].string ?? "" + Defaults[.countryOfPublicInstances] = json["countryOfPublicInstances"].string ?? "" + } + + instances.filter { includedInstancesIDs.contains($0.id) }.forEach { instance in + _ = InstancesModel.shared.insert(id: instance.id, app: instance.app, name: instance.name, url: instance.apiURLString) + } + + if let accounts = json["accounts"].array { + accounts.forEach { accountJSON in + let dict = accountJSON.dictionaryValue.mapValues { json in json.stringValue } + if let account = AccountsBridge().deserialize(dict), + includedAccountsIDs.contains(account.id) + { + var password = account.password + if password?.isEmpty ?? true { + password = includedAccountsPasswords[account.id] + } + if let password, + !password.isEmpty, + let instanceID = account.instanceID, + let instance = InstancesModel.shared.find(instanceID) + { + if !instance.accounts.contains(where: { instanceAccount in + let (username, _) = instanceAccount.credentials + return username == account.username + }) { + _ = AccountsModel.add(instance: instance, id: account.id, name: account.name, username: account.username, password: password) + } + } + } + } + } + } +} diff --git a/Model/Import Export Settings/Importers/OtherDataSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/OtherDataSettingsGroupImporter.swift new file mode 100644 index 00000000..aea8b36a --- /dev/null +++ b/Model/Import Export Settings/Importers/OtherDataSettingsGroupImporter.swift @@ -0,0 +1,70 @@ +import Defaults +import SwiftyJSON + +struct OtherDataSettingsGroupImporter { + var json: JSON + + func performImport() { + if let lastAccountID = json["lastAccountID"].string { + Defaults[.lastAccountID] = lastAccountID + } + + if let lastInstanceID = json["lastInstanceID"].string { + Defaults[.lastInstanceID] = lastInstanceID + } + + if let playerRate = json["playerRate"].double { + Defaults[.playerRate] = playerRate + } + + if let trendingCategoryString = json["trendingCategory"].string, + let trendingCategory = TrendingCategory(rawValue: trendingCategoryString) + { + Defaults[.trendingCategory] = trendingCategory + } + + if let trendingCountryString = json["trendingCountry"].string, + let trendingCountry = Country(rawValue: trendingCountryString) + { + Defaults[.trendingCountry] = trendingCountry + } + + if let subscriptionsViewPageString = json["subscriptionsViewPage"].string, + let subscriptionsViewPage = SubscriptionsView.Page(rawValue: subscriptionsViewPageString) + { + Defaults[.subscriptionsViewPage] = subscriptionsViewPage + } + + if let subscriptionsListingStyle = json["subscriptionsListingStyle"].string { + Defaults[.subscriptionsListingStyle] = ListingStyle(rawValue: subscriptionsListingStyle) ?? .list + } + + if let popularListingStyle = json["popularListingStyle"].string { + Defaults[.popularListingStyle] = ListingStyle(rawValue: popularListingStyle) ?? .list + } + + if let trendingListingStyle = json["trendingListingStyle"].string { + Defaults[.trendingListingStyle] = ListingStyle(rawValue: trendingListingStyle) ?? .list + } + + if let playlistListingStyle = json["playlistListingStyle"].string { + Defaults[.playlistListingStyle] = ListingStyle(rawValue: playlistListingStyle) ?? .list + } + + if let channelPlaylistListingStyle = json["channelPlaylistListingStyle"].string { + Defaults[.channelPlaylistListingStyle] = ListingStyle(rawValue: channelPlaylistListingStyle) ?? .list + } + + if let searchListingStyle = json["searchListingStyle"].string { + Defaults[.searchListingStyle] = ListingStyle(rawValue: searchListingStyle) ?? .list + } + + if let hideShorts = json["hideShorts"].bool { + Defaults[.hideShorts] = hideShorts + } + + if let hideWatched = json["hideWatched"].bool { + Defaults[.hideWatched] = hideWatched + } + } +} diff --git a/Model/Import Export Settings/Importers/PlayerSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/PlayerSettingsGroupImporter.swift new file mode 100644 index 00000000..df556438 --- /dev/null +++ b/Model/Import Export Settings/Importers/PlayerSettingsGroupImporter.swift @@ -0,0 +1,100 @@ +import Defaults +import SwiftyJSON + +struct PlayerSettingsGroupImporter { + var json: JSON + + func performImport() { + if let playerInstanceID = json["playerInstanceID"].string { + Defaults[.playerInstanceID] = playerInstanceID + } + + if let pauseOnHidingPlayer = json["pauseOnHidingPlayer"].bool { + Defaults[.pauseOnHidingPlayer] = pauseOnHidingPlayer + } + + if let closeVideoOnEOF = json["closeVideoOnEOF"].bool { + Defaults[.closeVideoOnEOF] = closeVideoOnEOF + } + + if let expandVideoDescription = json["expandVideoDescription"].bool { + Defaults[.expandVideoDescription] = expandVideoDescription + } + + if let collapsedLinesDescription = json["collapsedLinesDescription"].int { + Defaults[.collapsedLinesDescription] = collapsedLinesDescription + } + + if let showChapters = json["showChapters"].bool { + Defaults[.showChapters] = showChapters + } + + if let expandChapters = json["expandChapters"].bool { + Defaults[.expandChapters] = expandChapters + } + + if let showRelated = json["showRelated"].bool { + Defaults[.showRelated] = showRelated + } + + if let showInspectorString = json["showInspector"].string, + let showInspector = ShowInspectorSetting(rawValue: showInspectorString) + { + Defaults[.showInspector] = showInspector + } + + if let playerSidebarString = json["playerSidebar"].string, + let playerSidebar = PlayerSidebarSetting(rawValue: playerSidebarString) + { + Defaults[.playerSidebar] = playerSidebar + } + + if let showKeywords = json["showKeywords"].bool { + Defaults[.showKeywords] = showKeywords + } + + if let enableReturnYouTubeDislike = json["enableReturnYouTubeDislike"].bool { + Defaults[.enableReturnYouTubeDislike] = enableReturnYouTubeDislike + } + + if let closePiPOnNavigation = json["closePiPOnNavigation"].bool { + Defaults[.closePiPOnNavigation] = closePiPOnNavigation + } + + if let closePiPOnOpeningPlayer = json["closePiPOnOpeningPlayer"].bool { + Defaults[.closePiPOnOpeningPlayer] = closePiPOnOpeningPlayer + } + + if let closePlayerOnOpeningPiP = json["closePlayerOnOpeningPiP"].bool { + Defaults[.closePlayerOnOpeningPiP] = closePlayerOnOpeningPiP + } + + #if !os(macOS) + if let pauseOnEnteringBackground = json["pauseOnEnteringBackground"].bool { + Defaults[.pauseOnEnteringBackground] = pauseOnEnteringBackground + } + #endif + + #if !os(tvOS) + if let showScrollToTopInComments = json["showScrollToTopInComments"].bool { + Defaults[.showScrollToTopInComments] = showScrollToTopInComments + } + #endif + + #if os(iOS) + if let honorSystemOrientationLock = json["honorSystemOrientationLock"].bool { + Defaults[.honorSystemOrientationLock] = honorSystemOrientationLock + } + + if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool { + Defaults[.enterFullscreenInLandscape] = enterFullscreenInLandscape + } + + if let rotateToLandscapeOnEnterFullScreenString = json["rotateToLandscapeOnEnterFullScreen"].string, + let rotateToLandscapeOnEnterFullScreen = FullScreenRotationSetting(rawValue: rotateToLandscapeOnEnterFullScreenString) + { + Defaults[.rotateToLandscapeOnEnterFullScreen] = rotateToLandscapeOnEnterFullScreen + } + #endif + } +} diff --git a/Model/Import Export Settings/Importers/QualitySettingsGroupImporter.swift b/Model/Import Export Settings/Importers/QualitySettingsGroupImporter.swift new file mode 100644 index 00000000..27403ce2 --- /dev/null +++ b/Model/Import Export Settings/Importers/QualitySettingsGroupImporter.swift @@ -0,0 +1,37 @@ +import Defaults +import SwiftyJSON + +struct QualitySettingsGroupImporter { + var json: JSON + + func performImport() { + if let batteryCellularProfileString = json["batteryCellularProfile"].string { + Defaults[.batteryCellularProfile] = batteryCellularProfileString + } + + if let batteryNonCellularProfileString = json["batteryNonCellularProfile"].string { + Defaults[.batteryNonCellularProfile] = batteryNonCellularProfileString + } + + if let chargingCellularProfileString = json["chargingCellularProfile"].string { + Defaults[.chargingCellularProfile] = chargingCellularProfileString + } + + if let chargingNonCellularProfileString = json["chargingNonCellularProfile"].string { + Defaults[.chargingNonCellularProfile] = chargingNonCellularProfileString + } + + if let forceAVPlayerForLiveStreams = json["forceAVPlayerForLiveStreams"].bool { + Defaults[.forceAVPlayerForLiveStreams] = forceAVPlayerForLiveStreams + } + + if let qualityProfiles = json["qualityProfiles"].array { + qualityProfiles.forEach { qualityProfileJSON in + let dict = qualityProfileJSON.dictionaryValue.mapValues { json in json.stringValue } + if let item = QualityProfileBridge().deserialize(dict) { + QualityProfilesModel.shared.update(item, item) + } + } + } + } +} diff --git a/Model/Import Export Settings/Importers/RecentlyOpenedImporter.swift b/Model/Import Export Settings/Importers/RecentlyOpenedImporter.swift new file mode 100644 index 00000000..b3bf2535 --- /dev/null +++ b/Model/Import Export Settings/Importers/RecentlyOpenedImporter.swift @@ -0,0 +1,17 @@ +import Defaults +import SwiftyJSON + +struct RecentlyOpenedImporter { + var json: JSON + + func performImport() { + if let recentlyOpened = json["recentlyOpened"].array { + recentlyOpened.forEach { recentlyOpenedJSON in + let dict = recentlyOpenedJSON.dictionaryValue.mapValues { json in json.stringValue } + if let item = RecentItemBridge().deserialize(dict) { + RecentsModel.shared.add(item) + } + } + } + } +} diff --git a/Model/Import Export Settings/Importers/SponsorBlockSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/SponsorBlockSettingsGroupImporter.swift new file mode 100644 index 00000000..ea9da9c1 --- /dev/null +++ b/Model/Import Export Settings/Importers/SponsorBlockSettingsGroupImporter.swift @@ -0,0 +1,16 @@ +import Defaults +import SwiftyJSON + +struct SponsorBlockSettingsGroupImporter { + var json: JSON + + func performImport() { + if let sponsorBlockInstance = json["sponsorBlockInstance"].string { + Defaults[.sponsorBlockInstance] = sponsorBlockInstance + } + + if let sponsorBlockCategories = json["sponsorBlockCategories"].array { + Defaults[.sponsorBlockCategories] = Set(sponsorBlockCategories.compactMap { $0.string }) + } + } +} diff --git a/Model/NavigationModel.swift b/Model/NavigationModel.swift index f8d08010..641ef6ad 100644 --- a/Model/NavigationModel.swift +++ b/Model/NavigationModel.swift @@ -107,6 +107,10 @@ final class NavigationModel: ObservableObject { @Published var presentingFileImporter = false + @Published var presentingSettingsImportSheet = false + @Published var presentingSettingsFileImporter = false + @Published var settingsImportURL: URL? + func openChannel(_ channel: Channel, navigationStyle: NavigationStyle) { guard channel.id != Video.fixtureChannelID else { return @@ -269,6 +273,8 @@ final class NavigationModel: ObservableObject { presentingChannel = false presentingPlaylist = false presentingOpenVideos = false + presentingFileImporter = false + presentingSettingsImportSheet = false } func hideKeyboard() { @@ -279,8 +285,9 @@ final class NavigationModel: ObservableObject { func presentAlert(title: String, message: String? = nil) { let message = message.isNil ? nil : Text(message!) - alert = Alert(title: Text(title), message: message) - presentingAlert = true + let alert = Alert(title: Text(title), message: message) + + presentAlert(alert) } func presentRequestErrorAlert(_ error: RequestError) { @@ -289,6 +296,11 @@ final class NavigationModel: ObservableObject { } func presentAlert(_ alert: Alert) { + guard !presentingSettings else { + SettingsModel.shared.presentAlert(alert) + return + } + self.alert = alert presentingAlert = true } @@ -311,6 +323,16 @@ final class NavigationModel: ObservableObject { print("not implemented") } } + + func presentSettingsImportSheet(_ url: URL, forceSettings: Bool = false) { + guard !presentingSettings, !forceSettings else { + ImportExportSettingsModel.shared.reset() + SettingsModel.shared.presentSettingsImportSheet(url) + return + } + settingsImportURL = url + presentingSettingsImportSheet = true + } } typealias TabSelection = NavigationModel.TabSelection diff --git a/Model/SettingsModel.swift b/Model/SettingsModel.swift index 41fdd3d6..4d597ad4 100644 --- a/Model/SettingsModel.swift +++ b/Model/SettingsModel.swift @@ -7,6 +7,9 @@ final class SettingsModel: ObservableObject { @Published var presentingAlert = false @Published var alert = Alert(title: Text("Error")) + @Published var presentingSettingsImportSheet = false + @Published var settingsImportURL: URL? + func presentAlert(title: String, message: String? = nil) { let message = message.isNil ? nil : Text(message!) alert = Alert(title: Text(title), message: message) @@ -17,4 +20,9 @@ final class SettingsModel: ObservableObject { self.alert = alert presentingAlert = true } + + func presentSettingsImportSheet(_ url: URL) { + settingsImportURL = url + presentingSettingsImportSheet = true + } } diff --git a/Shared/Constants.swift b/Shared/Constants.swift index e6dafbdf..59bae11f 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -61,6 +61,26 @@ enum Constants { #endif } + static var deviceName: String { + #if os(macOS) + Host().localizedName ?? "Mac" + #else + UIDevice.current.name + #endif + } + + static var platform: String { + #if os(macOS) + "macOS" + #elseif os(iOS) + "iOS" + #elseif os(tvOS) + "tvOS" + #else + "unknown" + #endif + } + static func seekIcon(_ type: String, _ interval: TimeInterval) -> String { let interval = Int(interval) let allVersions = [10, 15, 30, 45, 60, 75, 90] diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index f6e86d08..7492ef86 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -6,37 +6,22 @@ import SwiftUI #endif extension Defaults.Keys { - static let instancesManifest = Key("instancesManifest", default: "") - static let countryOfPublicInstances = Key("countryOfPublicInstances") - - static let instances = Key<[Instance]>("instances", default: []) - static let accounts = Key<[Account]>("accounts", default: []) - static let lastAccountID = Key("lastAccountID") - static let lastInstanceID = Key("lastInstanceID") - static let lastUsedPlaylistID = Key("lastPlaylistID") - static let lastAccountIsPublic = Key("lastAccountIsPublic", default: false) - - static let sponsorBlockInstance = Key("sponsorBlockInstance", default: "https://sponsor.ajay.app") - static let sponsorBlockCategories = Key>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories)) - - static let enableReturnYouTubeDislike = Key("enableReturnYouTubeDislike", default: false) + // MARK: GROUP - Browsing static let showHome = Key("showHome", default: true) static let showOpenActionsInHome = Key("showOpenActionsInHome", default: true) static let showQueueInHome = Key("showQueueInHome", default: true) - static let showOpenActionsToolbarItem = Key("showOpenActionsToolbarItem", default: false) static let showFavoritesInHome = Key("showFavoritesInHome", default: true) + static let favorites = Key<[FavoriteItem]>("favorites", default: []) + static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: []) + static let startupSection = Key("startupSection", default: .home) + static let visibleSections = Key>("visibleSections", default: [.subscriptions, .trending, .playlists]) + + static let showOpenActionsToolbarItem = Key("showOpenActionsToolbarItem", default: false) #if os(iOS) static let showDocuments = Key("showDocuments", default: false) + static let lockPortraitWhenBrowsing = Key("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone) #endif - static let homeHistoryItems = Key("homeHistoryItems", default: 10) - static let favorites = Key<[FavoriteItem]>("favorites", default: []) - - static let playerButtonSingleTapGesture = Key("playerButtonSingleTapGesture", default: .togglePlayer) - static let playerButtonDoubleTapGesture = Key("playerButtonDoubleTapGesture", default: .nothing) - static let playerButtonShowsControlButtonsWhenMinimized = Key("playerButtonShowsControlButtonsWhenMinimized", default: false) - static let playerButtonIsExpanded = Key("playerButtonIsExpanded", default: false) - static let playerBarMaxWidth = Key("playerBarMaxWidth", default: "600") #if !os(tvOS) #if os(macOS) @@ -46,21 +31,146 @@ extension Defaults.Keys { #endif static let accountPickerDisplaysUsername = Key("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault) #endif + static let accountPickerDisplaysAnonymousAccounts = Key("accountPickerDisplaysAnonymousAccounts", default: true) - #if os(iOS) - static let lockPortraitWhenBrowsing = Key("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone) - #endif static let showUnwatchedFeedBadges = Key("showUnwatchedFeedBadges", default: false) - static let keepChannelsWithUnwatchedFeedOnTop = Key("keepChannelsWithUnwatchedFeedOnTop", default: true) - static let showToggleWatchedStatusButton = Key("showToggleWatchedStatusButton", default: false) static let expandChannelDescription = Key("expandChannelDescription", default: false) + + static let keepChannelsWithUnwatchedFeedOnTop = Key("keepChannelsWithUnwatchedFeedOnTop", default: true) + static let showChannelAvatarInChannelsLists = Key("showChannelAvatarInChannelsLists", default: true) + static let showChannelAvatarInVideosListing = Key("showChannelAvatarInVideosListing", default: true) + + static let playerButtonSingleTapGesture = Key("playerButtonSingleTapGesture", default: .togglePlayer) + static let playerButtonDoubleTapGesture = Key("playerButtonDoubleTapGesture", default: .nothing) + static let playerButtonShowsControlButtonsWhenMinimized = Key("playerButtonShowsControlButtonsWhenMinimized", default: false) + static let playerButtonIsExpanded = Key("playerButtonIsExpanded", default: false) + static let playerBarMaxWidth = Key("playerBarMaxWidth", default: "600") static let channelOnThumbnail = Key("channelOnThumbnail", default: false) static let timeOnThumbnail = Key("timeOnThumbnail", default: true) static let roundedThumbnails = Key("roundedThumbnails", default: true) static let thumbnailsQuality = Key("thumbnailsQuality", default: .highest) - static let captionsLanguageCode = Key("captionsLanguageCode") - static let activeBackend = Key("activeBackend", default: .mpv) + // MARK: GROUP - Player + + static let playerInstanceID = Key("playerInstance") + + #if os(tvOS) + static let pauseOnHidingPlayerDefault = true + #else + static let pauseOnHidingPlayerDefault = false + #endif + static let pauseOnHidingPlayer = Key("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault) + + static let closeVideoOnEOF = Key("closeVideoOnEOF", default: false) + + #if !os(macOS) + static let pauseOnEnteringBackground = Key("pauseOnEnteringBackground", default: true) + #endif + + #if os(iOS) + static let expandVideoDescriptionDefault = Constants.isIPad + #else + static let expandVideoDescriptionDefault = true + #endif + static let expandVideoDescription = Key("expandVideoDescription", default: expandVideoDescriptionDefault) + + static let collapsedLinesDescription = Key("collapsedLinesDescription", default: 5) + + static let showChapters = Key("showChapters", default: true) + static let expandChapters = Key("expandChapters", default: true) + static let showRelated = Key("showRelated", default: true) + static let showInspector = Key("showInspector", default: .onlyLocal) + + static let playerSidebar = Key("playerSidebar", default: .defaultValue) + static let showKeywords = Key("showKeywords", default: false) + #if !os(tvOS) + static let showScrollToTopInComments = Key("showScrollToTopInComments", default: true) + #endif + static let enableReturnYouTubeDislike = Key("enableReturnYouTubeDislike", default: false) + + #if os(iOS) + static let honorSystemOrientationLock = Key("honorSystemOrientationLock", default: true) + static let enterFullscreenInLandscape = Key("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone) + static let rotateToLandscapeOnEnterFullScreen = Key( + "rotateToLandscapeOnEnterFullScreen", + default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled + ) + #endif + + static let closePiPOnNavigation = Key("closePiPOnNavigation", default: false) + static let closePiPOnOpeningPlayer = Key("closePiPOnOpeningPlayer", default: false) + static let closePlayerOnOpeningPiP = Key("closePlayerOnOpeningPiP", default: false) + #if !os(macOS) + static let closePiPAndOpenPlayerOnEnteringForeground = Key("closePiPAndOpenPlayerOnEnteringForeground", default: false) + #endif + + // MARK: GROUP - Controls + + static let avPlayerUsesSystemControls = Key("avPlayerUsesSystemControls", default: true) + static let horizontalPlayerGestureEnabled = Key("horizontalPlayerGestureEnabled", default: true) + static let seekGestureSensitivity = Key("seekGestureSensitivity", default: 30.0) + static let seekGestureSpeed = Key("seekGestureSpeed", default: 0.5) + + #if os(iOS) + static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small + static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small + #elseif os(tvOS) + static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular + static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular + #else + static let playerControlsLayoutDefault = PlayerControlsLayout.medium + static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.medium + #endif + + static let playerControlsLayout = Key("playerControlsLayout", default: playerControlsLayoutDefault) + static let fullScreenPlayerControlsLayout = Key("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault) + + static let systemControlsCommands = Key("systemControlsCommands", default: .restartAndAdvanceToNext) + + static let buttonBackwardSeekDuration = Key("buttonBackwardSeekDuration", default: "10") + static let buttonForwardSeekDuration = Key("buttonForwardSeekDuration", default: "10") + static let gestureBackwardSeekDuration = Key("gestureBackwardSeekDuration", default: "10") + static let gestureForwardSeekDuration = Key("gestureForwardSeekDuration", default: "10") + static let systemControlsSeekDuration = Key("systemControlsBackwardSeekDuration", default: "10") + + #if os(iOS) + static let playerControlsLockOrientationEnabled = Key("playerControlsLockOrientationEnabled", default: true) + #endif + #if os(tvOS) + static let playerControlsSettingsEnabledDefault = true + #else + static let playerControlsSettingsEnabledDefault = false + #endif + static let playerControlsSettingsEnabled = Key("playerControlsSettingsEnabled", default: playerControlsSettingsEnabledDefault) + static let playerControlsCloseEnabled = Key("playerControlsCloseEnabled", default: true) + static let playerControlsRestartEnabled = Key("playerControlsRestartEnabled", default: false) + static let playerControlsAdvanceToNextEnabled = Key("playerControlsAdvanceToNextEnabled", default: false) + static let playerControlsPlaybackModeEnabled = Key("playerControlsPlaybackModeEnabled", default: false) + static let playerControlsMusicModeEnabled = Key("playerControlsMusicModeEnabled", default: false) + + // TODO: IMPLEMENT THIS + // ** rgdfo;fgks iojsiojf + #if os(macOS) + static let playerDetailsPageButtonLabelStyleDefault = ButtonLabelStyle.iconAndText + #else + static let playerDetailsPageButtonLabelStyleDefault = UIDevice.current.userInterfaceIdiom == .phone ? ButtonLabelStyle.iconOnly : .iconAndText + #endif + static let playerActionsButtonLabelStyle = Key("playerActionsButtonLabelStyle", default: playerDetailsPageButtonLabelStyleDefault) + + static let actionButtonShareEnabled = Key("actionButtonShareEnabled", default: true) + static let actionButtonAddToPlaylistEnabled = Key("actionButtonAddToPlaylistEnabled", default: true) + static let actionButtonSubscribeEnabled = Key("actionButtonSubscribeEnabled", default: false) + static let actionButtonSettingsEnabled = Key("actionButtonSettingsEnabled", default: true) + static let actionButtonHideEnabled = Key("actionButtonHideEnabled", default: false) + static let actionButtonCloseEnabled = Key("actionButtonCloseEnabled", default: true) + static let actionButtonFullScreenEnabled = Key("actionButtonFullScreenEnabled", default: false) + static let actionButtonPipEnabled = Key("actionButtonPipEnabled", default: false) + static let actionButtonLockOrientationEnabled = Key("actionButtonLockOrientationEnabled", default: false) + static let actionButtonRestartEnabled = Key("actionButtonRestartEnabled", default: false) + static let actionButtonAdvanceToNextItemEnabled = Key("actionButtonAdvanceToNextItemEnabled", default: false) + static let actionButtonMusicModeEnabled = Key("actionButtonMusicModeEnabled", default: true) + + // MARK: GROUP - Quality static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases) static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases) @@ -109,150 +219,66 @@ extension Defaults.Keys { static let chargingCellularProfileDefault = hd1080pMPVProfile.id static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id #endif - static let playerRate = Key("playerRate", default: 1.0) - static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault) + static let batteryCellularProfile = Key("batteryCellularProfile", default: batteryCellularProfileDefault) static let batteryNonCellularProfile = Key("batteryNonCellularProfile", default: batteryNonCellularProfileDefault) static let chargingCellularProfile = Key("chargingCellularProfile", default: chargingCellularProfileDefault) static let chargingNonCellularProfile = Key("chargingNonCellularProfile", default: chargingNonCellularProfileDefault) static let forceAVPlayerForLiveStreams = Key("forceAVPlayerForLiveStreams", default: true) - static let playerSidebar = Key("playerSidebar", default: .defaultValue) - static let playerInstanceID = Key("playerInstance") - #if os(iOS) - static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small - static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small - #elseif os(tvOS) - static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular - static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular - #else - static let playerControlsLayoutDefault = PlayerControlsLayout.medium - static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.medium - #endif + static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault) - static let playerControlsLayout = Key("playerControlsLayout", default: playerControlsLayoutDefault) - static let fullScreenPlayerControlsLayout = Key("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault) - static let avPlayerUsesSystemControls = Key("avPlayerUsesSystemControls", default: true) - static let horizontalPlayerGestureEnabled = Key("horizontalPlayerGestureEnabled", default: true) - static let seekGestureSpeed = Key("seekGestureSpeed", default: 0.5) - static let seekGestureSensitivity = Key("seekGestureSensitivity", default: 30.0) - static let showKeywords = Key("showKeywords", default: false) - #if !os(tvOS) - static let showScrollToTopInComments = Key("showScrollToTopInComments", default: true) - #endif - - #if os(iOS) - static let expandVideoDescriptionDefault = Constants.isIPad - #else - static let expandVideoDescriptionDefault = true - #endif - static let expandVideoDescription = Key("expandVideoDescription", default: expandVideoDescriptionDefault) - static let collapsedLinesDescription = Key("collapsedLinesDescription", default: 5) - - static let showChannelAvatarInChannelsLists = Key("showChannelAvatarInChannelsLists", default: true) - static let showChannelAvatarInVideosListing = Key("showChannelAvatarInVideosListing", default: true) - - #if os(tvOS) - static let pauseOnHidingPlayerDefault = true - #else - static let pauseOnHidingPlayerDefault = false - #endif - static let pauseOnHidingPlayer = Key("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault) - - #if !os(macOS) - static let pauseOnEnteringBackground = Key("pauseOnEnteringBackground", default: true) - #endif - static let closeVideoOnEOF = Key("closeVideoOnEOF", default: false) - static let closePiPOnNavigation = Key("closePiPOnNavigation", default: false) - static let closePiPOnOpeningPlayer = Key("closePiPOnOpeningPlayer", default: false) - #if !os(macOS) - static let closePiPAndOpenPlayerOnEnteringForeground = Key("closePiPAndOpenPlayerOnEnteringForeground", default: false) - #endif - static let closePlayerOnOpeningPiP = Key("closePlayerOnOpeningPiP", default: false) - - static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) - - static let queue = Key<[PlayerQueueItem]>("queue", default: []) - static let saveLastPlayed = Key("saveLastPlayed", default: false) - static let lastPlayed = Key("lastPlayed") - static let playbackMode = Key("playbackMode", default: .queue) + // MARK: GROUP - History + static let saveRecents = Key("saveRecents", default: true) static let saveHistory = Key("saveHistory", default: true) static let showWatchingProgress = Key("showWatchingProgress", default: true) + static let saveLastPlayed = Key("saveLastPlayed", default: false) + + static let watchedVideoPlayNowBehavior = Key("watchedVideoPlayNowBehavior", default: .continue) static let watchedThreshold = Key("watchedThreshold", default: 90) + static let resetWatchedStatusOnPlaying = Key("resetWatchedStatusOnPlaying", default: false) + static let watchedVideoStyle = Key("watchedVideoStyle", default: .badge) static let watchedVideoBadgeColor = Key("WatchedVideoBadgeColor", default: .red) - static let watchedVideoPlayNowBehavior = Key("watchedVideoPlayNowBehavior", default: .continue) - static let resetWatchedStatusOnPlaying = Key("resetWatchedStatusOnPlaying", default: false) - static let saveRecents = Key("saveRecents", default: true) + static let showToggleWatchedStatusButton = Key("showToggleWatchedStatusButton", default: false) - static let trendingCategory = Key("trendingCategory", default: .default) - static let trendingCountry = Key("trendingCountry", default: .us) + // MARK: GROUP - SponsorBlock - static let visibleSections = Key>("visibleSections", default: [.subscriptions, .trending, .playlists]) - static let startupSection = Key("startupSection", default: .home) + static let sponsorBlockInstance = Key("sponsorBlockInstance", default: "https://sponsor.ajay.app") + static let sponsorBlockCategories = Key>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories)) - #if os(iOS) - static let honorSystemOrientationLock = Key("honorSystemOrientationLock", default: true) - static let enterFullscreenInLandscape = Key("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone) - static let rotateToLandscapeOnEnterFullScreen = Key( - "rotateToLandscapeOnEnterFullScreen", - default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled - ) - #endif + // MARK: GROUP - Locations + + static let instancesManifest = Key("instancesManifest", default: "") + static let countryOfPublicInstances = Key("countryOfPublicInstances") + + static let instances = Key<[Instance]>("instances", default: []) + static let accounts = Key<[Account]>("accounts", default: []) + + // MARK: Group - Advanced - static let showMPVPlaybackStats = Key("showMPVPlaybackStats", default: false) static let showPlayNowInBackendContextMenu = Key("showPlayNowInBackendContextMenu", default: false) - #if os(macOS) - static let playerDetailsPageButtonLabelStyleDefault = ButtonLabelStyle.iconAndText - #else - static let playerDetailsPageButtonLabelStyleDefault = UIDevice.current.userInterfaceIdiom == .phone ? ButtonLabelStyle.iconOnly : .iconAndText - #endif - static let playerActionsButtonLabelStyle = Key("playerActionsButtonLabelStyle", default: .iconAndText) - - static let systemControlsCommands = Key("systemControlsCommands", default: .restartAndAdvanceToNext) - - static let buttonBackwardSeekDuration = Key("buttonBackwardSeekDuration", default: "10") - static let buttonForwardSeekDuration = Key("buttonForwardSeekDuration", default: "10") - static let gestureBackwardSeekDuration = Key("gestureBackwardSeekDuration", default: "10") - static let gestureForwardSeekDuration = Key("gestureForwardSeekDuration", default: "10") - static let systemControlsSeekDuration = Key("systemControlsBackwardSeekDuration", default: "10") - static let actionButtonShareEnabled = Key("actionButtonShareEnabled", default: true) - static let actionButtonAddToPlaylistEnabled = Key("actionButtonAddToPlaylistEnabled", default: true) - static let actionButtonSubscribeEnabled = Key("actionButtonSubscribeEnabled", default: false) - static let actionButtonSettingsEnabled = Key("actionButtonSettingsEnabled", default: true) - static let actionButtonHideEnabled = Key("actionButtonHideEnabled", default: false) - static let actionButtonCloseEnabled = Key("actionButtonCloseEnabled", default: true) - static let actionButtonFullScreenEnabled = Key("actionButtonFullScreenEnabled", default: false) - static let actionButtonPipEnabled = Key("actionButtonPipEnabled", default: false) - static let actionButtonLockOrientationEnabled = Key("actionButtonLockOrientationEnabled", default: false) - static let actionButtonRestartEnabled = Key("actionButtonRestartEnabled", default: false) - static let actionButtonAdvanceToNextItemEnabled = Key("actionButtonAdvanceToNextItemEnabled", default: false) - static let actionButtonMusicModeEnabled = Key("actionButtonMusicModeEnabled", default: true) - - #if os(iOS) - static let playerControlsLockOrientationEnabled = Key("playerControlsLockOrientationEnabled", default: true) - #endif - #if os(tvOS) - static let playerControlsSettingsEnabledDefault = true - #else - static let playerControlsSettingsEnabledDefault = false - #endif - static let playerControlsSettingsEnabled = Key("playerControlsSettingsEnabled", default: playerControlsSettingsEnabledDefault) - static let playerControlsCloseEnabled = Key("playerControlsCloseEnabled", default: true) - static let playerControlsRestartEnabled = Key("playerControlsRestartEnabled", default: false) - static let playerControlsAdvanceToNextEnabled = Key("playerControlsAdvanceToNextEnabled", default: false) - static let playerControlsPlaybackModeEnabled = Key("playerControlsPlaybackModeEnabled", default: false) - static let playerControlsMusicModeEnabled = Key("playerControlsMusicModeEnabled", default: false) - + static let showMPVPlaybackStats = Key("showMPVPlaybackStats", default: false) + static let mpvEnableLogging = Key("mpvEnableLogging", default: false) static let mpvCacheSecs = Key("mpvCacheSecs", default: "120") static let mpvCachePauseWait = Key("mpvCachePauseWait", default: "3") - static let mpvEnableLogging = Key("mpvEnableLogging", default: false) static let showCacheStatus = Key("showCacheStatus", default: false) static let feedCacheSize = Key("feedCacheSize", default: "50") + // MARK: GROUP - Other exportable + + static let lastAccountID = Key("lastAccountID") + static let lastInstanceID = Key("lastInstanceID") + + static let playerRate = Key("playerRate", default: 1.0) + static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) + + static let trendingCategory = Key("trendingCategory", default: .default) + static let trendingCountry = Key("trendingCountry", default: .us) + static let subscriptionsViewPage = Key("subscriptionsViewPage", default: .feed) static let subscriptionsListingStyle = Key("subscriptionsListingStyle", default: .cells) @@ -263,11 +289,22 @@ extension Defaults.Keys { static let searchListingStyle = Key("searchListingStyle", default: .cells) static let hideShorts = Key("hideShorts", default: false) static let hideWatched = Key("hideWatched", default: false) - static let showInspector = Key("showInspector", default: .onlyLocal) - static let showChapters = Key("showChapters", default: true) - static let expandChapters = Key("expandChapters", default: true) - static let showRelated = Key("showRelated", default: true) - static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: []) + + // MARK: GROUP - Not exportable + + static let queue = Key<[PlayerQueueItem]>("queue", default: []) + static let playbackMode = Key("playbackMode", default: .queue) + static let lastPlayed = Key("lastPlayed") + + static let activeBackend = Key("activeBackend", default: .mpv) + static let captionsLanguageCode = Key("captionsLanguageCode") + + static let lastUsedPlaylistID = Key("lastPlaylistID") + static let lastAccountIsPublic = Key("lastAccountIsPublic", default: false) + + // MARK: LEGACY + + static let homeHistoryItems = Key("homeHistoryItems", default: 10) } enum ResolutionSetting: String, CaseIterable, Defaults.Serializable { diff --git a/Shared/Home/HomeView.swift b/Shared/Home/HomeView.swift index 19822d0a..3ea74615 100644 --- a/Shared/Home/HomeView.swift +++ b/Shared/Home/HomeView.swift @@ -22,7 +22,6 @@ struct HomeView: View { @Default(.favorites) private var favorites @Default(.widgetsSettings) private var widgetsSettings #endif - @Default(.homeHistoryItems) private var homeHistoryItems @Default(.showFavoritesInHome) private var showFavoritesInHome @Default(.showOpenActionsInHome) private var showOpenActionsInHome @Default(.showQueueInHome) private var showQueueInHome diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index dd25318e..24fbce03 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -68,6 +68,7 @@ struct ContentView: View { SettingsView() } ) + .modifier(ImportSettingsSheetViewModifier(isPresented: $navigation.presentingSettingsImportSheet, settingsFile: $navigation.settingsImportURL)) .background( EmptyView().sheet(isPresented: $navigation.presentingAccounts) { AccountsView() diff --git a/Shared/OpenURLHandler.swift b/Shared/OpenURLHandler.swift index 0a4d769d..15c97024 100644 --- a/Shared/OpenURLHandler.swift +++ b/Shared/OpenURLHandler.swift @@ -14,6 +14,11 @@ struct OpenURLHandler { var navigationStyle: NavigationStyle func handle(_ url: URL) { + if url.isFileURL, url.standardizedFileURL.absoluteString.hasSuffix(".\(ImportExportSettingsModel.settingsExtension)") { + navigation.presentSettingsImportSheet(url) + return + } + if Self.firstHandle { Self.firstHandle = false diff --git a/Shared/Settings/AccountForm.swift b/Shared/Settings/AccountForm.swift index 6e3d8ec2..359c2de0 100644 --- a/Shared/Settings/AccountForm.swift +++ b/Shared/Settings/AccountForm.swift @@ -153,7 +153,7 @@ struct AccountForm: View { return } - let account = AccountsModel.add(instance: instance, name: name, username: username, password: password) + let account = AccountsModel.add(instance: instance, id: nil, name: name, username: username, password: password) selectedAccount?.wrappedValue = account presentationMode.wrappedValue.dismiss() diff --git a/Shared/Settings/ExportSettings.swift b/Shared/Settings/ExportSettings.swift new file mode 100644 index 00000000..8f31ad74 --- /dev/null +++ b/Shared/Settings/ExportSettings.swift @@ -0,0 +1,165 @@ +import SwiftUI + +struct ExportSettings: View { + @ObservedObject private var model = ImportExportSettingsModel.shared + @State private var presentingShareSheet = false + @StateObject private var settings = SettingsModel.shared + + private var filesToShare = [ImportExportSettingsModel.exportFile] + @ObservedObject private var navigation = NavigationModel.shared + + var body: some View { + Group { + #if os(macOS) + VStack { + list + + importExportButtons + } + #else + list + #if os(iOS) + .listStyle(.insetGrouped) + .sheet( + isPresented: $presentingShareSheet, + onDismiss: { self.model.isExportInProgress = false } + ) { + ShareSheet(activityItems: filesToShare) + .id("settings-share-\(filesToShare.count)") + } + #endif + #endif + } + .navigationTitle("Export Settings") + } + + var list: some View { + List { + exportView + } + .onAppear { + model.reset() + } + } + + var importExportButtons: some View { + HStack { + importButton + + Spacer() + + exportButton + } + } + + @ViewBuilder var importButton: some View { + #if os(macOS) + Button { + navigation.presentingSettingsFileImporter = true + } label: { + Label("Import", systemImage: "square.and.arrow.down") + } + #endif + } + + struct ExportGroupRow: View { + let group: ImportExportSettingsModel.ExportGroup + + @ObservedObject private var model = ImportExportSettingsModel.shared + + var body: some View { + Button(action: { model.toggleExportGroupSelection(group) }) { + HStack { + Text(group.label) + Spacer() + Image(systemName: "checkmark") + .foregroundColor(.accent) + .opacity(isGroupInSelectedGroups ? 1 : 0) + } + .animation(nil, value: isGroupInSelectedGroups) + .contentShape(Rectangle()) + } + } + + var isGroupInSelectedGroups: Bool { + model.selectedExportGroups.contains(group) + } + } + + var exportView: some View { + Group { + Section(header: Text("Settings")) { + ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in + ExportGroupRow(group: group) + } + } + + Section(header: Text("Locations")) { + ForEach(ImportExportSettingsModel.ExportGroup.locationsGroups) { group in + ExportGroupRow(group: group) + .disabled(!model.isGroupEnabled(group)) + } + } + + Section(header: Text("Other"), footer: otherGroupsFooter) { + ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in + ExportGroupRow(group: group) + } + } + + #if !os(macOS) + Section { + exportButton + } + #endif + } + .buttonStyle(.plain) + .disabled(model.isExportInProgress) + } + + var exportButton: some View { + Button(action: exportSettings) { + Label(model.isExportInProgress ? "Export in progress..." : "Export...", systemImage: model.isExportInProgress ? "fireworks" : "square.and.arrow.up") + .animation(nil, value: model.isExportInProgress) + #if !os(macOS) + .foregroundColor(.accent) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + #endif + } + .disabled(!model.isExportAvailable) + } + + @ViewBuilder var otherGroupsFooter: some View { + Text("Other data include last used playback preferences and listing options") + } + + func exportSettings() { + let export = { + model.isExportInProgress = true + Delay.by(0.3) { + model.exportAction() + #if !os(macOS) + self.presentingShareSheet = true + #endif + } + } + + if model.isGroupSelected(.accountsUnencryptedPasswords) { + settings.presentAlert(Alert( + title: Text("Are you sure you want to export unencrypted passwords?"), + message: Text("Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import"), + primaryButton: .destructive(Text("Export"), action: export), + secondaryButton: .cancel() + )) + } else { + export() + } + } +} + +#Preview { + NavigationView { + ExportSettings() + } +} diff --git a/Shared/Settings/Import/ImportSettingsAccountRow.swift b/Shared/Settings/Import/ImportSettingsAccountRow.swift new file mode 100644 index 00000000..8069973e --- /dev/null +++ b/Shared/Settings/Import/ImportSettingsAccountRow.swift @@ -0,0 +1,187 @@ +import SwiftUI + +struct ImportSettingsAccountRow: View { + var account: Account + var fileModel: ImportSettingsFileModel + + @State private var password = "" + + @State private var isValid = false + @State private var isValidated = false + @State private var isValidating = false + @State private var validationError: String? + @State private var validationDebounce = Debounce() + + @ObservedObject private var model = ImportSettingsSheetViewModel.shared + + func afterValidation() { + if isValid { + model.importableAccounts.insert(account.id) + model.selectedAccounts.insert(account.id) + model.importableAccountsPasswords[account.id] = password + } else { + model.selectedAccounts.remove(account.id) + model.importableAccounts.remove(account.id) + model.importableAccountsPasswords.removeValue(forKey: account.id) + } + } + + var body: some View { + Button(action: { model.toggleAccount(account, accounts: accounts) }) { + let accountExists = AccountsModel.shared.find(account.id) != nil + + VStack(alignment: .leading) { + HStack { + Text(account.username) + Spacer() + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + .opacity(isChecked ? 1 : 0) + } + Text(account.instance?.description ?? "") + .font(.caption) + .foregroundColor(.secondary) + + Group { + if let instanceID = account.instanceID { + if accountExists { + HStack { + Image(systemName: "xmark.circle.fill") + .foregroundColor(Color("AppRedColor")) + Text("Account already exists") + } + } else { + Group { + if InstancesModel.shared.find(instanceID) != nil { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("Custom Location already exists") + } + } else if model.selectedInstances.contains(instanceID) { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("Custom Location selected for import") + } + } else { + HStack { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + Text("Custom Location not selected for import") + } + .foregroundColor(Color("AppRedColor")) + } + } + .frame(minHeight: 20) + + if account.password.isNil || account.password!.isEmpty { + Group { + if password.isEmpty { + HStack { + Image(systemName: "key") + Text("Password required to import") + } + .foregroundColor(Color("AppRedColor")) + } else { + AccountValidationStatus( + app: .constant(instance.app), + isValid: $isValid, + isValidated: $isValidated, + isValidating: $isValidating, + error: $validationError + ) + } + } + .frame(minHeight: 20) + } else { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + + Text("Password saved in import file") + } + } + } + } + } + .foregroundColor(.primary) + .font(.caption) + .padding(.vertical, 2) + + if !accountExists && (account.password.isNil || account.password!.isEmpty) { + SecureField("Password", text: $password) + .onChange(of: password) { _ in validate() } + #if !os(tvOS) + .textFieldStyle(RoundedBorderTextFieldStyle()) + #endif + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onChange(of: isValid) { _ in afterValidation() } + .animation(nil, value: isChecked) + } + .buttonStyle(.plain) + } + + var isChecked: Bool { + model.isSelectedForImport(account) + } + + var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? { + fileModel.locationsSettingsGroupImporter + } + + var accounts: [Account] { + fileModel.locationsSettingsGroupImporter?.accounts ?? [] + } + + private var instance: Instance! { + (fileModel.locationsSettingsGroupImporter?.instances ?? []).first { $0.id == account.instanceID } + } + + private var validator: AccountValidator { + AccountValidator( + app: .constant(instance.app), + url: instance.apiURLString, + account: Account(instanceID: instance.id, urlString: instance.apiURLString, username: account.username, password: password), + id: .constant(account.username), + isValid: $isValid, + isValidated: $isValidated, + isValidating: $isValidating, + error: $validationError + ) + } + + private func validate() { + isValid = false + validationDebounce.invalidate() + + guard !account.username.isEmpty, !password.isEmpty else { + validator.reset() + return + } + + isValidating = true + + validationDebounce.debouncing(1) { + validator.validateAccount() + } + } +} + +#Preview { + let fileModel = ImportSettingsFileModel(url: URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!) + + return List { + ImportSettingsAccountRow( + account: .init(name: "arekf", urlString: "https://instance.com", username: "arekf"), + fileModel: fileModel + ) + ImportSettingsAccountRow( + account: .init(name: "arekf", urlString: "https://instance.com", username: "arekf", password: "a"), + fileModel: fileModel + ) + } +} diff --git a/Shared/Settings/Import/ImportSettingsFileImporterViewModifier.swift b/Shared/Settings/Import/ImportSettingsFileImporterViewModifier.swift new file mode 100644 index 00000000..575961f8 --- /dev/null +++ b/Shared/Settings/Import/ImportSettingsFileImporterViewModifier.swift @@ -0,0 +1,30 @@ +import Foundation +import SwiftUI + +struct ImportSettingsFileImporterViewModifier: ViewModifier { + @Binding var isPresented: Bool + + func body(content: Content) -> some View { + content + .fileImporter(isPresented: $isPresented, allowedContentTypes: [.json]) { result in + do { + let selectedFile = try result.get() + var urlToOpen: URL? + + if let bookmarkURL = URLBookmarkModel.shared.loadBookmark(selectedFile) { + urlToOpen = bookmarkURL + } + + if selectedFile.startAccessingSecurityScopedResource() { + URLBookmarkModel.shared.saveBookmark(selectedFile) + urlToOpen = selectedFile + } + + guard let urlToOpen else { return } + NavigationModel.shared.presentSettingsImportSheet(urlToOpen, forceSettings: true) + } catch { + NavigationModel.shared.presentAlert(title: "Could not open Files") + } + } + } +} diff --git a/Shared/Settings/Import/ImportSettingsSheetView.swift b/Shared/Settings/Import/ImportSettingsSheetView.swift new file mode 100644 index 00000000..5372e7fb --- /dev/null +++ b/Shared/Settings/Import/ImportSettingsSheetView.swift @@ -0,0 +1,260 @@ +import SwiftUI + +struct ImportSettingsSheetView: View { + @Binding var settingsFile: URL? + @StateObject private var model = ImportSettingsSheetViewModel.shared + @StateObject private var importExportModel = ImportExportSettingsModel.shared + + @Environment(\.presentationMode) private var presentationMode + + @State private var presentingCompletedAlert = false + + private let accountsModel = AccountsModel.shared + + var body: some View { + Group { + #if os(macOS) + list + .frame(width: 700, height: 800) + #else + NavigationView { + list + } + #endif + } + .onAppear { + guard let fileModel else { return } + model.reset(fileModel.locationsSettingsGroupImporter) + importExportModel.reset(fileModel) + } + .onChange(of: settingsFile) { _ in + importExportModel.reset(fileModel) + } + } + + var list: some View { + List { + importGroupView + + importOptions + + metadata + } + .alert(isPresented: $presentingCompletedAlert) { + completedAlert + } + #if os(iOS) + .backport + .scrollDismissesKeyboardInteractively() + #endif + .navigationTitle("Import Settings") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(action: { presentationMode.wrappedValue.dismiss() }) { + Text("Cancel") + } + } + ToolbarItem(placement: .confirmationAction) { + Button(action: { + fileModel?.performImport() + presentingCompletedAlert = true + ImportExportSettingsModel.shared.reset() + }) { + Text("Import") + } + .disabled(!canImport) + } + } + } + + var completedAlert: Alert { + Alert( + title: Text("Import Completed"), + dismissButton: .default(Text("Close")) { + if accountsModel.isEmpty, + let account = InstancesModel.shared.all.first?.anonymousAccount + { + accountsModel.setCurrent(account) + } + presentationMode.wrappedValue.dismiss() + } + ) + } + + var canImport: Bool { + return !model.selectedAccounts.isEmpty || !model.selectedInstances.isEmpty || !importExportModel.selectedExportGroups.isEmpty + } + + var fileModel: ImportSettingsFileModel? { + guard let settingsFile else { return nil } + + return ImportSettingsFileModel(url: settingsFile) + } + + var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? { + guard let fileModel else { return nil } + + return fileModel.locationsSettingsGroupImporter + } + + struct ExportGroupRow: View { + let group: ImportExportSettingsModel.ExportGroup + + @ObservedObject private var model = ImportExportSettingsModel.shared + + var body: some View { + Button(action: { model.toggleExportGroupSelection(group) }) { + HStack { + Text(group.label) + Spacer() + Image(systemName: "checkmark") + .foregroundColor(.accent) + .opacity(isChecked ? 1 : 0) + } + .contentShape(Rectangle()) + .foregroundColor(.primary) + .animation(nil, value: isChecked) + } + .buttonStyle(.plain) + } + + var isChecked: Bool { + model.selectedExportGroups.contains(group) + } + } + + var importGroupView: some View { + Group { + Section(header: Text("Settings")) { + ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in + ExportGroupRow(group: group) + .disabled(!fileModel!.isGroupIncludedInFile(group)) + } + } + + Section(header: Text("Other")) { + ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in + ExportGroupRow(group: group) + .disabled(!fileModel!.isGroupIncludedInFile(group)) + } + } + } + } + + @ViewBuilder var metadata: some View { + if let fileModel { + Section(header: Text("File information")) { + MetadataRow(name: Text("Name"), value: Text(fileModel.filename)) + + if let date = fileModel.metadataDate { + MetadataRow(name: Text("Date"), value: Text(date)) + } + + if let build = fileModel.metadataBuild { + MetadataRow(name: Text("Build"), value: Text(build)) + } + + if let platform = fileModel.metadataPlatform { + MetadataRow(name: Text("Platform"), value: Text(platform)) + } + } + } + } + + struct MetadataRow: View { + let name: Text + let value: Text + + var body: some View { + HStack { + name + .layoutPriority(2) + + Spacer() + + value + .layoutPriority(1) + .lineLimit(2) + .foregroundColor(.secondary) + } + } + } + + var instances: [Instance] { + locationsSettingsGroupImporter?.instances ?? [] + } + + var accounts: [Account] { + locationsSettingsGroupImporter?.accounts ?? [] + } + + struct ImportInstanceRow: View { + var instance: Instance + var accounts: [Account] + + @ObservedObject private var model = ImportSettingsSheetViewModel.shared + + var body: some View { + Button(action: { model.toggleInstance(instance, accounts: accounts) }) { + VStack { + Group { + HStack { + Text(instance.description) + Spacer() + Image(systemName: "checkmark") + .opacity(isChecked ? 1 : 0) + .foregroundColor(.accentColor) + } + + if model.isInstanceAlreadyAdded(instance) { + HStack { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + Text("Custom Location already exists") + } + .font(.caption) + .padding(.vertical, 2) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .contentShape(Rectangle()) + .foregroundColor(.primary) + .transaction { t in t.animation = nil } + } + .buttonStyle(.plain) + } + + var isChecked: Bool { + model.isImportable(instance) && model.selectedInstances.contains(instance.id) + } + } + + @ViewBuilder var importOptions: some View { + if let fileModel { + if fileModel.isPublicInstancesSettingsGroupInFile || !instances.isEmpty { + Section(header: Text("Locations")) { + if fileModel.isPublicInstancesSettingsGroupInFile { + ExportGroupRow(group: .locationsSettings) + } + + ForEach(instances) { instance in + ImportInstanceRow(instance: instance, accounts: accounts) + } + } + } + + if !accounts.isEmpty { + Section(header: Text("Accounts")) { + ForEach(accounts) { account in + ImportSettingsAccountRow(account: account, fileModel: fileModel) + } + } + } + } + } +} + +#Preview { + ImportSettingsSheetView(settingsFile: .constant(URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!)) +} diff --git a/Shared/Settings/Import/ImportSettingsSheetViewModel.swift b/Shared/Settings/Import/ImportSettingsSheetViewModel.swift new file mode 100644 index 00000000..343e59da --- /dev/null +++ b/Shared/Settings/Import/ImportSettingsSheetViewModel.swift @@ -0,0 +1,77 @@ +import Foundation +import SwiftUI + +class ImportSettingsSheetViewModel: ObservableObject { + static let shared = ImportSettingsSheetViewModel() + + @Published var selectedInstances = Set() + @Published var selectedAccounts = Set() + + @Published var importableAccounts = Set() + @Published var importableAccountsPasswords = [Account.ID: String]() + + func toggleInstance(_ instance: Instance, accounts: [Account]) { + if selectedInstances.contains(instance.id) { + selectedInstances.remove(instance.id) + } else { + guard isImportable(instance) else { return } + selectedInstances.insert(instance.id) + } + + removeNonImportableFromSelectedAccounts(accounts: accounts) + } + + func toggleAccount(_ account: Account, accounts: [Account]) { + if selectedAccounts.contains(account.id) { + selectedAccounts.remove(account.id) + } else { + guard isImportable(account.id, accounts: accounts) else { return } + selectedAccounts.insert(account.id) + } + } + + func isSelectedForImport(_ account: Account) -> Bool { + importableAccounts.contains(account.id) && selectedAccounts.contains(account.id) + } + + func isImportable(_ accountID: Account.ID, accounts: [Account]) -> Bool { + guard let account = accounts.first(where: { $0.id == accountID }), + let instanceID = account.instanceID, + AccountsModel.shared.find(accountID) == nil + else { return false } + + return ((account.password != nil && !account.password!.isEmpty) || + importableAccounts.contains(account.id)) && ( + (InstancesModel.shared.find(instanceID) != nil) || + selectedInstances.contains(instanceID) + ) + } + + func isImportable(_ instance: Instance) -> Bool { + !isInstanceAlreadyAdded(instance) + } + + func isInstanceAlreadyAdded(_ instance: Instance) -> Bool { + InstancesModel.shared.find(instance.id) != nil || InstancesModel.shared.findByURLString(instance.apiURLString) != nil + } + + func removeNonImportableFromSelectedAccounts(accounts: [Account]) { + selectedAccounts = Set(selectedAccounts.filter { isImportable($0, accounts: accounts) }) + } + + func reset() { + selectedAccounts = [] + selectedInstances = [] + importableAccounts = [] + } + + func reset(_ importer: LocationsSettingsGroupImporter? = nil) { + reset() + + guard let importer else { return } + + selectedInstances = Set(importer.instances.filter { isImportable($0) }.map(\.id)) + importableAccounts = Set(importer.accounts.filter { isImportable($0.id, accounts: importer.accounts) }.map(\.id)) + selectedAccounts = importableAccounts + } +} diff --git a/Shared/Settings/Import/ImportSettingsSheetViewModifier.swift b/Shared/Settings/Import/ImportSettingsSheetViewModifier.swift new file mode 100644 index 00000000..76eec764 --- /dev/null +++ b/Shared/Settings/Import/ImportSettingsSheetViewModifier.swift @@ -0,0 +1,25 @@ +import Foundation +import SwiftUI +import SwiftyJSON + +struct ImportSettingsSheetViewModifier: ViewModifier { + @Binding var isPresented: Bool + @Binding var settingsFile: URL? + + func body(content: Content) -> some View { + content + .sheet(isPresented: $isPresented) { + ImportSettingsSheetView(settingsFile: $settingsFile) + } + } +} + +#Preview { + Text("") + .modifier( + ImportSettingsSheetViewModifier( + isPresented: .constant(true), + settingsFile: .constant(URL(string: "https://gist.githubusercontent.com/arekf/87b4d6702755b01139431dcb809f9fdc/raw/7bb5cdba3ffc0c479f5260430ddc43c4a79a7a72/yattee-177-iPhone.yatteesettings")!) + ) + ) +} diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index 63d63085..d226b725 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -7,7 +7,7 @@ struct SettingsView: View { #if os(macOS) private enum Tabs: Hashable { - case browsing, player, controls, quality, history, sponsorBlock, locations, advanced, help + case browsing, player, controls, quality, history, sponsorBlock, locations, advanced, importExport, help } @State private var selection: Tabs = .browsing @@ -24,13 +24,22 @@ struct SettingsView: View { @Default(.instances) private var instances + @State private var filesToShare = [] + + @ObservedObject private var navigation = NavigationModel.shared + @ObservedObject private var settingsModel = SettingsModel.shared + var body: some View { settings - .alert(isPresented: $model.presentingAlert) { model.alert } - #if os(iOS) - .backport - .scrollDismissesKeyboardInteractively() + #if !os(tvOS) + .modifier(ImportSettingsFileImporterViewModifier(isPresented: $navigation.presentingSettingsFileImporter)) + .modifier(ImportSettingsSheetViewModifier(isPresented: $settingsModel.presentingSettingsImportSheet, settingsFile: $settingsModel.settingsImportURL)) #endif + #if os(iOS) + .backport + .scrollDismissesKeyboardInteractively() + #endif + .alert(isPresented: $model.presentingAlert) { model.alert } } var settings: some View { @@ -101,6 +110,14 @@ struct SettingsView: View { } .tag(Tabs.advanced) + Group { + ExportSettings() + } + .tabItem { + Label("Export", systemImage: "square.and.arrow.up") + } + .tag(Tabs.importExport) + Form { Help() } @@ -110,7 +127,7 @@ struct SettingsView: View { .tag(Tabs.help) } .padding(20) - .frame(width: 650, height: windowHeight) + .frame(width: 700, height: windowHeight) #else NavigationView { settingsList @@ -206,6 +223,8 @@ struct SettingsView: View { .padding(.horizontal, 20) #endif + importView + Section(footer: helpFooter) { NavigationLink { Help() @@ -260,6 +279,28 @@ struct SettingsView: View { } #endif + var importView: some View { + Section { + Button(action: importSettings) { + Label("Import Settings...", systemImage: "square.and.arrow.down") + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .foregroundColor(.accent) + .buttonStyle(.plain) + + NavigationLink(destination: LazyView(ExportSettings())) { + Label("Export Settings", systemImage: "square.and.arrow.up") + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + } + } + + func importSettings() { + navigation.presentingSettingsFileImporter = true + } + #if os(macOS) private var windowHeight: Double { switch selection { @@ -278,7 +319,9 @@ struct SettingsView: View { case .locations: return 600 case .advanced: - return 380 + return 500 + case .importExport: + return 580 case .help: return 650 } diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 3f69a67e..110d638e 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -21,6 +21,14 @@ struct YatteeApp: App { } static var logsDirectory: URL { + temporaryDirectory + } + + static var settingsExportDirectory: URL { + temporaryDirectory + } + + private static var temporaryDirectory: URL { URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 01e0d348..fc088835 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -188,6 +188,13 @@ 372AA410286D067B0000B1DC /* Repeat in Frameworks */ = {isa = PBXBuildFile; productRef = 372AA40F286D067B0000B1DC /* Repeat */; }; 372AA412286D06950000B1DC /* Repeat in Frameworks */ = {isa = PBXBuildFile; productRef = 372AA411286D06950000B1DC /* Repeat */; }; 372AA414286D06A10000B1DC /* Repeat in Frameworks */ = {isa = PBXBuildFile; productRef = 372AA413286D06A10000B1DC /* Repeat */; }; + 372C74632B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */; }; + 372C74642B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */; }; + 372C74662B67044300BE179B /* ImportSettingsSheetViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74652B67044300BE179B /* ImportSettingsSheetViewModifier.swift */; }; + 372C74672B67044300BE179B /* ImportSettingsSheetViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74652B67044300BE179B /* ImportSettingsSheetViewModifier.swift */; }; + 372C74682B67044900BE179B /* ImportSettingsSheetViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74652B67044300BE179B /* ImportSettingsSheetViewModifier.swift */; }; + 372C746A2B67098A00BE179B /* ImportSettingsFileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */; }; + 372C746B2B67098A00BE179B /* ImportSettingsFileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */; }; 372CFD15285F2E2A00B0B54B /* ControlsBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372CFD14285F2E2A00B0B54B /* ControlsBar.swift */; }; 372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372CFD14285F2E2A00B0B54B /* ControlsBar.swift */; }; 372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; }; @@ -332,6 +339,12 @@ 37599F36272B44000087F250 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F33272B44000087F250 /* FavoritesModel.swift */; }; 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; }; 37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; }; + 375AC29A2B66B7D600B680E7 /* ExportSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375AC2992B66B7D600B680E7 /* ExportSettings.swift */; }; + 375AC29B2B66B7D600B680E7 /* ExportSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375AC2992B66B7D600B680E7 /* ExportSettings.swift */; }; + 375AC29C2B66B7D600B680E7 /* ExportSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375AC2992B66B7D600B680E7 /* ExportSettings.swift */; }; + 375AC29E2B66BDD600B680E7 /* ImportExportSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */; }; + 375AC29F2B66BDD600B680E7 /* ImportExportSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */; }; + 375AC2A02B66BDD600B680E7 /* ImportExportSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */; }; 375B537428DF6CBB004C1D19 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 375B537828DF6CBB004C1D19 /* Localizable.strings */; }; 375B537528DF6CBB004C1D19 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 375B537828DF6CBB004C1D19 /* Localizable.strings */; }; 375B537628DF6CBB004C1D19 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 375B537828DF6CBB004C1D19 /* Localizable.strings */; }; @@ -649,6 +662,64 @@ 37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; }; 37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; }; 37A5DBCA285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */; }; + 37A7D6E32B67E303009CB1ED /* ImportSettingsFileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */; }; + 37A7D6E52B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */; }; + 37A7D6E62B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */; }; + 37A7D6E72B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */; }; + 37A7D6E92B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */; }; + 37A7D6EA2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */; }; + 37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */; }; + 37A7D6ED2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */; }; + 37A7D6EE2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */; }; + 37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */; }; + 37A7D6F32B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6F22B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift */; }; + 37A7D6F42B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6F22B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift */; }; + 37A7D6F52B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6F22B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift */; }; + 37A7D6F72B68071C009CB1ED /* PlayerSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6F62B68071C009CB1ED /* PlayerSettingsGroupImporter.swift */; }; + 37A7D6F82B68071C009CB1ED /* PlayerSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6F62B68071C009CB1ED /* PlayerSettingsGroupImporter.swift */; }; + 37A7D6F92B68071C009CB1ED /* PlayerSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6F62B68071C009CB1ED /* PlayerSettingsGroupImporter.swift */; }; + 37A7D6FB2B680822009CB1ED /* ControlsSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6FA2B680822009CB1ED /* ControlsSettingsGroupExporter.swift */; }; + 37A7D6FC2B680822009CB1ED /* ControlsSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6FA2B680822009CB1ED /* ControlsSettingsGroupExporter.swift */; }; + 37A7D6FD2B680822009CB1ED /* ControlsSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6FA2B680822009CB1ED /* ControlsSettingsGroupExporter.swift */; }; + 37A7D6FF2B68082F009CB1ED /* ControlsSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6FE2B68082F009CB1ED /* ControlsSettingsGroupImporter.swift */; }; + 37A7D7002B68082F009CB1ED /* ControlsSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6FE2B68082F009CB1ED /* ControlsSettingsGroupImporter.swift */; }; + 37A7D7012B68082F009CB1ED /* ControlsSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D6FE2B68082F009CB1ED /* ControlsSettingsGroupImporter.swift */; }; + 37A7D7032B680A97009CB1ED /* QualitySettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7022B680A97009CB1ED /* QualitySettingsGroupExporter.swift */; }; + 37A7D7042B680A97009CB1ED /* QualitySettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7022B680A97009CB1ED /* QualitySettingsGroupExporter.swift */; }; + 37A7D7052B680A97009CB1ED /* QualitySettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7022B680A97009CB1ED /* QualitySettingsGroupExporter.swift */; }; + 37A7D7072B680A9E009CB1ED /* QualitySettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7062B680A9E009CB1ED /* QualitySettingsGroupImporter.swift */; }; + 37A7D7082B680A9E009CB1ED /* QualitySettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7062B680A9E009CB1ED /* QualitySettingsGroupImporter.swift */; }; + 37A7D7092B680A9E009CB1ED /* QualitySettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7062B680A9E009CB1ED /* QualitySettingsGroupImporter.swift */; }; + 37A7D70B2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D70A2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift */; }; + 37A7D70C2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D70A2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift */; }; + 37A7D70D2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D70A2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift */; }; + 37A7D70F2B680CED009CB1ED /* HistorySettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D70E2B680CED009CB1ED /* HistorySettingsGroupImporter.swift */; }; + 37A7D7102B680CED009CB1ED /* HistorySettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D70E2B680CED009CB1ED /* HistorySettingsGroupImporter.swift */; }; + 37A7D7112B680CED009CB1ED /* HistorySettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D70E2B680CED009CB1ED /* HistorySettingsGroupImporter.swift */; }; + 37A7D7132B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7122B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift */; }; + 37A7D7142B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7122B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift */; }; + 37A7D7152B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7122B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift */; }; + 37A7D7172B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7162B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift */; }; + 37A7D7182B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7162B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift */; }; + 37A7D7192B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7162B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift */; }; + 37A7D71B2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D71A2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift */; }; + 37A7D71C2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D71A2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift */; }; + 37A7D71D2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D71A2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift */; }; + 37A7D71F2B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D71E2B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift */; }; + 37A7D7202B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D71E2B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift */; }; + 37A7D7212B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D71E2B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift */; }; + 37A7D7232B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7222B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift */; }; + 37A7D7242B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7222B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift */; }; + 37A7D7252B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7222B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift */; }; + 37A7D7272B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7262B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift */; }; + 37A7D7282B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7262B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift */; }; + 37A7D7292B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D7262B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift */; }; + 37A7D72B2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D72A2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift */; }; + 37A7D72C2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D72A2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift */; }; + 37A7D72D2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D72A2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift */; }; + 37A7D72F2B681011009CB1ED /* OtherDataSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D72E2B681011009CB1ED /* OtherDataSettingsGroupImporter.swift */; }; + 37A7D7302B681011009CB1ED /* OtherDataSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D72E2B681011009CB1ED /* OtherDataSettingsGroupImporter.swift */; }; + 37A7D7312B681011009CB1ED /* OtherDataSettingsGroupImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A7D72E2B681011009CB1ED /* OtherDataSettingsGroupImporter.swift */; }; 37A81BF9294BD1440081D322 /* WatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A81BF8294BD1440081D322 /* WatchView.swift */; }; 37A81BFA294BD1440081D322 /* WatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A81BF8294BD1440081D322 /* WatchView.swift */; }; 37A81BFB294BD1440081D322 /* WatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A81BF8294BD1440081D322 /* WatchView.swift */; }; @@ -712,6 +783,15 @@ 37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */; }; 37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA42699FB72009BE4FB /* Alamofire */; }; 37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA8269A570B009BE4FB /* Alamofire */; }; + 37BBB33A2B6B9053001C4845 /* ImportSettingsSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB3392B6B9053001C4845 /* ImportSettingsSheetViewModel.swift */; }; + 37BBB33B2B6B9053001C4845 /* ImportSettingsSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB3392B6B9053001C4845 /* ImportSettingsSheetViewModel.swift */; }; + 37BBB33C2B6B9053001C4845 /* ImportSettingsSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB3392B6B9053001C4845 /* ImportSettingsSheetViewModel.swift */; }; + 37BBB33F2B6B9D52001C4845 /* ImportSettingsSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */; }; + 37BBB3402B6B9D52001C4845 /* ImportSettingsSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */; }; + 37BBB3412B6B9D52001C4845 /* ImportSettingsSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */; }; + 37BBB3432B6BB88F001C4845 /* ImportSettingsAccountRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */; }; + 37BBB3442B6BB88F001C4845 /* ImportSettingsAccountRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */; }; + 37BBB3452B6BB88F001C4845 /* ImportSettingsAccountRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */; }; 37BC50A82778A84700510953 /* HistorySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50A72778A84700510953 /* HistorySettings.swift */; }; 37BC50A92778A84700510953 /* HistorySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50A72778A84700510953 /* HistorySettings.swift */; }; 37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50A72778A84700510953 /* HistorySettings.swift */; }; @@ -872,6 +952,12 @@ 37E70927271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */; }; 37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */; }; 37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */; }; + 37E75CC72B6AEAF7003A6237 /* RecentlyOpenedImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E75CC62B6AEAF7003A6237 /* RecentlyOpenedImporter.swift */; }; + 37E75CC82B6AEAF7003A6237 /* RecentlyOpenedImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E75CC62B6AEAF7003A6237 /* RecentlyOpenedImporter.swift */; }; + 37E75CC92B6AEAF7003A6237 /* RecentlyOpenedImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E75CC62B6AEAF7003A6237 /* RecentlyOpenedImporter.swift */; }; + 37E75CCB2B6AEB01003A6237 /* RecentlyOpenedExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E75CCA2B6AEB01003A6237 /* RecentlyOpenedExporter.swift */; }; + 37E75CCC2B6AEB01003A6237 /* RecentlyOpenedExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E75CCA2B6AEB01003A6237 /* RecentlyOpenedExporter.swift */; }; + 37E75CCD2B6AEB01003A6237 /* RecentlyOpenedExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E75CCA2B6AEB01003A6237 /* RecentlyOpenedExporter.swift */; }; 37E80F3C287B107F00561799 /* VideoDetailsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */; }; 37E80F3D287B107F00561799 /* VideoDetailsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */; }; 37E80F40287B472300561799 /* ScrollContentBackground+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E80F3F287B472300561799 /* ScrollContentBackground+Backport.swift */; }; @@ -1100,6 +1186,9 @@ 3728203F2945E4A8009A0E2D /* SubscriptionsPageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsPageButton.swift; sourceTree = ""; }; 3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = ""; }; 372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; + 372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsFileImporterViewModifier.swift; sourceTree = ""; }; + 372C74652B67044300BE179B /* ImportSettingsSheetViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsSheetViewModifier.swift; sourceTree = ""; }; + 372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsFileModel.swift; sourceTree = ""; }; 372CFD14285F2E2A00B0B54B /* ControlsBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsBar.swift; sourceTree = ""; }; 373031F22838388A000CFD59 /* PlayerLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLayerView.swift; sourceTree = ""; tabWidth = 5; }; 373031F428383A89000CFD59 /* PiPDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPDelegate.swift; sourceTree = ""; }; @@ -1158,6 +1247,8 @@ 37599F2F272B42810087F250 /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = ""; }; 37599F33272B44000087F250 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = ""; }; 37599F37272B4D740087F250 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = ""; }; + 375AC2992B66B7D600B680E7 /* ExportSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSettings.swift; sourceTree = ""; }; + 375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportSettingsModel.swift; sourceTree = ""; }; 375B537728DF6CBB004C1D19 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 375B537928DF6CC4004C1D19 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 375B8AB228B580D300397B31 /* KeychainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainModel.swift; sourceTree = ""; }; @@ -1269,6 +1360,25 @@ 37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettings.swift; sourceTree = ""; }; 37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaybackSettingsPresentationDetents+Backport.swift"; sourceTree = ""; }; 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlBackgroundModifier.swift; sourceTree = ""; }; + 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupImporter.swift; sourceTree = ""; }; + 37A7D6F22B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D6F62B68071C009CB1ED /* PlayerSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSettingsGroupImporter.swift; sourceTree = ""; }; + 37A7D6FA2B680822009CB1ED /* ControlsSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsSettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D6FE2B68082F009CB1ED /* ControlsSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsSettingsGroupImporter.swift; sourceTree = ""; }; + 37A7D7022B680A97009CB1ED /* QualitySettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D7062B680A9E009CB1ED /* QualitySettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettingsGroupImporter.swift; sourceTree = ""; }; + 37A7D70A2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D70E2B680CED009CB1ED /* HistorySettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySettingsGroupImporter.swift; sourceTree = ""; }; + 37A7D7122B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D7162B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSettingsGroupImporter.swift; sourceTree = ""; }; + 37A7D71A2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsSettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D71E2B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsSettingsGroupImporter.swift; sourceTree = ""; }; + 37A7D7222B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D7262B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsGroupImporter.swift; sourceTree = ""; }; + 37A7D72A2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherDataSettingsGroupExporter.swift; sourceTree = ""; }; + 37A7D72E2B681011009CB1ED /* OtherDataSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherDataSettingsGroupImporter.swift; sourceTree = ""; }; 37A81BF8294BD1440081D322 /* WatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchView.swift; sourceTree = ""; }; 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCells.swift; sourceTree = ""; }; 37A9965D26D6F9B9006E3224 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; @@ -1299,6 +1409,9 @@ 37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = ""; }; 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+FormatTests.swift"; sourceTree = ""; }; 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = ""; }; + 37BBB3392B6B9053001C4845 /* ImportSettingsSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsSheetViewModel.swift; sourceTree = ""; }; + 37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsSheetView.swift; sourceTree = ""; }; + 37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsAccountRow.swift; sourceTree = ""; }; 37BC50A72778A84700510953 /* HistorySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySettings.swift; sourceTree = ""; }; 37BC50AB2778BCBA00510953 /* HistoryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HistoryModel.swift; path = Model/HistoryModel.swift; sourceTree = SOURCE_ROOT; }; 37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -1373,6 +1486,8 @@ 37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStatusHeader.swift; sourceTree = ""; }; 37E70922271CD43000D34DDE /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = ""; }; 37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettingsButton.swift; sourceTree = ""; }; + 37E75CC62B6AEAF7003A6237 /* RecentlyOpenedImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyOpenedImporter.swift; sourceTree = ""; }; + 37E75CCA2B6AEB01003A6237 /* RecentlyOpenedExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyOpenedExporter.swift; sourceTree = ""; }; 37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsOverlay.swift; sourceTree = ""; }; 37E80F3F287B472300561799 /* ScrollContentBackground+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollContentBackground+Backport.swift"; sourceTree = ""; }; 37E868FD29AA400B003128D0 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; @@ -1780,9 +1895,11 @@ 376418892A6FE32D008DDCC1 /* AddPublicInstanceButton.swift */, 37F0F4ED286F734400C06C2E /* AdvancedSettings.swift */, 376BE50A27349108009AD608 /* BrowsingSettings.swift */, + 375AC2992B66B7D600B680E7 /* ExportSettings.swift */, 37579D5C27864F5F00FD0B98 /* Help.swift */, 37BC50A72778A84700510953 /* HistorySettings.swift */, 37FADFFF272ED58000330459 /* HomeSettings.swift */, + 37BBB33D2B6B9C80001C4845 /* Import */, 37484C2426FC83E000287258 /* InstanceForm.swift */, 37484C2C26FC844700287258 /* InstanceSettings.swift */, 374924D92921050B0017D862 /* LocationsSettings.swift */, @@ -1988,6 +2105,52 @@ path = iOS; sourceTree = ""; }; + 37A7D6E22B67E2EF009CB1ED /* Import Export Settings */ = { + isa = PBXGroup; + children = ( + 37A7D6F12B67E433009CB1ED /* Importers */, + 37A7D6F02B67E42D009CB1ED /* Exporters */, + 372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */, + 375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */, + ); + path = "Import Export Settings"; + sourceTree = ""; + }; + 37A7D6F02B67E42D009CB1ED /* Exporters */ = { + isa = PBXGroup; + children = ( + 37A7D7222B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift */, + 37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */, + 37A7D6FA2B680822009CB1ED /* ControlsSettingsGroupExporter.swift */, + 37A7D70A2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift */, + 37A7D71A2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift */, + 37A7D72A2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift */, + 37A7D6F22B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift */, + 37A7D7022B680A97009CB1ED /* QualitySettingsGroupExporter.swift */, + 37E75CCA2B6AEB01003A6237 /* RecentlyOpenedExporter.swift */, + 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */, + 37A7D7122B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift */, + ); + path = Exporters; + sourceTree = ""; + }; + 37A7D6F12B67E433009CB1ED /* Importers */ = { + isa = PBXGroup; + children = ( + 37A7D7262B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift */, + 37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */, + 37A7D6FE2B68082F009CB1ED /* ControlsSettingsGroupImporter.swift */, + 37A7D70E2B680CED009CB1ED /* HistorySettingsGroupImporter.swift */, + 37A7D71E2B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift */, + 37A7D72E2B681011009CB1ED /* OtherDataSettingsGroupImporter.swift */, + 37A7D6F62B68071C009CB1ED /* PlayerSettingsGroupImporter.swift */, + 37A7D7062B680A9E009CB1ED /* QualitySettingsGroupImporter.swift */, + 37E75CC62B6AEAF7003A6237 /* RecentlyOpenedImporter.swift */, + 37A7D7162B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift */, + ); + path = Importers; + sourceTree = ""; + }; 37BA796C26DC4105002A0235 /* Extensions */ = { isa = PBXGroup; children = ( @@ -1996,6 +2159,18 @@ path = Extensions; sourceTree = ""; }; + 37BBB33D2B6B9C80001C4845 /* Import */ = { + isa = PBXGroup; + children = ( + 37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */, + 372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */, + 37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */, + 37BBB3392B6B9053001C4845 /* ImportSettingsSheetViewModel.swift */, + 372C74652B67044300BE179B /* ImportSettingsSheetViewModifier.swift */, + ); + path = Import; + sourceTree = ""; + }; 37BDFF1829487B74000C6404 /* Channels */ = { isa = PBXGroup; children = ( @@ -2171,6 +2346,7 @@ 37D4B1B72672CFE300C925CA /* Model */ = { isa = PBXGroup; children = ( + 37A7D6E22B67E2EF009CB1ED /* Import Export Settings */, 3743B86627216A1E00261544 /* Accounts */, 3743B864272169E200261544 /* Applications */, 377F9F79294403DC0043F856 /* Cache */, @@ -2850,13 +3026,16 @@ 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37C2211D27ADA33300305B41 /* MPVViewController.swift in Sources */, 37A362BE29537AAA00BDF328 /* PlaybackSettings.swift in Sources */, + 37A7D6F72B68071C009CB1ED /* PlayerSettingsGroupImporter.swift in Sources */, 371B7E612759706A00D21217 /* CommentsView.swift in Sources */, 37D9BA0629507F69002586BD /* PlayerControlsSettings.swift in Sources */, + 37A7D7232B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift in Sources */, 3773B80A2ADC076800B5FEF3 /* RefreshControlModifier.swift in Sources */, 379DC3D128BA4EB400B09677 /* Seek.swift in Sources */, 371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */, 37E8B0F027B326F30024006F /* Comparable+Clamped.swift in Sources */, 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */, + 372C74632B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift in Sources */, 372CFD15285F2E2A00B0B54B /* ControlsBar.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */, @@ -2877,12 +3056,14 @@ 37D2E0D428B67EFC00F64D52 /* Delay.swift in Sources */, 3776925229463C310055EC18 /* PlaylistsCacheModel.swift in Sources */, 3759234628C26C7B00C052EC /* Refreshable+Backport.swift in Sources */, + 37A7D6E92B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */, 374924ED2921669B0017D862 /* PreferenceKeys.swift in Sources */, 37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */, 37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */, 3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 377FF88F291A99580028EB0B /* HistoryView.swift in Sources */, 3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */, + 37A7D7172B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift in Sources */, 378E50FF26FE8EEE00F49626 /* AccountViewButton.swift in Sources */, 374924F029216C630017D862 /* VideoActions.swift in Sources */, 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */, @@ -2897,6 +3078,7 @@ 374AB3D728BCAF0000DF56FB /* SeekModel.swift in Sources */, 37130A5F277657300033018A /* PersistenceController.swift in Sources */, 37FD43E32704847C0073EE42 /* View+Fixtures.swift in Sources */, + 37A7D6F32B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift in Sources */, 3776ADD6287381240078EBC4 /* Captions.swift in Sources */, 37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */, @@ -2918,6 +3100,7 @@ 37BDFF1F29488117000C6404 /* ChannelPlaylistListItem.swift in Sources */, 371CC76C29466F5A00979C1A /* AccountsViewModel.swift in Sources */, 37B4E805277D0AB4004BF56A /* Orientation.swift in Sources */, + 37E75CCB2B6AEB01003A6237 /* RecentlyOpenedExporter.swift in Sources */, 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */, 375EC95D289EEEE000751258 /* QualityProfile.swift in Sources */, @@ -2931,8 +3114,11 @@ 37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */, 376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 3722AEBC274DA396005EA4D6 /* Badge+Backport.swift in Sources */, + 37A7D71F2B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift in Sources */, 3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */, 37599F34272B44000087F250 /* FavoritesModel.swift in Sources */, + 37A7D6FF2B68082F009CB1ED /* ControlsSettingsGroupImporter.swift in Sources */, + 37BBB33F2B6B9D52001C4845 /* ImportSettingsSheetView.swift in Sources */, 3773B8152ADC081300B5FEF3 /* VisualEffectBlur-iOS.swift in Sources */, 3717407D2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, 379ACB512A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */, @@ -2959,12 +3145,16 @@ 37F13B62285E43C000B137E4 /* ControlsOverlay.swift in Sources */, 375E45F827B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */, 37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */, + 37A7D70F2B680CED009CB1ED /* HistorySettingsGroupImporter.swift in Sources */, + 37A7D6FB2B680822009CB1ED /* ControlsSettingsGroupExporter.swift in Sources */, 375168D62700FAFF008F96A6 /* Debounce.swift in Sources */, 37E64DD126D597EB00C71877 /* SubscribedChannelsModel.swift in Sources */, 37C89322294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */, 376578892685471400D4EA09 /* Playlist.swift in Sources */, + 37A7D7272B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift in Sources */, 37B4E803277D0A72004BF56A /* AppDelegate.swift in Sources */, 37FD77002932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */, + 375AC29E2B66BDD600B680E7 /* ImportExportSettingsModel.swift in Sources */, 373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */, 37772E0D2A216F8600608BD9 /* String+ReplacingHTMLEntities.swift in Sources */, 3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, @@ -3005,6 +3195,7 @@ 37E6D7A02944CD3800550C3D /* CacheStatusHeader.swift in Sources */, 374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 375F7410289DC35A00747050 /* PlayerBackendView.swift in Sources */, + 37A7D70B2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift in Sources */, 37FB28412721B22200A57617 /* ContentItem.swift in Sources */, 379F141F289ECE7F00DE48B5 /* QualitySettings.swift in Sources */, 378E9C4029455A5800B2D696 /* ChannelsView.swift in Sources */, @@ -3028,6 +3219,7 @@ 37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */, 3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */, 379775932689365600DD52A8 /* Array+Next.swift in Sources */, + 372C74662B67044300BE179B /* ImportSettingsSheetViewModifier.swift in Sources */, 37CFB48528AFE2510070024C /* VideoDescription.swift in Sources */, 3773B8042ADC076800B5FEF3 /* UIView+Extensions.swift in Sources */, 37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, @@ -3042,6 +3234,7 @@ 37EBD8CA27AF26C200F1C24B /* MPVBackend.swift in Sources */, 37635FE4291EA6CF00C11E79 /* AccentButton.swift in Sources */, 37DCD3152A18F7630059A470 /* SafeAreaModel.swift in Sources */, + 37A7D72B2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift in Sources */, 37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */, 37484C2526FC83E000287258 /* InstanceForm.swift in Sources */, 37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, @@ -3057,11 +3250,14 @@ 37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 377E17142928265900894889 /* ListRowSeparator+Backport.swift in Sources */, 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */, + 37A7D71B2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */, + 37A7D7032B680A97009CB1ED /* QualitySettingsGroupExporter.swift in Sources */, 37F7AB4D28A9361F00FB46B5 /* UIDevice+Cellular.swift in Sources */, 37141673267A8E10006CA35D /* Country.swift in Sources */, 37FEF11327EFD8580033912F /* PlaceholderCell.swift in Sources */, 37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */, 37A362C229537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift in Sources */, + 37BBB33A2B6B9053001C4845 /* ImportSettingsSheetViewModel.swift in Sources */, 3748186E26A769D60084E870 /* DetailBadge.swift in Sources */, 3744A96028B99ADD005DE0A7 /* PlayerControlsLayout.swift in Sources */, 376BE50B27349108009AD608 /* BrowsingSettings.swift in Sources */, @@ -3069,7 +3265,9 @@ 37F0F4EE286F734400C06C2E /* AdvancedSettings.swift in Sources */, 37AAF2A026741C97007FC770 /* FeedView.swift in Sources */, 374924E3292141320017D862 /* InspectorView.swift in Sources */, + 37A7D6ED2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */, 37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */, + 37A7D7072B680A9E009CB1ED /* QualitySettingsGroupImporter.swift in Sources */, 37599F30272B42810087F250 /* FavoriteItem.swift in Sources */, 374924E729215FB60017D862 /* TapRecognizerViewModifier.swift in Sources */, 3773B8072ADC076800B5FEF3 /* UIResponder+Extensions.swift in Sources */, @@ -3087,6 +3285,7 @@ 37D4B19726717E1500C925CA /* Video.swift in Sources */, 37484C2926FC83FF00287258 /* AccountForm.swift in Sources */, 37E70927271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, + 37A7D7132B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift in Sources */, 371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */, 37BA221129526A19000DAD1F /* ControlsGradientView.swift in Sources */, 377ABC44286E4B74009C986F /* ManifestedInstance.swift in Sources */, @@ -3094,9 +3293,13 @@ 375E45F527B1976B00BA7902 /* MPVOGLView.swift in Sources */, 37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 377692562946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */, + 37E75CC72B6AEAF7003A6237 /* RecentlyOpenedImporter.swift in Sources */, 3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */, + 375AC29A2B66B7D600B680E7 /* ExportSettings.swift in Sources */, 37B7CFE92A19603B001B0564 /* ToolbarBackground+Backport.swift in Sources */, 37DCD3172A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */, + 372C746A2B67098A00BE179B /* ImportSettingsFileModel.swift in Sources */, + 37A7D6E52B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */, 379EF9E029AA585F009FE6C6 /* HideShortsButtons.swift in Sources */, 37F5E8B6291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */, 37579D5D27864F5F00FD0B98 /* Help.swift in Sources */, @@ -3107,6 +3310,7 @@ 37001563271B1F250049C794 /* AccountsModel.swift in Sources */, 3795593627B08538007FF8F4 /* StreamControl.swift in Sources */, 37B7CFEB2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift in Sources */, + 37BBB3432B6BB88F001C4845 /* ImportSettingsAccountRow.swift in Sources */, 37A2B346294723850050933E /* CacheModel.swift in Sources */, 37CC3F50270D010D00608308 /* VideoBanner.swift in Sources */, 378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */, @@ -3127,6 +3331,7 @@ 377F9F7B294403F20043F856 /* VideosCacheModel.swift in Sources */, 37B795902771DAE0001CF27B /* OpenURLHandler.swift in Sources */, 37732FF02703A26300F04329 /* AccountValidationStatus.swift in Sources */, + 37A7D72F2B681011009CB1ED /* OtherDataSettingsGroupImporter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3151,6 +3356,8 @@ 371CC77129468BDC00979C1A /* SettingsButtons.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 3703100327B0713600ECDDAA /* PlayerGestures.swift in Sources */, + 37A7D7282B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift in Sources */, + 37A7D72C2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift in Sources */, 374C0540272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 3738535529451DC800D2D0CB /* BookmarksCacheModel.swift in Sources */, 379F1420289ECE7F00DE48B5 /* QualitySettings.swift in Sources */, @@ -3187,6 +3394,7 @@ 379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */, 37F5C7E12A1E2AF300927B73 /* ListView.swift in Sources */, 37192D5828B179D60012EEDD /* ChaptersView.swift in Sources */, + 37E75CCC2B6AEB01003A6237 /* RecentlyOpenedExporter.swift in Sources */, 3784CDE327772EE40055BBF2 /* Watch.swift in Sources */, 371AC0B7294D1D6E0085989E /* PlayingIndicatorView.swift in Sources */, 3773B8182ADC081300B5FEF3 /* VisualEffectBlur-macOS.swift in Sources */, @@ -3199,14 +3407,17 @@ 3756C2A72861131100E4B059 /* NetworkState.swift in Sources */, 37D6025A28C17375009E8D98 /* PlaybackStatsView.swift in Sources */, 3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */, + 37BBB3442B6BB88F001C4845 /* ImportSettingsAccountRow.swift in Sources */, 3763C98A290C7A50004D3B5F /* OpenVideosView.swift in Sources */, 37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 371CC7752946963000979C1A /* ListingStyleButtons.swift in Sources */, 374AB3DC28BCAF7E00DF56FB /* SeekType.swift in Sources */, 376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */, + 37E75CC82B6AEAF7003A6237 /* RecentlyOpenedImporter.swift in Sources */, 37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */, 37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */, 3752069A285E8DD300CA655F /* Chapter.swift in Sources */, + 37A7D6EE2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */, 373EBD69291F252D002ADB9C /* HomeSettings.swift in Sources */, 37B7CFEE2A19789F001B0564 /* MacOSPiPDelegate.swift in Sources */, 37484C1A26FC837400287258 /* PlayerSettings.swift in Sources */, @@ -3215,14 +3426,17 @@ 37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */, 37D9BA0729507F69002586BD /* PlayerControlsSettings.swift in Sources */, 378E9C4129455A5800B2D696 /* ChannelsView.swift in Sources */, + 375AC29B2B66B7D600B680E7 /* ExportSettings.swift in Sources */, 378AE944274EF00A006A4EE1 /* Color+Background.swift in Sources */, 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */, 3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 377FF88C291A60310028EB0B /* OpenVideosModel.swift in Sources */, 378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */, + 37A7D6F42B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift in Sources */, 3773B8162ADC081300B5FEF3 /* VisualEffectBlur-iOS.swift in Sources */, 37599F35272B44000087F250 /* FavoritesModel.swift in Sources */, + 372C74672B67044300BE179B /* ImportSettingsSheetViewModifier.swift in Sources */, 376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */, @@ -3239,9 +3453,11 @@ 378E510026FE8EEE00F49626 /* AccountViewButton.swift in Sources */, 370F4FA927CC163A001B35DC /* PlayerBackend.swift in Sources */, 37141670267A8ACC006CA35D /* TrendingView.swift in Sources */, + 37A7D7002B68082F009CB1ED /* ControlsSettingsGroupImporter.swift in Sources */, 379DC3D228BA4EB400B09677 /* Seek.swift in Sources */, 376BE50727347B57009AD608 /* SettingsHeader.swift in Sources */, 378AE93C274EDFB2006A4EE1 /* Backport.swift in Sources */, + 37BBB3402B6B9D52001C4845 /* ImportSettingsSheetView.swift in Sources */, 37A2B347294723850050933E /* CacheModel.swift in Sources */, 37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */, 37F4AD2028612DFD004D0F66 /* Buffering.swift in Sources */, @@ -3276,6 +3492,7 @@ 373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */, 37AAF29126740715007FC770 /* Channel.swift in Sources */, 37F4AD1C28612B23004D0F66 /* OpeningStream.swift in Sources */, + 37A7D6EA2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */, 376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */, 37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, 37579D5E27864F5F00FD0B98 /* Help.swift in Sources */, @@ -3286,18 +3503,23 @@ 377E17152928265900894889 /* ListRowSeparator+Backport.swift in Sources */, 371CC76929466ED000979C1A /* AccountsView.swift in Sources */, 37C3A242272359900087A57A /* Double+Format.swift in Sources */, + 37A7D7302B681011009CB1ED /* OtherDataSettingsGroupImporter.swift in Sources */, + 37A7D6FC2B680822009CB1ED /* ControlsSettingsGroupExporter.swift in Sources */, 37B795912771DAE0001CF27B /* OpenURLHandler.swift in Sources */, 37EFAC0928C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */, 37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 376578922685490700D4EA09 /* PlaylistsView.swift in Sources */, 37484C2626FC83E000287258 /* InstanceForm.swift in Sources */, 3751BA7E27E63F1D007B1A60 /* MPVOGLView.swift in Sources */, + 37BBB33B2B6B9053001C4845 /* ImportSettingsSheetViewModel.swift in Sources */, 377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */, 37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, + 37A7D6E62B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */, 377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */, 376A33E52720CB35000C1D6B /* Account.swift in Sources */, 376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 37A9965F26D6F9B9006E3224 /* HomeView.swift in Sources */, + 37A7D7142B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift in Sources */, 37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */, 37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */, @@ -3306,14 +3528,18 @@ 37C8E702294FC97D00EEAB14 /* QueueView.swift in Sources */, 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 379ACB522A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */, + 37A7D7082B680A9E009CB1ED /* QualitySettingsGroupImporter.swift in Sources */, 37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 37E6D79D2944AE1A00550C3D /* FeedModel.swift in Sources */, + 375AC29F2B66BDD600B680E7 /* ImportExportSettingsModel.swift in Sources */, 37732FF52703D32400F04329 /* Sidebar.swift in Sources */, 379775942689365600DD52A8 /* Array+Next.swift in Sources */, + 37A7D7102B680CED009CB1ED /* HistorySettingsGroupImporter.swift in Sources */, 377ABC49286E5887009C986F /* Sequence+Unique.swift in Sources */, 3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */, 3784B23E2728B85300B09468 /* ShareButton.swift in Sources */, 375F7411289DC35A00747050 /* PlayerBackendView.swift in Sources */, + 37A7D70C2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift in Sources */, 37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */, 37E64DD226D597EB00C71877 /* SubscribedChannelsModel.swift in Sources */, 37F0F4EF286F734400C06C2E /* AdvancedSettings.swift in Sources */, @@ -3330,7 +3556,9 @@ 3703100027B04DCC00ECDDAA /* PlayerControls.swift in Sources */, 37130A5C277657090033018A /* Yattee.xcdatamodeld in Sources */, 37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */, + 37A7D7242B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift in Sources */, 37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */, + 372C74642B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift in Sources */, 37AAF2A126741C97007FC770 /* FeedView.swift in Sources */, 37F4AD2728613B81004D0F66 /* Color+Debug.swift in Sources */, 37732FF12703A26300F04329 /* AccountValidationStatus.swift in Sources */, @@ -3346,6 +3574,7 @@ 37D4B19826717E1500C925CA /* Video.swift in Sources */, 371B7E5D27596B8400D21217 /* Comment.swift in Sources */, 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */, + 37A7D71C2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */, 37BC50A92778A84700510953 /* HistorySettings.swift in Sources */, 374DE88128BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */, 37599F31272B42810087F250 /* FavoriteItem.swift in Sources */, @@ -3353,9 +3582,11 @@ 37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */, 377F9F7C294403F20043F856 /* VideosCacheModel.swift in Sources */, 374924E4292141320017D862 /* InspectorView.swift in Sources */, + 37A7D7202B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift in Sources */, 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, 37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */, 3764188B2A6FE32D008DDCC1 /* AddPublicInstanceButton.swift in Sources */, + 37A7D7182B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift in Sources */, 377F9F802944175F0043F856 /* FeedCacheModel.swift in Sources */, 373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */, 37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */, @@ -3367,8 +3598,10 @@ 37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37BDFF2029488117000C6404 /* ChannelPlaylistListItem.swift in Sources */, 3711404026B206A6005B3555 /* SearchModel.swift in Sources */, + 37A7D7042B680A97009CB1ED /* QualitySettingsGroupExporter.swift in Sources */, 37484C2A26FC83FF00287258 /* AccountForm.swift in Sources */, 37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, + 372C746B2B67098A00BE179B /* ImportSettingsFileModel.swift in Sources */, 373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 37D2E0D528B67EFC00F64D52 /* Delay.swift in Sources */, 37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */, @@ -3383,6 +3616,7 @@ 3743B86927216D3600261544 /* ChannelCell.swift in Sources */, 3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, 3782B95E2755858100990149 /* NSTextField+FocusRingType.swift in Sources */, + 37A7D6F82B68071C009CB1ED /* PlayerSettingsGroupImporter.swift in Sources */, 37C3A252272366440087A57A /* ChannelPlaylistView.swift in Sources */, 3754B01628B7F84D009717C8 /* Constants.swift in Sources */, 37270F1D28E06E3E00856150 /* String+Localizable.swift in Sources */, @@ -3470,10 +3704,12 @@ 37579D5F27864F5F00FD0B98 /* Help.swift in Sources */, 370015AB28BBAE7F000149FD /* ProgressBar.swift in Sources */, 375EC95F289EEEE000751258 /* QualityProfile.swift in Sources */, + 37A7D7112B680CED009CB1ED /* HistorySettingsGroupImporter.swift in Sources */, 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */, 373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */, 375EC974289F2ABF00751258 /* MultiselectRow.swift in Sources */, 37EBD8C827AF26B300F1C24B /* AVPlayerBackend.swift in Sources */, + 37BBB3452B6BB88F001C4845 /* ImportSettingsAccountRow.swift in Sources */, 378E9C3E2945565500B2D696 /* SubscriptionsView.swift in Sources */, 37EFAC0A28C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */, 37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */, @@ -3496,11 +3732,15 @@ 376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 37BDFF1D29487C5A000C6404 /* ChannelListItem.swift in Sources */, 37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */, + 37A7D6E32B67E303009CB1ED /* ImportSettingsFileModel.swift in Sources */, + 37A7D7152B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift in Sources */, + 37BBB33C2B6B9053001C4845 /* ImportSettingsSheetViewModel.swift in Sources */, 3748187026A769D60084E870 /* DetailBadge.swift in Sources */, 3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */, 371B7E632759706A00D21217 /* CommentsView.swift in Sources */, 37D6025D28C17719009E8D98 /* ControlsOverlayButton.swift in Sources */, 37D2E0D628B67EFC00F64D52 /* Delay.swift in Sources */, + 37A7D70D2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift in Sources */, 379F1421289ECE7F00DE48B5 /* QualitySettings.swift in Sources */, 37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 371CC76A29466ED000979C1A /* AccountsView.swift in Sources */, @@ -3511,6 +3751,8 @@ 37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */, 37BDFF2129488117000C6404 /* ChannelPlaylistListItem.swift in Sources */, 37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */, + 37A7D6E72B67E315009CB1ED /* SettingsGroupExporter.swift in Sources */, + 375AC2A02B66BDD600B680E7 /* ImportExportSettingsModel.swift in Sources */, 37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */, 376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */, 378FFBC628660172009E3FBE /* URLParser.swift in Sources */, @@ -3521,6 +3763,8 @@ 3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */, 3718B9A52921A97F0003DB2E /* InspectorView.swift in Sources */, + 37A7D72D2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift in Sources */, + 372C74682B67044900BE179B /* ImportSettingsSheetViewModifier.swift in Sources */, 37E70925271CD43000D34DDE /* WelcomeScreen.swift in Sources */, 376BE50D27349108009AD608 /* BrowsingSettings.swift in Sources */, 37CFB48728AFE2510070024C /* VideoDescription.swift in Sources */, @@ -3534,12 +3778,14 @@ 37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 371CC77229468BDC00979C1A /* SettingsButtons.swift in Sources */, 37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, + 375AC29C2B66B7D600B680E7 /* ExportSettings.swift in Sources */, 3718B9A12921A9640003DB2E /* VideoDetails.swift in Sources */, 378AE93D274EDFB3006A4EE1 /* Backport.swift in Sources */, 377F9F812944175F0043F856 /* FeedCacheModel.swift in Sources */, 37130A5D277657090033018A /* Yattee.xcdatamodeld in Sources */, 37C3A243272359900087A57A /* Double+Format.swift in Sources */, 37AAF29226740715007FC770 /* Channel.swift in Sources */, + 37A7D7092B680A9E009CB1ED /* QualitySettingsGroupImporter.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 376527BD285F60F700102284 /* PlayerTimeModel.swift in Sources */, 377692582946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */, @@ -3562,9 +3808,11 @@ 3765788B2685471400D4EA09 /* Playlist.swift in Sources */, 376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */, 373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */, + 37A7D7012B68082F009CB1ED /* ControlsSettingsGroupImporter.swift in Sources */, 376B0562293FF45F0062AC78 /* PeerTubeAPI.swift in Sources */, 3738535629451DC800D2D0CB /* BookmarksCacheModel.swift in Sources */, 37E64DD326D597EB00C71877 /* SubscribedChannelsModel.swift in Sources */, + 37A7D6F92B68071C009CB1ED /* PlayerSettingsGroupImporter.swift in Sources */, 3752069B285E8DD300CA655F /* Chapter.swift in Sources */, 37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */, 3743B86A27216D3600261544 /* ChannelCell.swift in Sources */, @@ -3576,12 +3824,14 @@ 37192D5928B179D60012EEDD /* ChaptersView.swift in Sources */, 37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, 373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, + 37A7D6FD2B680822009CB1ED /* ControlsSettingsGroupExporter.swift in Sources */, 37C89324294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */, 3784CDE427772EE40055BBF2 /* Watch.swift in Sources */, 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */, 37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */, 375E45F627B1976B00BA7902 /* MPVOGLView.swift in Sources */, + 37A7D7052B680A97009CB1ED /* QualitySettingsGroupExporter.swift in Sources */, 375EC95B289EEB8200751258 /* QualityProfileForm.swift in Sources */, 371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */, 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, @@ -3594,6 +3844,7 @@ 372820402945E4A8009A0E2D /* SubscriptionsPageButton.swift in Sources */, 37001565271B1F250049C794 /* AccountsModel.swift in Sources */, 3751B4B427836902000B7DF4 /* SearchPage.swift in Sources */, + 37A7D7252B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift in Sources */, 377ABC46286E4B74009C986F /* ManifestedInstance.swift in Sources */, 37BA221329526A19000DAD1F /* ControlsGradientView.swift in Sources */, 374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, @@ -3609,6 +3860,7 @@ 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, 3756C2A82861131100E4B059 /* NetworkState.swift in Sources */, 376578932685490700D4EA09 /* PlaylistsView.swift in Sources */, + 37E75CCD2B6AEB01003A6237 /* RecentlyOpenedExporter.swift in Sources */, 377FF891291A99580028EB0B /* HistoryView.swift in Sources */, 37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */, 37001561271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, @@ -3624,6 +3876,7 @@ 37C3A253272366440087A57A /* ChannelPlaylistView.swift in Sources */, 378AE945274EF00A006A4EE1 /* Color+Background.swift in Sources */, 3743CA54270F284F00E4D32B /* View+Borders.swift in Sources */, + 37A7D6F52B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift in Sources */, 371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */, 37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */, 37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, @@ -3673,6 +3926,7 @@ 37141675267A8E10006CA35D /* Country.swift in Sources */, 370F500C27CC1821001B35DC /* MPVViewController.swift in Sources */, 3782B9542755667600990149 /* String+Format.swift in Sources */, + 37E75CC92B6AEAF7003A6237 /* RecentlyOpenedImporter.swift in Sources */, 3764188C2A6FE32D008DDCC1 /* AddPublicInstanceButton.swift in Sources */, 37D836BE294927E700005E5E /* ChannelsCacheModel.swift in Sources */, 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, @@ -3680,11 +3934,14 @@ 37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */, 379ACB4E2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, + 37A7D7292B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift in Sources */, 37F5C7E22A1E2AF300927B73 /* ListView.swift in Sources */, + 37BBB3412B6B9D52001C4845 /* ImportSettingsSheetView.swift in Sources */, 37E6D7A22944CD3800550C3D /* CacheStatusHeader.swift in Sources */, 37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, 37F0F4F0286F734400C06C2E /* AdvancedSettings.swift in Sources */, 373197DA2732060100EF734F /* RelatedView.swift in Sources */, + 37A7D71D2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */, 37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */, 377ABC4A286E5887009C986F /* Sequence+Unique.swift in Sources */, 37E6D79E2944AE1A00550C3D /* FeedModel.swift in Sources */, @@ -3701,6 +3958,7 @@ 37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */, 371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */, 374AB3D928BCAF0000DF56FB /* SeekModel.swift in Sources */, + 37A7D7212B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift in Sources */, 375E45F927B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */, 371CC7762946963000979C1A /* ListingStyleButtons.swift in Sources */, 3782B95627557E4E00990149 /* SearchView.swift in Sources */, @@ -3709,15 +3967,19 @@ 3718B9A02921A9620003DB2E /* VideoDetailsOverlay.swift in Sources */, 377E17162928265900894889 /* ListRowSeparator+Backport.swift in Sources */, 37FB28432721B22200A57617 /* ContentItem.swift in Sources */, + 37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */, 37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */, + 37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */, 37AAF2A226741C97007FC770 /* FeedView.swift in Sources */, 37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */, 3773B8122ADC076800B5FEF3 /* ScrollViewMatcher.swift in Sources */, 372915E82687E3B900F5A35B /* Defaults.swift in Sources */, 37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */, 3718B9A62921A9BE0003DB2E /* PreferenceKeys.swift in Sources */, + 37A7D7192B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift in Sources */, 3797758D2689345500DD52A8 /* Store.swift in Sources */, 37484C2F26FC844700287258 /* InstanceSettings.swift in Sources */, + 37A7D7312B681011009CB1ED /* OtherDataSettingsGroupImporter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iOS/Info.plist b/iOS/Info.plist index d8b5000e..6857256a 100644 --- a/iOS/Info.plist +++ b/iOS/Info.plist @@ -34,6 +34,16 @@ public.file-url + + CFBundleTypeName + Settings text + LSHandlerRank + Default + LSItemContentTypes + + public.json + + CFBundleURLTypes @@ -68,5 +78,31 @@ UIFileSharingEnabled + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.json + + UTTypeDescription + Yattee Settings + UTTypeIconFiles + + UTTypeIdentifier + stream.yattee.app-settings + UTTypeTagSpecification + + public.filename-extension + + yatteesettings + + public.mime-type + + application/json + + + + diff --git a/macOS/Info.plist b/macOS/Info.plist index e040d760..ed5138fa 100644 --- a/macOS/Info.plist +++ b/macOS/Info.plist @@ -16,6 +16,18 @@ public.mpeg-4 + + CFBundleTypeName + Settings + CFBundleTypeRole + Editor + LSHandlerRank + Default + LSItemContentTypes + + public.json + + CFBundleURLTypes @@ -37,5 +49,51 @@ NSAllowsArbitraryLoads + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.json + + UTTypeDescription + Yattee Settings + UTTypeIcons + + UTTypeIdentifier + stream.yattee.app-settings + UTTypeTagSpecification + + public.filename-extension + + yatteesettings + + public.mime-type + + + + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.json + + UTTypeDescription + Yattee Settings + UTTypeIcons + + UTTypeIdentifier + stream.yattee.app-settings + UTTypeTagSpecification + + public.filename-extension + + yatteesettings + + + +