From e6deb9ef26eef7c662549227e9a942579de3295b Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 3 Feb 2024 21:04:37 +0100 Subject: [PATCH] Add import on tvOS, other export/import improvements --- .../ImportSettingsFileModel.swift | 49 +++-- Shared/OpenURLHandler.swift | 10 +- Shared/Settings/ExportSettings.swift | 15 +- Shared/Settings/Import/ImportSettings.swift | 34 ++++ .../Import/ImportSettingsAccountRow.swift | 177 +++++++++--------- .../Import/ImportSettingsSheetView.swift | 64 +++---- Shared/Settings/SettingsView.swift | 36 ++-- Yattee.xcodeproj/project.pbxproj | 4 + 8 files changed, 230 insertions(+), 159 deletions(-) create mode 100644 Shared/Settings/Import/ImportSettings.swift diff --git a/Model/Import Export Settings/ImportSettingsFileModel.swift b/Model/Import Export Settings/ImportSettingsFileModel.swift index 157e9975..d6817392 100644 --- a/Model/Import Export Settings/ImportSettingsFileModel.swift +++ b/Model/Import Export Settings/ImportSettingsFileModel.swift @@ -2,15 +2,11 @@ import Defaults import Foundation import SwiftyJSON -struct ImportSettingsFileModel { - let url: URL - - var filename: String { - String(url.lastPathComponent.dropLast(ImportExportSettingsModel.settingsExtension.count + 1)) - } +final class ImportSettingsFileModel: ObservableObject { + static let shared = ImportSettingsFileModel() var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? { - if let locationsSettings = json?.dictionaryValue["locationsSettings"] { + if let locationsSettings = json.dictionaryValue["locationsSettings"] { return LocationsSettingsGroupImporter( json: locationsSettings, includePublicLocations: importExportModel.isGroupEnabled(.locationsSettings), @@ -25,6 +21,8 @@ struct ImportSettingsFileModel { var importExportModel = ImportExportSettingsModel.shared var sheetViewModel = ImportSettingsSheetViewModel.shared + var loadTask: URLSessionTask? + func isGroupIncludedInFile(_ group: ImportExportSettingsModel.ExportGroup) -> Bool { switch group { case .locationsSettings: @@ -48,7 +46,7 @@ struct ImportSettingsFileModel { } func groupJSON(_ group: ImportExportSettingsModel.ExportGroup) -> JSON { - json?.dictionaryValue[group.rawValue] ?? .init() + json.dictionaryValue[group.rawValue] ?? .init() } func performImport() { @@ -91,17 +89,34 @@ struct ImportSettingsFileModel { } } - var json: JSON? { - if let fileContents = try? Data(contentsOf: url), - let json = try? JSON(data: fileContents) - { - return json + @Published var json = JSON() + + func loadData(_ url: URL) { + json = JSON() + loadTask?.cancel() + + loadTask = URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in + guard let data else { return } + + if let json = try? JSON(data: data) { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.json = json + + self.sheetViewModel.reset(locationsSettingsGroupImporter) + self.importExportModel.reset(self) + } + } } - return nil + loadTask?.resume() + } + + func filename(_ url: URL) -> String { + String(url.lastPathComponent.dropLast(ImportExportSettingsModel.settingsExtension.count + 1)) } var metadataBuild: String? { - if let build = json?.dictionaryValue["metadata"]?.dictionaryValue["build"]?.string { + if let build = json.dictionaryValue["metadata"]?.dictionaryValue["build"]?.string { return build } @@ -109,7 +124,7 @@ struct ImportSettingsFileModel { } var metadataPlatform: String? { - if let platform = json?.dictionaryValue["metadata"]?.dictionaryValue["platform"]?.string { + if let platform = json.dictionaryValue["metadata"]?.dictionaryValue["platform"]?.string { return platform } @@ -117,7 +132,7 @@ struct ImportSettingsFileModel { } var metadataDate: String? { - if let timestamp = json?.dictionaryValue["metadata"]?.dictionaryValue["timestamp"]?.doubleValue { + if let timestamp = json.dictionaryValue["metadata"]?.dictionaryValue["timestamp"]?.doubleValue { let date = Date(timeIntervalSince1970: timestamp) return dateFormatter.string(from: date) } diff --git a/Shared/OpenURLHandler.swift b/Shared/OpenURLHandler.swift index 15c97024..65a5ef71 100644 --- a/Shared/OpenURLHandler.swift +++ b/Shared/OpenURLHandler.swift @@ -14,11 +14,6 @@ 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 @@ -26,6 +21,11 @@ struct OpenURLHandler { return } + if url.isFileURL, url.standardizedFileURL.absoluteString.hasSuffix(".\(ImportExportSettingsModel.settingsExtension)") { + navigation.presentSettingsImportSheet(url) + return + } + if accounts.current.isNil { accounts.setCurrent(accounts.any) } diff --git a/Shared/Settings/ExportSettings.swift b/Shared/Settings/ExportSettings.swift index af4bf6e5..8aa8bef6 100644 --- a/Shared/Settings/ExportSettings.swift +++ b/Shared/Settings/ExportSettings.swift @@ -30,6 +30,13 @@ struct ExportSettings: View { #endif #endif } + #if os(iOS) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + exportButton + } + } + #endif .navigationTitle("Export Settings") } @@ -106,12 +113,6 @@ struct ExportSettings: View { ExportGroupRow(group: group) } } - - #if !os(macOS) - Section { - exportButton - } - #endif } .buttonStyle(.plain) .disabled(model.isExportInProgress) @@ -119,7 +120,7 @@ struct ExportSettings: View { var exportButton: some View { Button(action: exportSettings) { - Label(model.isExportInProgress ? "Export in progress..." : "Export...", systemImage: model.isExportInProgress ? "fireworks" : "square.and.arrow.up") + Text(model.isExportInProgress ? "In progress..." : "Export") .animation(nil, value: model.isExportInProgress) #if !os(macOS) .foregroundColor(.accentColor) diff --git a/Shared/Settings/Import/ImportSettings.swift b/Shared/Settings/Import/ImportSettings.swift new file mode 100644 index 00000000..c97879fc --- /dev/null +++ b/Shared/Settings/Import/ImportSettings.swift @@ -0,0 +1,34 @@ + +import SwiftUI + +struct ImportSettings: View { + @State private var fileURL = "" + + var body: some View { + VStack(spacing: 100) { + VStack(alignment: .leading, spacing: 20) { + Text("1. Export settings from Yattee for iOS or macOS") + Text("2. Upload it to a file hosting (e. g. Pastebin or GitHub Gist)") + Text("3. Enter file URL in the field below. You can use iOS remote to paste.") + } + + TextField("URL", text: $fileURL) + + Button { + if let url = URL(string: fileURL) { + NavigationModel.shared.presentSettingsImportSheet(url) + } + } label: { + Text("Import") + } + } + .padding(20) + .navigationTitle("Import Settings") + } +} + +struct ImportSettings_Previews: PreviewProvider { + static var previews: some View { + ImportSettings() + } +} diff --git a/Shared/Settings/Import/ImportSettingsAccountRow.swift b/Shared/Settings/Import/ImportSettingsAccountRow.swift index 15c9db7b..918e3a81 100644 --- a/Shared/Settings/Import/ImportSettingsAccountRow.swift +++ b/Shared/Settings/Import/ImportSettingsAccountRow.swift @@ -27,102 +27,110 @@ struct ImportSettingsAccountRow: View { } var body: some View { - Button(action: { model.toggleAccount(account, accounts: accounts) }) { - let accountExists = AccountsModel.shared.find(account.id) != nil + #if os(tvOS) + row + #else + Button(action: { model.toggleAccount(account, accounts: accounts) }) { + row + } + .buttonStyle(.plain) + #endif + } - 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) + var row: some View { + let accountExists = AccountsModel.shared.find(account.id) != nil - 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) + return 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) - 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 { + 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("Password saved in import file") + 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) + .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 + } } - .buttonStyle(.plain) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onChange(of: isValid) { _ in afterValidation() } + .animation(nil, value: isChecked) } var isChecked: Bool { @@ -173,7 +181,8 @@ struct ImportSettingsAccountRow: View { struct ImportSettingsAccountRow_Previews: PreviewProvider { static var previews: some View { - let fileModel = ImportSettingsFileModel(url: URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!) + let fileModel = ImportSettingsFileModel() + fileModel.loadData(URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!) return List { ImportSettingsAccountRow( diff --git a/Shared/Settings/Import/ImportSettingsSheetView.swift b/Shared/Settings/Import/ImportSettingsSheetView.swift index 416ff09c..6055d61b 100644 --- a/Shared/Settings/Import/ImportSettingsSheetView.swift +++ b/Shared/Settings/Import/ImportSettingsSheetView.swift @@ -4,6 +4,7 @@ struct ImportSettingsSheetView: View { @Binding var settingsFile: URL? @StateObject private var model = ImportSettingsSheetViewModel.shared @StateObject private var importExportModel = ImportExportSettingsModel.shared + @StateObject private var fileModel = ImportSettingsFileModel.shared @Environment(\.presentationMode) private var presentationMode @@ -23,12 +24,12 @@ struct ImportSettingsSheetView: View { #endif } .onAppear { - guard let fileModel else { return } - model.reset(fileModel.locationsSettingsGroupImporter) - importExportModel.reset(fileModel) + guard let settingsFile else { return } + fileModel.loadData(settingsFile) } .onChange(of: settingsFile) { _ in - importExportModel.reset(fileModel) + guard let settingsFile else { return } + fileModel.loadData(settingsFile) } } @@ -56,7 +57,7 @@ struct ImportSettingsSheetView: View { } ToolbarItem(placement: .confirmationAction) { Button(action: { - fileModel?.performImport() + fileModel.performImport() presentingCompletedAlert = true ImportExportSettingsModel.shared.reset() }) { @@ -85,16 +86,8 @@ struct ImportSettingsSheetView: View { 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 + fileModel.locationsSettingsGroupImporter } struct ExportGroupRow: View { @@ -128,34 +121,43 @@ struct ImportSettingsSheetView: View { Section(header: Text("Settings")) { ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in ExportGroupRow(group: group) - .disabled(!fileModel!.isGroupIncludedInFile(group)) + .disabled(!fileModel.isGroupIncludedInFile(group)) } } Section(header: Text("Other")) { ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in ExportGroupRow(group: group) - .disabled(!fileModel!.isGroupIncludedInFile(group)) + .disabled(!fileModel.isGroupIncludedInFile(group)) } } } } @ViewBuilder var metadata: some View { - if let fileModel { + if let settingsFile { Section(header: Text("File information")) { - MetadataRow(name: Text("Name"), value: Text(fileModel.filename)) + MetadataRow(name: Text("Name"), value: Text(fileModel.filename(settingsFile))) if let date = fileModel.metadataDate { MetadataRow(name: Text("Date"), value: Text(date)) + #if os(tvOS) + .focusable() + #endif } if let build = fileModel.metadataBuild { MetadataRow(name: Text("Build"), value: Text(build)) + #if os(tvOS) + .focusable() + #endif } if let platform = fileModel.metadataPlatform { MetadataRow(name: Text("Platform"), value: Text(platform)) + #if os(tvOS) + .focusable() + #endif } } } @@ -231,24 +233,22 @@ struct ImportSettingsSheetView: View { } @ViewBuilder var importOptions: some View { - if let fileModel { - if fileModel.isPublicInstancesSettingsGroupInFile || !instances.isEmpty { - Section(header: Text("Locations")) { - if fileModel.isPublicInstancesSettingsGroupInFile { - ExportGroupRow(group: .locationsSettings) - } + if fileModel.isPublicInstancesSettingsGroupInFile || !instances.isEmpty { + Section(header: Text("Locations")) { + if fileModel.isPublicInstancesSettingsGroupInFile { + ExportGroupRow(group: .locationsSettings) + } - ForEach(instances) { instance in - ImportInstanceRow(instance: instance, accounts: accounts) - } + 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) - } + if !accounts.isEmpty { + Section(header: Text("Accounts")) { + ForEach(accounts) { account in + ImportSettingsAccountRow(account: account, fileModel: fileModel) } } } diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index 2344126a..3695c86a 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -31,9 +31,9 @@ struct SettingsView: View { var body: some View { settings + .modifier(ImportSettingsSheetViewModifier(isPresented: $settingsModel.presentingSettingsImportSheet, settingsFile: $settingsModel.settingsImportURL)) #if !os(tvOS) - .modifier(ImportSettingsFileImporterViewModifier(isPresented: $navigation.presentingSettingsFileImporter)) - .modifier(ImportSettingsSheetViewModifier(isPresented: $settingsModel.presentingSettingsImportSheet, settingsFile: $settingsModel.settingsImportURL)) + .modifier(ImportSettingsFileImporterViewModifier(isPresented: $navigation.presentingSettingsFileImporter)) #endif #if os(iOS) .backport @@ -281,19 +281,27 @@ struct SettingsView: View { var importView: some View { Section { - Button(action: importSettings) { - Label("Import Settings...", systemImage: "square.and.arrow.down") - .frame(maxWidth: .infinity, alignment: .leading) - .contentShape(Rectangle()) - } - .foregroundColor(.accentColor) - .buttonStyle(.plain) + #if os(tvOS) + NavigationLink(destination: LazyView(ImportSettings())) { + Label("Import Settings", systemImage: "square.and.arrow.down") + .labelStyle(SettingsLabel()) + } + .padding(.horizontal, 20) + #else + Button(action: importSettings) { + Label("Import Settings...", systemImage: "square.and.arrow.down") + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .foregroundColor(.accentColor) + .buttonStyle(.plain) - NavigationLink(destination: LazyView(ExportSettings())) { - Label("Export Settings", systemImage: "square.and.arrow.up") - .frame(maxWidth: .infinity, alignment: .leading) - .contentShape(Rectangle()) - } + NavigationLink(destination: LazyView(ExportSettings())) { + Label("Export Settings", systemImage: "square.and.arrow.up") + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + #endif } } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 24f13cb6..6d62a0a1 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -662,6 +662,7 @@ 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 */; }; + 37A6D4ED2B6E372700B26299 /* ImportSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6D4EC2B6E372700B26299 /* ImportSettings.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 */; }; @@ -1360,6 +1361,7 @@ 37A362BD29537AAA00BDF328 /* PlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettings.swift; sourceTree = ""; }; 37A362C129537FED00BDF328 /* PlaybackSettingsPresentationDetents+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaybackSettingsPresentationDetents+Backport.swift"; sourceTree = ""; }; 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlBackgroundModifier.swift; sourceTree = ""; }; + 37A6D4EC2B6E372700B26299 /* ImportSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportSettings.swift; sourceTree = ""; }; 37A7D6E42B67E315009CB1ED /* SettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupExporter.swift; sourceTree = ""; }; 37A7D6E82B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupExporter.swift; sourceTree = ""; }; 37A7D6EC2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingSettingsGroupImporter.swift; sourceTree = ""; }; @@ -2162,6 +2164,7 @@ 37BBB33D2B6B9C80001C4845 /* Import */ = { isa = PBXGroup; children = ( + 37A6D4EC2B6E372700B26299 /* ImportSettings.swift */, 37BBB3422B6BB88F001C4845 /* ImportSettingsAccountRow.swift */, 372C74622B66FFFC00BE179B /* ImportSettingsFileImporterViewModifier.swift */, 37BBB33E2B6B9D52001C4845 /* ImportSettingsSheetView.swift */, @@ -3840,6 +3843,7 @@ 37E80F45287B7AC000561799 /* ControlsBar.swift in Sources */, 3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 376BE50827347B57009AD608 /* SettingsHeader.swift in Sources */, + 37A6D4ED2B6E372700B26299 /* ImportSettings.swift in Sources */, 37A9966026D6F9B9006E3224 /* HomeView.swift in Sources */, 372820402945E4A8009A0E2D /* SubscriptionsPageButton.swift in Sources */, 37001565271B1F250049C794 /* AccountsModel.swift in Sources */,