Import export settings

This commit is contained in:
Arkadiusz Fal 2024-02-01 23:54:16 +01:00
parent 9826ee4d36
commit 2be6f04e37
46 changed files with 2801 additions and 169 deletions

View File

@ -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)

View File

@ -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 }
}
}

View File

@ -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]

View File

@ -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)
}
}
}

View File

@ -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]
]
}
}

View File

@ -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
}
}

View File

@ -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]
]
}
}

View File

@ -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]
]
}
}

View File

@ -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
}
}

View File

@ -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]
]
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,11 @@
import Defaults
import SwiftyJSON
final class SponsorBlockSettingsGroupExporter: SettingsGroupExporter {
override var globalJSON: JSON {
[
"sponsorBlockInstance": Defaults[.sponsorBlockInstance],
"sponsorBlockCategories": Array(Defaults[.sponsorBlockCategories])
]
}
}

View File

@ -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<ExportGroup>()
static var defaultExportGroups = Set<ExportGroup>([
.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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,84 @@
import Defaults
import SwiftyJSON
struct LocationsSettingsGroupImporter {
var json: JSON
var includePublicLocations = true
var includedInstancesIDs = Set<Instance.ID>()
var includedAccountsIDs = Set<Account.ID>()
var includedAccountsPasswords = [Account.ID: String]()
init(
json: JSON,
includePublicLocations: Bool = true,
includedInstancesIDs: Set<Instance.ID> = [],
includedAccountsIDs: Set<Account.ID> = [],
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)
}
}
}
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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 })
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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]

View File

