diff --git a/Multiplatform/iOS/Settings/FeedsSettingsModel.swift b/Multiplatform/iOS/Settings/FeedsSettingsModel.swift new file mode 100644 index 000000000..b32c17c71 --- /dev/null +++ b/Multiplatform/iOS/Settings/FeedsSettingsModel.swift @@ -0,0 +1,68 @@ +// +// FeedsSettingsModel.swift +// Multiplatform iOS +// +// Created by Rizwan on 04/07/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import SwiftUI +import Account + +class FeedsSettingsModel: ObservableObject { + @Published var showingImportActionSheet = false + @Published var showingExportActionSheet = false + @Published var exportingFilePath = "" + + func onTapExportOPML(action: ((Account?) -> Void)) { + if AccountManager.shared.accounts.count == 1 { + action(AccountManager.shared.accounts.first) + } + else { + showingExportActionSheet = true + } + } + + func onTapImportOPML(action: ((Account?) -> Void)) { + switch AccountManager.shared.activeAccounts.count { + case 0: + //TODO:- show error + return + case 1: + action(AccountManager.shared.activeAccounts.first) + default: + showingImportActionSheet = true + } + } + + func generateExportURL(for account: Account) -> URL? { + let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces) + let filename = "Subscriptions-\(accountName).opml" + let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename) + let opmlString = OPMLExporter.OPMLString(with: account, title: filename) + do { + try opmlString.write(to: tempFile, atomically: true, encoding: String.Encoding.utf8) + } catch { + //TODO:- show error + return nil + } + + return tempFile + } + + func processImportedFiles(_ urls: [URL],_ account: Account?) { + urls.forEach{ + account?.importOPML($0, completion: { result in + switch result { + case .success: + break + case .failure: + //TODO:- show error + break + } + }) + } + } +} + diff --git a/Multiplatform/iOS/Settings/SettingsView.swift b/Multiplatform/iOS/Settings/SettingsView.swift index 9aca56100..95fbd6894 100644 --- a/Multiplatform/iOS/Settings/SettingsView.swift +++ b/Multiplatform/iOS/Settings/SettingsView.swift @@ -8,7 +8,7 @@ import SwiftUI import Account - +import UniformTypeIdentifiers class SettingsViewModel: ObservableObject { @@ -55,10 +55,13 @@ struct SettingsView: View { let sortedAccounts = AccountManager.shared.sortedAccounts @Environment(\.presentationMode) var presentationMode - + @Environment(\.exportFiles) var exportAction + @Environment(\.importFiles) var importAction + @StateObject private var viewModel = SettingsViewModel() + @StateObject private var feedsSettingsModel = FeedsSettingsModel() @StateObject private var settings = AppDefaults.shared - + var body: some View { NavigationView { List { @@ -114,19 +117,50 @@ struct SettingsView: View { var importExport: some View { Section(header: Text("Feeds"), content: { - NavigationLink( - destination: EmptyView(), - label: { - Text("Import Subscriptions") - }) - NavigationLink( - destination: EmptyView(), - label: { - Text("Export Subscriptions") - }) + Button(action:{ + feedsSettingsModel.onTapImportOPML(action: importOPML) + }) { + Text("Import Subscriptions") + .actionSheet(isPresented: $feedsSettingsModel.showingImportActionSheet, content: importActionSheet) + .foregroundColor(.primary) + } + Button(action:{ + feedsSettingsModel.onTapExportOPML(action: exportOPML) + }) { + Text("Export Subscriptions") + .actionSheet(isPresented: $feedsSettingsModel.showingExportActionSheet, content: exportActionSheet) + .foregroundColor(.primary) + } }) + } - + + private func importActionSheet() -> ActionSheet { + var buttons = sortedAccounts.map { (account) -> ActionSheet.Button in + ActionSheet.Button.default(Text(account.nameForDisplay)) { + importOPML(account: account) + } + } + buttons.append(.cancel()) + return ActionSheet( + title: Text("Choose an account to receive the imported feeds and folders"), + buttons: buttons + ) + } + + private func exportActionSheet() -> ActionSheet { + var buttons = sortedAccounts.map { (account) -> ActionSheet.Button in + ActionSheet.Button.default(Text(account.nameForDisplay)) { + exportOPML(account: account) + } + } + buttons.append(.cancel()) + return ActionSheet( + title: Text("Choose an account with the subscriptions to export"), + buttons: buttons + ) + } + var timeline: some View { Section(header: Text("Timeline"), content: { Toggle("Sort Oldest to Newest", isOn: $settings.timelineSortDirection) @@ -202,7 +236,24 @@ struct SettingsView: View { let build = dict?.object(forKey: "CFBundleVersion") as? String ?? "" return "NetNewsWire \(version) (Build \(build))" } - + + private func exportOPML(account: Account?) { + guard let account = account, + let url = feedsSettingsModel.generateExportURL(for: account) else { + return + } + + exportAction(moving: url) { _ in } + } + + private func importOPML(account: Account?) { + let types = [UTType(filenameExtension: "opml"), UTType("public.xml")].compactMap { $0 } + importAction(multipleOfType: types) { (result: Result<[URL], Error>?) in + if let urls = try? result?.get() { + feedsSettingsModel.processImportedFiles(urls, account) + } + } + } } struct SettingsView_Previews: PreviewProvider { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index bdb2dc841..1e7515704 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -635,6 +635,8 @@ 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */; }; 6581C74020CED60100F4AD34 /* netnewswire-subscribe-to-feed.js in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73F20CED60100F4AD34 /* netnewswire-subscribe-to-feed.js */; }; 6581C74220CED60100F4AD34 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */; }; + 6594CA3B24AF6F2A005C7D7C /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; }; + 65C2E40124B05D8A000AFDF6 /* FeedsSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2E40024B05D8A000AFDF6 /* FeedsSettingsModel.swift */; }; 65CBAD5A24AE03C20006DD91 /* ColorPaletteContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65CBAD5924AE03C20006DD91 /* ColorPaletteContainerView.swift */; }; 65ED3FB7235DEF6C0081F399 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 65ED3FB8235DEF6C0081F399 /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848B937121C8C5540038DC0D /* CrashReporter.swift */; }; @@ -2007,6 +2009,7 @@ 6581C73F20CED60100F4AD34 /* netnewswire-subscribe-to-feed.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "netnewswire-subscribe-to-feed.js"; sourceTree = ""; }; 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = ToolbarItemIcon.pdf; sourceTree = ""; }; 6581C74320CED60100F4AD34 /* Subscribe_to_Feed.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Subscribe_to_Feed.entitlements; sourceTree = ""; }; + 65C2E40024B05D8A000AFDF6 /* FeedsSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsSettingsModel.swift; sourceTree = ""; }; 65CBAD5924AE03C20006DD91 /* ColorPaletteContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPaletteContainerView.swift; sourceTree = ""; }; 65ED4083235DEF6C0081F399 /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; }; 65ED409D235DEF770081F399 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2384,6 +2387,7 @@ isa = PBXGroup; children = ( 172199C824AB228900A31D04 /* SettingsView.swift */, + 65C2E40024B05D8A000AFDF6 /* FeedsSettingsModel.swift */, 17B223B924AC24A8001E4592 /* Submenus */, ); path = Settings; @@ -4807,6 +4811,7 @@ 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */, 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */, 514E6C0924AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */, + 6594CA3B24AF6F2A005C7D7C /* OPMLExporter.swift in Sources */, 51919FAF24AA8EFA00541E64 /* SidebarItemView.swift in Sources */, 514E6BDA24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */, 51E4990D24A808C500B667CB /* RSHTMLMetadata+Extension.swift in Sources */, @@ -4827,6 +4832,7 @@ 514E6BFF24AD255D00AC6F6E /* PreviewArticles.swift in Sources */, 51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */, 51408B7E24A9EC6F0073CF4E /* SidebarItem.swift in Sources */, + 65C2E40124B05D8A000AFDF6 /* FeedsSettingsModel.swift in Sources */, 51E4990A24A808C500B667CB /* FeaturedImageDownloader.swift in Sources */, 51E4993824A8680E00B667CB /* Reachability.swift in Sources */, 51E4993224A8676400B667CB /* FetchRequestQueue.swift in Sources */,