@ -6,37 +6,22 @@ import SwiftUI
#endif
extension Defaults.Keys {
static let instancesManifest = Key<String>("instancesManifest", default: "")
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
static let instances = Key<[Instance]>("instances", default: [])
static let accounts = Key<[Account]>("accounts", default: [])
static let lastAccountID = Key<Account.ID?>("lastAccountID")
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)
static let sponsorBlockInstance = Key<String>("sponsorBlockInstance", default: "https://sponsor.ajay.app")
static let sponsorBlockCategories = Key<Set<String>>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories))
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
// MARK: GROUP - Browsing
static let showHome = Key<Bool>("showHome", default: true)
static let showOpenActionsInHome = Key<Bool>("showOpenActionsInHome", default: true)
static let showQueueInHome = Key<Bool>("showQueueInHome", default: true)
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
static let showFavoritesInHome = Key<Bool>("showFavoritesInHome", default: true)
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: [])
static let startupSection = Key<StartupSection>("startupSection", default: .home)
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.subscriptions, .trending, .playlists])
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
#if os(iOS)
static let showDocuments = Key<Bool>("showDocuments", default: false)
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
#endif
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10)
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
#if !os(tvOS)
#if os(macOS)
@ -46,21 +31,146 @@ extension Defaults.Keys {
#endif
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
#endif
static let accountPickerDisplaysAnonymousAccounts = Key<Bool>("accountPickerDisplaysAnonymousAccounts", default: true)
#if os(iOS)
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
#endif
static let showUnwatchedFeedBadges = Key<Bool>("showUnwatchedFeedBadges", default: false)
static let keepChannelsWithUnwatchedFeedOnTop = Key<Bool>("keepChannelsWithUnwatchedFeedOnTop", default: true)
static let showToggleWatchedStatusButton = Key<Bool>("showToggleWatchedStatusButton", default: false)
static let expandChannelDescription = Key<Bool>("expandChannelDescription", default: false)
static let keepChannelsWithUnwatchedFeedOnTop = Key<Bool>("keepChannelsWithUnwatchedFeedOnTop", default: true)
static let showChannelAvatarInChannelsLists = Key<Bool>("showChannelAvatarInChannelsLists", default: true)
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
static let roundedThumbnails = Key<Bool>("roundedThumbnails", default: true)
static let thumbnailsQuality = Key<ThumbnailsQuality>("thumbnailsQuality", default: .highest)
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
// MARK: GROUP - Player
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
#if os(tvOS)
static let pauseOnHidingPlayerDefault = true
#else
static let pauseOnHidingPlayerDefault = false
#endif
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault)
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
#if !os(macOS)
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
#endif
#if os(iOS)
static let expandVideoDescriptionDefault = Constants.isIPad
#else
static let expandVideoDescriptionDefault = true
#endif
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
static let collapsedLinesDescription = Key<Int>("collapsedLinesDescription", default: 5)
static let showChapters = Key<Bool>("showChapters", default: true)
static let expandChapters = Key<Bool>("expandChapters", default: true)
static let showRelated = Key<Bool>("showRelated", default: true)
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: .defaultValue)
static let showKeywords = Key<Bool>("showKeywords", default: false)
#if !os(tvOS)
static let showScrollToTopInComments = Key<Bool>("showScrollToTopInComments", default: true)
#endif
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
#if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
"rotateToLandscapeOnEnterFullScreen",
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
)
#endif
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
static let closePlayerOnOpeningPiP = Key<Bool>("closePlayerOnOpeningPiP", default: false)
#if !os(macOS)
static let closePiPAndOpenPlayerOnEnteringForeground = Key<Bool>("closePiPAndOpenPlayerOnEnteringForeground", default: false)
#endif
// MARK: GROUP - Controls
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
static let seekGestureSpeed = Key<Double>("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>("playerControlsLayout", default: playerControlsLayoutDefault)
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
static let buttonBackwardSeekDuration = Key<String>("buttonBackwardSeekDuration", default: "10")
static let buttonForwardSeekDuration = Key<String>("buttonForwardSeekDuration", default: "10")
static let gestureBackwardSeekDuration = Key<String>("gestureBackwardSeekDuration", default: "10")
static let gestureForwardSeekDuration = Key<String>("gestureForwardSeekDuration", default: "10")
static let systemControlsSeekDuration = Key<String>("systemControlsBackwardSeekDuration", default: "10")
#if os(iOS)
static let playerControlsLockOrientationEnabled = Key<Bool>("playerControlsLockOrientationEnabled", default: true)
#endif
#if os(tvOS)
static let playerControlsSettingsEnabledDefault = true
#else
static let playerControlsSettingsEnabledDefault = false
#endif
static let playerControlsSettingsEnabled = Key<Bool>("playerControlsSettingsEnabled", default: playerControlsSettingsEnabledDefault)
static let playerControlsCloseEnabled = Key<Bool>("playerControlsCloseEnabled", default: true)
static let playerControlsRestartEnabled = Key<Bool>("playerControlsRestartEnabled", default: false)
static let playerControlsAdvanceToNextEnabled = Key<Bool>("playerControlsAdvanceToNextEnabled", default: false)
static let playerControlsPlaybackModeEnabled = Key<Bool>("playerControlsPlaybackModeEnabled", default: false)
static let playerControlsMusicModeEnabled = Key<Bool>("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<ButtonLabelStyle>("playerActionsButtonLabelStyle", default: playerDetailsPageButtonLabelStyleDefault)
static let actionButtonShareEnabled = Key<Bool>("actionButtonShareEnabled", default: true)
static let actionButtonAddToPlaylistEnabled = Key<Bool>("actionButtonAddToPlaylistEnabled", default: true)
static let actionButtonSubscribeEnabled = Key<Bool>("actionButtonSubscribeEnabled", default: false)
static let actionButtonSettingsEnabled = Key<Bool>("actionButtonSettingsEnabled", default: true)
static let actionButtonHideEnabled = Key<Bool>("actionButtonHideEnabled", default: false)
static let actionButtonCloseEnabled = Key<Bool>("actionButtonCloseEnabled", default: true)
static let actionButtonFullScreenEnabled = Key<Bool>("actionButtonFullScreenEnabled", default: false)
static let actionButtonPipEnabled = Key<Bool>("actionButtonPipEnabled", default: false)
static let actionButtonLockOrientationEnabled = Key<Bool>("actionButtonLockOrientationEnabled", default: false)
static let actionButtonRestartEnabled = Key<Bool>("actionButtonRestartEnabled", default: false)
static let actionButtonAdvanceToNextItemEnabled = Key<Bool>("actionButtonAdvanceToNextItemEnabled", default: false)
static let actionButtonMusicModeEnabled = Key<Bool>("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<Double>("playerRate", default: 1.0)
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: batteryCellularProfileDefault)
static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: batteryNonCellularProfileDefault)
static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: chargingCellularProfileDefault)
static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: chargingNonCellularProfileDefault)
static let forceAVPlayerForLiveStreams = Key<Bool>("forceAVPlayerForLiveStreams", default: true)
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: .defaultValue)
static let playerInstanceID = Key<Instance.ID?>("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>("playerControlsLayout", default: playerControlsLayoutDefault)
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
static let showKeywords = Key<Bool>("showKeywords", default: false)
#if !os(tvOS)
static let showScrollToTopInComments = Key<Bool>("showScrollToTopInComments", default: true)
#endif
#if os(iOS)
static let expandVideoDescriptionDefault = Constants.isIPad
#else
static let expandVideoDescriptionDefault = true
#endif
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
static let collapsedLinesDescription = Key<Int>("collapsedLinesDescription", default: 5)
static let showChannelAvatarInChannelsLists = Key<Bool>("showChannelAvatarInChannelsLists", default: true)
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
#if os(tvOS)
static let pauseOnHidingPlayerDefault = true
#else
static let pauseOnHidingPlayerDefault = false
#endif
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault)
#if !os(macOS)
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
#endif
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
#if !os(macOS)
static let closePiPAndOpenPlayerOnEnteringForeground = Key<Bool>("closePiPAndOpenPlayerOnEnteringForeground", default: false)
#endif
static let closePlayerOnOpeningPiP = Key<Bool>("closePlayerOnOpeningPiP", default: false)
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
static let saveLastPlayed = Key<Bool>("saveLastPlayed", default: false)
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
static let playbackMode = Key<PlayerModel.PlaybackMode>("playbackMode", default: .queue)
// MARK: GROUP - History
static let saveRecents = Key<Bool>("saveRecents", default: true)
static let saveHistory = Key<Bool>("saveHistory", default: true)
static let showWatchingProgress = Key<Bool>("showWatchingProgress", default: true)
static let saveLastPlayed = Key<Bool>("saveLastPlayed", default: false)
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
static let watchedThreshold = Key<Int>("watchedThreshold", default: 90)
static let resetWatchedStatusOnPlaying = Key<Bool>("resetWatchedStatusOnPlaying", default: false)
static let watchedVideoStyle = Key<WatchedVideoStyle>("watchedVideoStyle", default: .badge)
static let watchedVideoBadgeColor = Key<WatchedVideoBadgeColor>("WatchedVideoBadgeColor", default: .red)
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
static let resetWatchedStatusOnPlaying = Key<Bool>("resetWatchedStatusOnPlaying", default: false)
static let saveRecents = Key<Bool>("saveRecents", default: true)
static let showToggleWatchedStatusButton = Key<Bool>("showToggleWatchedStatusButton", default: false)
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
// MARK: GROUP - SponsorBlock
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.subscriptions, .trending, .playlists])
static let startupSection = Key<StartupSection>("startupSection", default: .home)
static let sponsorBlockInstance = Key<String>("sponsorBlockInstance", default: "https://sponsor.ajay.app")
static let sponsorBlockCategories = Key<Set<String>>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories))
#if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
"rotateToLandscapeOnEnterFullScreen",
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
)
#endif
// MARK: GROUP - Locations
static let instancesManifest = Key<String>("instancesManifest", default: "")
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
static let instances = Key<[Instance]>("instances", default: [])
static let accounts = Key<[Account]>("accounts", default: [])
// MARK: Group - Advanced
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
static let showPlayNowInBackendContextMenu = Key<Bool>("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<ButtonLabelStyle>("playerActionsButtonLabelStyle", default: .iconAndText)
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
static let buttonBackwardSeekDuration = Key<String>("buttonBackwardSeekDuration", default: "10")
static let buttonForwardSeekDuration = Key<String>("buttonForwardSeekDuration", default: "10")
static let gestureBackwardSeekDuration = Key<String>("gestureBackwardSeekDuration", default: "10")
static let gestureForwardSeekDuration = Key<String>("gestureForwardSeekDuration", default: "10")
static let systemControlsSeekDuration = Key<String>("systemControlsBackwardSeekDuration", default: "10")
static let actionButtonShareEnabled = Key<Bool>("actionButtonShareEnabled", default: true)
static let actionButtonAddToPlaylistEnabled = Key<Bool>("actionButtonAddToPlaylistEnabled", default: true)
static let actionButtonSubscribeEnabled = Key<Bool>("actionButtonSubscribeEnabled", default: false)
static let actionButtonSettingsEnabled = Key<Bool>("actionButtonSettingsEnabled", default: true)
static let actionButtonHideEnabled = Key<Bool>("actionButtonHideEnabled", default: false)
static let actionButtonCloseEnabled = Key<Bool>("actionButtonCloseEnabled", default: true)
static let actionButtonFullScreenEnabled = Key<Bool>("actionButtonFullScreenEnabled", default: false)
static let actionButtonPipEnabled = Key<Bool>("actionButtonPipEnabled", default: false)
static let actionButtonLockOrientationEnabled = Key<Bool>("actionButtonLockOrientationEnabled", default: false)
static let actionButtonRestartEnabled = Key<Bool>("actionButtonRestartEnabled", default: false)
static let actionButtonAdvanceToNextItemEnabled = Key<Bool>("actionButtonAdvanceToNextItemEnabled", default: false)
static let actionButtonMusicModeEnabled = Key<Bool>("actionButtonMusicModeEnabled", default: true)
#if os(iOS)
static let playerControlsLockOrientationEnabled = Key<Bool>("playerControlsLockOrientationEnabled", default: true)
#endif
#if os(tvOS)
static let playerControlsSettingsEnabledDefault = true
#else
static let playerControlsSettingsEnabledDefault = false
#endif
static let playerControlsSettingsEnabled = Key<Bool>("playerControlsSettingsEnabled", default: playerControlsSettingsEnabledDefault)
static let playerControlsCloseEnabled = Key<Bool>("playerControlsCloseEnabled", default: true)
static let playerControlsRestartEnabled = Key<Bool>("playerControlsRestartEnabled", default: false)
static let playerControlsAdvanceToNextEnabled = Key<Bool>("playerControlsAdvanceToNextEnabled", default: false)
static let playerControlsPlaybackModeEnabled = Key<Bool>("playerControlsPlaybackModeEnabled", default: false)
static let playerControlsMusicModeEnabled = Key<Bool>("playerControlsMusicModeEnabled", default: false)
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "120")
static let mpvCachePauseWait = Key<String>("mpvCachePauseWait", default: "3")
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
static let showCacheStatus = Key<Bool>("showCacheStatus", default: false)
static let feedCacheSize = Key<String>("feedCacheSize", default: "50")
// MARK: GROUP - Other exportable
static let lastAccountID = Key<Account.ID?>("lastAccountID")
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
static let playerRate = Key<Double>("playerRate", default: 1.0)
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
static let subscriptionsViewPage = Key<SubscriptionsView.Page>("subscriptionsViewPage", default: .feed)
static let subscriptionsListingStyle = Key<ListingStyle>("subscriptionsListingStyle", default: .cells)
@ -263,11 +289,22 @@ extension Defaults.Keys {
static let searchListingStyle = Key<ListingStyle>("searchListingStyle", default: .cells)
static let hideShorts = Key<Bool>("hideShorts", default: false)
static let hideWatched = Key<Bool>("hideWatched", default: false)
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
static let showChapters = Key<Bool>("showChapters", default: true)
static let expandChapters = Key<Bool>("expandChapters", default: true)
static let showRelated = Key<Bool>("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<PlayerModel.PlaybackMode>("playbackMode", default: .queue)
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)
// MARK: LEGACY
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10)
}
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {

View File

@ -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

View File

@ -68,6 +68,7 @@ struct ContentView: View {
SettingsView()
}
)
.modifier(ImportSettingsSheetViewModifier(isPresented: $navigation.presentingSettingsImportSheet, settingsFile: $navigation.settingsImportURL))
.background(
EmptyView().sheet(isPresented: $navigation.presentingAccounts) {
AccountsView()

View File

@ -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

View File

@ -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()

View File

@ -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()
}
}

View File

@ -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
)
}
}

View File

@ -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")
}
}
}
}

View File

@ -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")!))
}

View File

@ -0,0 +1,77 @@
import Foundation
import SwiftUI
class ImportSettingsSheetViewModel: ObservableObject {
static let shared = ImportSettingsSheetViewModel()
@Published var selectedInstances = Set<Instance.ID>()
@Published var selectedAccounts = Set<Account.ID>()
@Published var importableAccounts = Set<Account.ID>()
@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
}
}

View File

@ -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")!)
)
)
}

View File

@ -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(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
}

View File

@ -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)
}

View File

@ -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 = "<group>"; };
3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = "<group>"; };
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsFileImporterViewModifier.swift; sourceTree = "<group>"; };
372C74652B67044300BE179B /* ImportSettingsSheetViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsSheetViewModifier.swift; sourceTree = "<group>"; };
372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsFileModel.swift; sourceTree = "<group>"; };
372CFD14285F2E2A00B0B54B /* ControlsBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsBar.swift; sourceTree = "<group>"; };
373031F22838388A000CFD59 /* PlayerLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLayerView.swift; sourceTree = "<group>"; tabWidth = 5; };
373031F428383A89000CFD59 /* PiPDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPDelegate.swift; sourceTree = "<group>"; };
@ -1158,6 +1247,8 @@
37599F2F272B42810087F250 /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = "<group>"; };
37599F33272B44000087F250 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = "<group>"; };
37599F37272B4D740087F250 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = "<group>"; };
375AC2992B66B7D600B680E7 /* ExportSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSettings.swift; sourceTree = "<group>"; };
375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportSettingsModel.swift; sourceTree = "<group>"; };
375B537728DF6CBB004C1D19 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
375B537928DF6CC4004C1D19 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
375B8AB228B580D300397B31 /* KeychainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainModel.swift; sourceTree = "<group>"; };
@ -1269,6 +1360,25 @@
37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettings.swift; sourceTree = "<group>"; };
37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaybackSettingsPresentationDetents+Backport.swift"; sourceTree = "<group>"; };
37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlBackgroundModifier.swift; sourceTree = "<group>"; };
37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupImporter.swift; sourceTree = "<group>"; };
37A7D6F22B67E44F009CB1ED /* PlayerSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D6F62B68071C009CB1ED /* PlayerSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSettingsGroupImporter.swift; sourceTree = "<group>"; };
37A7D6FA2B680822009CB1ED /* ControlsSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsSettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D6FE2B68082F009CB1ED /* ControlsSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsSettingsGroupImporter.swift; sourceTree = "<group>"; };
37A7D7022B680A97009CB1ED /* QualitySettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D7062B680A9E009CB1ED /* QualitySettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettingsGroupImporter.swift; sourceTree = "<group>"; };
37A7D70A2B680CE6009CB1ED /* HistorySettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D70E2B680CED009CB1ED /* HistorySettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySettingsGroupImporter.swift; sourceTree = "<group>"; };
37A7D7122B680D62009CB1ED /* SponsorBlockSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D7162B680D6C009CB1ED /* SponsorBlockSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSettingsGroupImporter.swift; sourceTree = "<group>"; };
37A7D71A2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsSettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D71E2B680E6B009CB1ED /* LocationsSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsSettingsGroupImporter.swift; sourceTree = "<group>"; };
37A7D7222B680F6F009CB1ED /* AdvancedSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D7262B680F75009CB1ED /* AdvancedSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsGroupImporter.swift; sourceTree = "<group>"; };
37A7D72A2B68100A009CB1ED /* OtherDataSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherDataSettingsGroupExporter.swift; sourceTree = "<group>"; };
37A7D72E2B681011009CB1ED /* OtherDataSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherDataSettingsGroupImporter.swift; sourceTree = "<group>"; };
37A81BF8294BD1440081D322 /* WatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchView.swift; sourceTree = "<group>"; };
37A9965926D6F8CA006E3224 /* HorizontalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCells.swift; sourceTree = "<group>"; };
37A9965D26D6F9B9006E3224 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
@ -1299,6 +1409,9 @@
37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = "<group>"; };
37BA796D26DC412E002A0235 /* Int+FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+FormatTests.swift"; sourceTree = "<group>"; };
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = "<group>"; };
37BBB3392B6B9053001C4845 /* ImportSettingsSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsSheetViewModel.swift; sourceTree = "<group>"; };
37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsSheetView.swift; sourceTree = "<group>"; };
37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettingsAccountRow.swift; sourceTree = "<group>"; };
37BC50A72778A84700510953 /* HistorySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySettings.swift; sourceTree = "<group>"; };
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 = "<group>"; };
@ -1373,6 +1486,8 @@
37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStatusHeader.swift; sourceTree = "<group>"; };
37E70922271CD43000D34DDE /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = "<group>"; };
37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettingsButton.swift; sourceTree = "<group>"; };
37E75CC62B6AEAF7003A6237 /* RecentlyOpenedImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyOpenedImporter.swift; sourceTree = "<group>"; };
37E75CCA2B6AEB01003A6237 /* RecentlyOpenedExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyOpenedExporter.swift; sourceTree = "<group>"; };
37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsOverlay.swift; sourceTree = "<group>"; };
37E80F3F287B472300561799 /* ScrollContentBackground+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollContentBackground+Backport.swift"; sourceTree = "<group>"; };
37E868FD29AA400B003128D0 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -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 = "<group>";
};
37A7D6E22B67E2EF009CB1ED /* Import Export Settings */ = {
isa = PBXGroup;
children = (
37A7D6F12B67E433009CB1ED /* Importers */,
37A7D6F02B67E42D009CB1ED /* Exporters */,
372C74692B67098A00BE179B /* ImportSettingsFileModel.swift */,
375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */,
);
path = "Import Export Settings";
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
37BA796C26DC4105002A0235 /* Extensions */ = {
isa = PBXGroup;
children = (
@ -1996,6 +2159,18 @@
path = Extensions;
sourceTree = "<group>";
};
37BBB33D2B6B9C80001C4845 /* Import */ = {
isa = PBXGroup;
children = (
37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */,
372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */,
37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */,
37BBB3392B6B9053001C4845 /* ImportSettingsSheetViewModel.swift */,
372C74652B67044300BE179B /* ImportSettingsSheetViewModifier.swift */,
);
path = Import;
sourceTree = "<group>";
};
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;
};

View File

@ -34,6 +34,16 @@
<string>public.file-url</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Settings text</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.json</string>
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
@ -68,5 +78,31 @@
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Yattee Settings</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>stream.yattee.app-settings</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>yatteesettings</string>
</array>
<key>public.mime-type</key>
<array>
<string>application/json</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@ -16,6 +16,18 @@
<string>public.mpeg-4</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Settings</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.json</string>
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
@ -37,5 +49,51 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Yattee Settings</string>
<key>UTTypeIcons</key>
<dict/>
<key>UTTypeIdentifier</key>
<string>stream.yattee.app-settings</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>yatteesettings</string>
</array>
<key>public.mime-type</key>
<array/>
</dict>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Yattee Settings</string>
<key>UTTypeIcons</key>
<dict/>
<key>UTTypeIdentifier</key>
<string>stream.yattee.app-settings</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>yatteesettings</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>