diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 45291f1d1..c736e1da9 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -72,6 +72,9 @@ 173A642C2547BE9600267F6E /* AccountType+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173A64162547BE0900267F6E /* AccountType+Helpers.swift */; }; 175942AA24AD533200585066 /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; 175942AB24AD533200585066 /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; + 175EBD7426D2405A009B5B23 /* BrowserConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175EBD7326D2405A009B5B23 /* BrowserConfigurationViewController.swift */; }; + 175EBD7626D240D8009B5B23 /* BrowserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175EBD7526D240D8009B5B23 /* BrowserCell.swift */; }; + 1760888126D1F04500E853E9 /* BrowserManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1760888026D1F04500E853E9 /* BrowserManager.swift */; }; 176813D02564BA5900D98635 /* WidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813B62564B9F800D98635 /* WidgetData.swift */; }; 176813D12564BA5900D98635 /* WidgetDataDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813C92564BA5400D98635 /* WidgetDataDecoder.swift */; }; 176813D22564BA5900D98635 /* WidgetDataEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813BD2564BA2800D98635 /* WidgetDataEncoder.swift */; }; @@ -1210,6 +1213,7 @@ DF98E2B02578AA5C00F18944 /* AddFeedWranglerAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2AF2578AA5C00F18944 /* AddFeedWranglerAccountView.swift */; }; DF98E2BE2578AC0000F18944 /* AddNewsBlurAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2BD2578AC0000F18944 /* AddNewsBlurAccountView.swift */; }; DF98E2C62578AD1B00F18944 /* AddReaderAPIAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2C52578AD1B00F18944 /* AddReaderAPIAccountView.swift */; }; + DFC05D2226D3E0770047E788 /* Browser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC05D2126D3E0770047E788 /* Browser.swift */; }; FA80C11724B0728000974098 /* AddFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA80C11624B0728000974098 /* AddFolderView.swift */; }; FA80C11824B0728000974098 /* AddFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA80C11624B0728000974098 /* AddFolderView.swift */; }; FA80C13E24B072AA00974098 /* AddFolderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA80C13D24B072AA00974098 /* AddFolderModel.swift */; }; @@ -1552,6 +1556,9 @@ 17386B792577C4BF0014C8B2 /* AddLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLocalAccountView.swift; sourceTree = ""; }; 17386BC32577CC600014C8B2 /* AddFeedbinAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedbinAccountView.swift; sourceTree = ""; }; 173A64162547BE0900267F6E /* AccountType+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountType+Helpers.swift"; sourceTree = ""; }; + 175EBD7326D2405A009B5B23 /* BrowserConfigurationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserConfigurationViewController.swift; sourceTree = ""; }; + 175EBD7526D240D8009B5B23 /* BrowserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserCell.swift; sourceTree = ""; }; + 1760888026D1F04500E853E9 /* BrowserManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserManager.swift; sourceTree = ""; }; 176813B62564B9F800D98635 /* WidgetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetData.swift; sourceTree = ""; }; 176813BD2564BA2800D98635 /* WidgetDataEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataEncoder.swift; sourceTree = ""; }; 176813C92564BA5400D98635 /* WidgetDataDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataDecoder.swift; sourceTree = ""; }; @@ -2137,6 +2144,7 @@ DF98E2AF2578AA5C00F18944 /* AddFeedWranglerAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedWranglerAccountView.swift; sourceTree = ""; }; DF98E2BD2578AC0000F18944 /* AddNewsBlurAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddNewsBlurAccountView.swift; sourceTree = ""; }; DF98E2C52578AD1B00F18944 /* AddReaderAPIAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReaderAPIAccountView.swift; sourceTree = ""; }; + DFC05D2126D3E0770047E788 /* Browser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Browser.swift; sourceTree = ""; }; FA80C11624B0728000974098 /* AddFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFolderView.swift; sourceTree = ""; }; FA80C13D24B072AA00974098 /* AddFolderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFolderModel.swift; sourceTree = ""; }; FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = ""; }; @@ -2793,6 +2801,10 @@ 51A16992235E10D600EB091F /* AddAccountViewController.swift */, 519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */, 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */, + DFC05D2126D3E0770047E788 /* Browser.swift */, + 1760888026D1F04500E853E9 /* BrowserManager.swift */, + 175EBD7326D2405A009B5B23 /* BrowserConfigurationViewController.swift */, + 175EBD7526D240D8009B5B23 /* BrowserCell.swift */, 516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */, 516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */, 516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */, @@ -4609,7 +4621,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if ! command -v swiftgen &> /dev/null\nthen\n echo \"swiftgen could not be found\"\n exit\nfi\n\narch -x86_64 swiftgen run strings -t structured-swift5 \"$PROJECT_DIR/Widget/Resources/en.lproj/Localizable.strings\" \"$PROJECT_DIR/Widget/Resources/Localizable.stringsdict\" --output \"$PROJECT_DIR/Widget/Resources/Localized.swift\";\n"; + shellScript = "if ! command -v swiftgen &> /dev/null\nthen\n echo \"swiftgen could not be found\"\n exit\nfi\n\nswiftgen run strings -t structured-swift5 \"$PROJECT_DIR/Widget/Resources/en.lproj/Localizable.strings\" \"$PROJECT_DIR/Widget/Resources/Localizable.stringsdict\" --output \"$PROJECT_DIR/Widget/Resources/Localized.swift\";\n"; }; 513F328A2593EFCE0003048F /* Delete Unnecessary Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -5490,6 +5502,7 @@ 510289CD24519A1D00426DDF /* SelectComboTableViewCell.swift in Sources */, 514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */, 51FA73A52332BE110090D516 /* ArticleExtractor.swift in Sources */, + DFC05D2226D3E0770047E788 /* Browser.swift in Sources */, 51314704235C41FC00387FDC /* Intents.intentdefinition in Sources */, FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */, 51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */, @@ -5546,6 +5559,7 @@ 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, 51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */, 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */, + 175EBD7626D240D8009B5B23 /* BrowserCell.swift in Sources */, 51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */, 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */, 5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */, @@ -5602,11 +5616,13 @@ 516AE9E02372269A007DEEAA /* IconImage.swift in Sources */, 519ED456244828C3007F8E94 /* AddExtensionPointViewController.swift in Sources */, 51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */, + 1760888126D1F04500E853E9 /* BrowserManager.swift in Sources */, D3A39865246505DF00F9A366 /* FindInArticleActivity.swift in Sources */, 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */, 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, 5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */, 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */, + 175EBD7426D2405A009B5B23 /* BrowserConfigurationViewController.swift in Sources */, FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index d1e7ec909..1abc9063e 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -54,6 +54,7 @@ final class AppDefaults { static let addWebFeedAccountID = "addWebFeedAccountID" static let addWebFeedFolderName = "addWebFeedFolderName" static let addFolderAccountID = "addFolderAccountID" + static let browserPreference = "browserPreference" } let isDeveloperBuild: Bool = { @@ -119,6 +120,18 @@ final class AppDefaults { } } + var browserPreference: String { + get { + guard let preference = UserDefaults.standard.string(forKey: Key.browserPreference) else { + return "browser.inapp" + } + return preference + } + set { + UserDefaults.standard.setValue(newValue, forKey: Key.browserPreference) + } + } + var lastImageCacheFlushDate: Date? { get { return AppDefaults.date(for: Key.lastImageCacheFlushDate) @@ -191,6 +204,20 @@ final class AppDefaults { } } + /// The default behaviour is to open links in the in-app browser, as such, `Browsers.inApp.browserID` is the default. + +// var openLinksPreferredBrowser: String { +// get { +// if AppDefaults.store.string(forKey: Key.openLinksPreferredBrowser) == nil { +// return Browsers.inApp.browserID +// } else { +// return AppDefaults.store.string(forKey: Key.openLinksPreferredBrowser)! +// } +// } +// +// set { AppDefaults.store.setValue(newValue, forKey: Key.openLinksPreferredBrowser) } +// } + var timelineNumberOfLines: Int { get { return AppDefaults.int(for: Key.timelineNumberOfLines) diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index fe6188903..922a095b1 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -250,8 +250,12 @@ class WebViewController: UIViewController { func openInAppBrowser() { guard let url = article?.preferredURL else { return } - let vc = SFSafariViewController(url: url) - present(vc, animated: true) + if BrowserManager.shared.currentBrowser() == .inApp { + let vc = SFSafariViewController(url: url) + present(vc, animated: true) + } else { + BrowserManager.shared.openURL(urlString: url.absoluteString) + } } } @@ -344,7 +348,12 @@ extension WebViewController: WKNavigationDelegate { let components = URLComponents(url: url, resolvingAgainstBaseURL: false) if components?.scheme == "http" || components?.scheme == "https" { decisionHandler(.cancel) - openURL(url) + if BrowserManager.shared.currentBrowser() == .inApp { + openURL(url) + } else { + BrowserManager.shared.openURL(urlString: url.absoluteString) + } + } else if components?.scheme == "mailto" { decisionHandler(.cancel) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 1a99608bd..b2e5f8413 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -899,7 +899,9 @@ private extension MasterTimelineViewController { func openInBrowserAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? { guard let _ = article.preferredURL else { return nil } - let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") + var displayName: String + BrowserManager.shared.currentBrowser() == .inApp ? (displayName = Browser.safari.displayName) : (displayName = BrowserManager.shared.currentBrowser().displayName) + let title = NSLocalizedString("Open in \(displayName)", comment: "Open in Browser") let action = UIAlertAction(title: title, style: .default) { [weak self] action in self?.coordinator.showBrowserForArticle(article) completion(true) diff --git a/iOS/Resources/Assets.xcassets/browsers/Contents.json b/iOS/Resources/Assets.xcassets/browsers/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.brave.imageset/Contents.json b/iOS/Resources/Assets.xcassets/browsers/browser.brave.imageset/Contents.json new file mode 100644 index 000000000..6ff7b5b01 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.brave.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icons8-brave-web-browser.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.brave.imageset/icons8-brave-web-browser.pdf b/iOS/Resources/Assets.xcassets/browsers/browser.brave.imageset/icons8-brave-web-browser.pdf new file mode 100644 index 000000000..d20ed9cb4 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.brave.imageset/icons8-brave-web-browser.pdf differ diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.chrome.imageset/Contents.json b/iOS/Resources/Assets.xcassets/browsers/browser.chrome.imageset/Contents.json new file mode 100644 index 000000000..1d65e4191 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.chrome.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icons8-chrome.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.chrome.imageset/icons8-chrome.pdf b/iOS/Resources/Assets.xcassets/browsers/browser.chrome.imageset/icons8-chrome.pdf new file mode 100644 index 000000000..7babc85aa Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.chrome.imageset/icons8-chrome.pdf differ diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.edge.imageset/Contents.json b/iOS/Resources/Assets.xcassets/browsers/browser.edge.imageset/Contents.json new file mode 100644 index 000000000..648ebe289 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.edge.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icons8-microsoft-edge.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.edge.imageset/icons8-microsoft-edge.pdf b/iOS/Resources/Assets.xcassets/browsers/browser.edge.imageset/icons8-microsoft-edge.pdf new file mode 100644 index 000000000..1682aa600 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.edge.imageset/icons8-microsoft-edge.pdf differ diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.firefox.imageset/Contents.json b/iOS/Resources/Assets.xcassets/browsers/browser.firefox.imageset/Contents.json new file mode 100644 index 000000000..a0c2f2974 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.firefox.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "firefox-logo-glyph.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.firefox.imageset/firefox-logo-glyph.pdf b/iOS/Resources/Assets.xcassets/browsers/browser.firefox.imageset/firefox-logo-glyph.pdf new file mode 100644 index 000000000..e680ac94a Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.firefox.imageset/firefox-logo-glyph.pdf differ diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-120px-40pt@3x.png b/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-120px-40pt@3x.png new file mode 100644 index 000000000..724a12a23 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-120px-40pt@3x.png differ diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-40px-40pt@1x.png b/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-40px-40pt@1x.png new file mode 100644 index 000000000..b5b617036 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-40px-40pt@1x.png differ diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-80px-40pt@2x.png b/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-80px-40pt@2x.png new file mode 100644 index 000000000..47a4b083a Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/AppIcon-80px-40pt@2x.png differ diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/Contents.json b/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/Contents.json new file mode 100644 index 000000000..4b418c667 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.inapp.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "AppIcon-40px-40pt@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "AppIcon-80px-40pt@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "AppIcon-120px-40pt@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.onepassword.imageset/1password.pdf b/iOS/Resources/Assets.xcassets/browsers/browser.onepassword.imageset/1password.pdf new file mode 100644 index 000000000..418cb7453 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.onepassword.imageset/1password.pdf differ diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.onepassword.imageset/Contents.json b/iOS/Resources/Assets.xcassets/browsers/browser.onepassword.imageset/Contents.json new file mode 100644 index 000000000..cea03a9f2 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.onepassword.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "1password.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.opera.imageset/Contents.json b/iOS/Resources/Assets.xcassets/browsers/browser.opera.imageset/Contents.json new file mode 100644 index 000000000..020c9a183 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.opera.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icons8-opera.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.opera.imageset/icons8-opera.pdf b/iOS/Resources/Assets.xcassets/browsers/browser.opera.imageset/icons8-opera.pdf new file mode 100644 index 000000000..b7fe0d20e --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.opera.imageset/icons8-opera.pdf @@ -0,0 +1,72 @@ +%PDF-1.4 +% +4 0 obj +<< /Length 5 0 R + /Filter /FlateDecode +>> +stream +xmTA1 @]Km"@=?PRLr($$So.= H[;龁5DžKU̮ݣLj6xPrsVmDs2p6ڔd$:H}@kB/CtD =^M!,BCYu$ɠhWG +ͩ3ph%E7FOu/,gLH\= ++Ofa]1MI:ؑ"9tq4 MOT7V1/VFMRT"%UU깉hjx[%X6ۺOt8!Sa_Yʺuϒ͙JH'Ka/3 AaӗV{uq8 +cSd#$vX0Ex3>c"r؟R-WnRBuYy:{+Yt)؏7zn)fI/+ (ÞxL|"D +endstream +endobj +5 0 obj + 656 +endobj +3 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +2 0 obj +<< /Type /Page % 1 + /Parent 1 0 R + /MediaBox [ 0 0 50 50 ] + /Contents 4 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 3 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 2 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Producer (cairo 1.16.0 (https://cairographics.org)) + /CreationDate (D:20210822124817Z) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000001058 00000 n +0000000842 00000 n +0000000770 00000 n +0000000015 00000 n +0000000748 00000 n +0000001123 00000 n +0000001234 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +1286 +%%EOF diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.safari.imageset/Contents.json b/iOS/Resources/Assets.xcassets/browsers/browser.safari.imageset/Contents.json new file mode 100644 index 000000000..422493fef --- /dev/null +++ b/iOS/Resources/Assets.xcassets/browsers/browser.safari.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "safari.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/iOS/Resources/Assets.xcassets/browsers/browser.safari.imageset/safari.pdf b/iOS/Resources/Assets.xcassets/browsers/browser.safari.imageset/safari.pdf new file mode 100644 index 000000000..a0f7f8e03 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/browsers/browser.safari.imageset/safari.pdf differ diff --git a/iOS/Resources/Info.plist b/iOS/Resources/Info.plist index 2ccfb374d..dbcc0f683 100644 --- a/iOS/Resources/Info.plist +++ b/iOS/Resources/Info.plist @@ -2,10 +2,6 @@ - OrganizationIdentifier - $(ORGANIZATION_IDENTIFIER) - DeveloperEntitlements - $(DEVELOPER_ENTITLEMENTS) AppGroup group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS AppIdentifierPrefix @@ -54,10 +50,18 @@ CFBundleVersion $(CURRENT_PROJECT_VERSION) + DeveloperEntitlements + $(DEVELOPER_ENTITLEMENTS) LSApplicationQueriesSchemes mailto org-appextension-feature-password-management + brave + touch-http + googlechrome + microsoft-edge-https + firefox + ophttps LSRequiresIPhoneOS @@ -76,6 +80,8 @@ Restoration SelectFeed + OrganizationIdentifier + $(ORGANIZATION_IDENTIFIER) UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 7bc4b4037..607d382e2 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1229,7 +1229,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func showBrowserForArticle(_ article: Article) { guard let url = article.preferredURL else { return } - UIApplication.shared.open(url, options: [:]) + if BrowserManager.shared.currentBrowser() == .inApp { + UIApplication.shared.open(url, options: [:]) + } else { + BrowserManager.shared.openURL(urlString: url.absoluteString) + } + } func showBrowserForCurrentArticle() { diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 39a26aa2d..902faaddb 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -77,6 +77,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { appDelegate.prepareAccountsForForeground() coordinator.configurePanelMode(for: window!.frame.size) coordinator.resetFocus() + BrowserManager.shared.configureAvailableBrowsers() } func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { diff --git a/iOS/Settings/Browser.swift b/iOS/Settings/Browser.swift new file mode 100644 index 000000000..98d333c50 --- /dev/null +++ b/iOS/Settings/Browser.swift @@ -0,0 +1,113 @@ +// +// Browser.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 23/08/2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation + + + +/// The `Browser` enum contains browsers supported by NetNewsWire. +/// +/// To support a new browser, create a case, configure it, add its URL scheme +/// to `LSApplicationQueriesSchemes` in `Info.plist`, and add an +/// appropriate image to the Asset catalog. The browserID should be used as the +/// image filename. +public enum Browser: CaseIterable { + + case inApp + case safari + case brave + case chrome + case edge + case firefox + case opera + case onePassword + + var urlScheme: String { + switch self { + case .inApp: + return "" // not required, will open in SFSafariViewController + case .safari: + return "" // not required, will use openURL + case .brave: + return "brave://open-url?url=" + case .chrome: + return "googlechrome://" + case .edge: + return "microsoft-edge-https://" + case .firefox: + return "firefox://open-url?url=" + case .opera: + return "touch-http://" + case .onePassword: + return "ophttps://" + } + } + + var canOpenURL: Bool { + switch self { + case .inApp: + return true + case .safari: + return UIApplication.shared.canOpenURL(URL(string: "https://apple.com")!) + case .brave: + return UIApplication.shared.canOpenURL(URL(string: Browser.brave.urlScheme)!) + case .chrome: + return UIApplication.shared.canOpenURL(URL(string: Browser.chrome.urlScheme)!) + case .edge: + return UIApplication.shared.canOpenURL(URL(string: Browser.edge.urlScheme)!) + case .firefox: + return UIApplication.shared.canOpenURL(URL(string: Browser.firefox.urlScheme)!) + case .opera: + return UIApplication.shared.canOpenURL(URL(string: Browser.opera.urlScheme)!) + case .onePassword: + return UIApplication.shared.canOpenURL(URL(string: Browser.onePassword.urlScheme)!) + } + } + + var browserID: String { + switch self { + case .inApp: + return "browser.inapp" + case .safari: + return "browser.safari" + case .brave: + return "browser.brave" + case .chrome: + return "browser.chrome" + case .edge: + return "browser.edge" + case .firefox: + return "browser.firefox" + case .opera: + return "browser.opera" + case .onePassword: + return "browser.onepassword" + } + } + + var displayName: String { + switch self { + case .inApp: + return NSLocalizedString("NetNewsWire", comment: "In-app") + case .safari: + return NSLocalizedString("Default Browser", comment: "Default") + case .brave: + return NSLocalizedString("Brave", comment: "Brave") + case .chrome: + return NSLocalizedString("Chrome", comment: "Chrome") + case .edge: + return NSLocalizedString("Edge", comment: "Edge") + case .firefox: + return NSLocalizedString("Firefox", comment: "Firefox") + case .opera: + return NSLocalizedString("Opera", comment: "Opera") + case .onePassword: + return NSLocalizedString("1Password", comment: "1Password") + } + } +} diff --git a/iOS/Settings/BrowserCell.swift b/iOS/Settings/BrowserCell.swift new file mode 100644 index 000000000..4f8459c5a --- /dev/null +++ b/iOS/Settings/BrowserCell.swift @@ -0,0 +1,47 @@ +// +// BrowserCell.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 22/8/21. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import UIKit + +class BrowserCell: VibrantTableViewCell { + + @IBOutlet weak var browserName: UILabel! + private var browser: Browser! + + override func updateVibrancy(animated: Bool) { + super.updateVibrancy(animated: animated) + updateLabelVibrancy(browserName, color: labelColor, animated: animated) + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + func configure(with browser: Browser) { + browserName.text = browser.displayName + self.browser = browser + + if BrowserManager.shared.currentBrowser().browserID == browser.browserID { + accessoryType = .checkmark + } else { + accessoryType = .none + } + } + + func updateBrowserSelection() { + BrowserManager.shared.currentBrowserPreference = self.browser.browserID + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + +} diff --git a/iOS/Settings/BrowserConfigurationViewController.swift b/iOS/Settings/BrowserConfigurationViewController.swift new file mode 100644 index 000000000..fb8cabd4c --- /dev/null +++ b/iOS/Settings/BrowserConfigurationViewController.swift @@ -0,0 +1,89 @@ +// +// BrowserConfigurationViewController.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 22/8/21. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import UIKit + +class BrowserConfigurationViewController: UITableViewController { + + let browserManager = BrowserManager.shared + + override func viewDidLoad() { + super.viewDidLoad() + title = NSLocalizedString("Browser Selection", comment: "Browser") + NotificationCenter.default.addObserver(self, selector: #selector(browserPreferenceDidChange), name: .browserPreferenceDidChange, object: nil) + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 3 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { return 1 } + if section == 1 { return 1 } + return browserManager.availableBrowsers.count - 2 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.section == 0 { + let cell = tableView.dequeueReusableCell(withIdentifier: "BrowserCell", for: indexPath) as! BrowserCell + cell.configure(with: browserManager.availableBrowsers[indexPath.row]) + return cell + } + + if indexPath.section == 1 { + let cell = tableView.dequeueReusableCell(withIdentifier: "BrowserCell", for: indexPath) as! BrowserCell + cell.configure(with: browserManager.availableBrowsers[indexPath.row + 1]) + return cell + } + + if indexPath.section == 2 { + let cell = tableView.dequeueReusableCell(withIdentifier: "BrowserCell", for: indexPath) as! BrowserCell + cell.configure(with: browserManager.availableBrowsers[indexPath.row + 2]) + return cell + } + + return tableView.dequeueReusableCell(withIdentifier: "BrowserCell") ?? UITableViewCell() + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let cell = tableView.cellForRow(at: indexPath) as? BrowserCell else { + return + } + cell.updateBrowserSelection() + navigationController?.popViewController(animated: true) + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if section == 0 { + return NSLocalizedString("Open Links In", comment: "Open Links") + } + return nil + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + if section == 0 { + return NSLocalizedString("Links will open in NetNewsWire.", comment: "NNW browser footer.") + } + + if section == 1 { + return NSLocalizedString("Links will open in the default system browser configured in Settings.", comment: "Default browser footer.") + } + + + return nil + } + + // MARK: - Notifications + @objc func browserPreferenceDidChange() { + tableView.reloadData() + } + +} diff --git a/iOS/Settings/BrowserManager.swift b/iOS/Settings/BrowserManager.swift new file mode 100644 index 000000000..f13946c8a --- /dev/null +++ b/iOS/Settings/BrowserManager.swift @@ -0,0 +1,97 @@ +// +// BrowserManager.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 22/8/21. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation +import UIKit + +extension Notification.Name { + static let browserPreferenceDidChange = Notification.Name("browserPreferenceDidChange") +} + +public final class BrowserManager { + + /// The available browsers supported by the application. If `enableExtendedBrowserPreferences` + /// is `false`, this will only contain in-app and system browser options. + public private(set) var availableBrowsers = [Browser]() + + /// The currently selected `BrowserID`. The default is `Browser.inApp`. + public var currentBrowserPreference: String { + get { + return AppDefaults.shared.browserPreference + } + set { + AppDefaults.shared.browserPreference = newValue + NotificationCenter.default.post(name: .browserPreferenceDidChange, object: nil) + } + } + + /// When `true`, the user can select a specific browser instead of the system default browser. + private var enableExtendedBrowserPreferences = false + + public static let shared = BrowserManager() + + private init() { + configureAvailableBrowsers() + } + + /// Refreshes the available browsers. This is called when BrowserManager is inited, + /// and when the application returns to the foreground. + func configureAvailableBrowsers() { + if !enableExtendedBrowserPreferences { + availableBrowsers = [.inApp, .safari] + } else { + availableBrowsers = Browser.allCases.filter({ $0.canOpenURL == true }) + } + resetBrowserPreferenceIfRequired() + } + + /// Opens the URL in the specified browser. + /// - Parameter urlString: the url to open. + func openURL(urlString: String) { + guard let url = URL(string: urlString) else { + return + } + if currentBrowser() == .safari { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + return + } + + switch currentBrowser() { + case .edge, .chrome, .opera, .onePassword: + var string = urlString + string = string.replacingOccurrences(of: "https://", with: "") + string = string.replacingOccurrences(of: "http://", with: "") + guard let browserURL = URL(string: currentBrowser().urlScheme + string) else { + return + } + UIApplication.shared.open(browserURL, options: [:], completionHandler: nil) + default: + guard let browserURL = URL(string: currentBrowser().urlScheme + urlString) else { + return + } + UIApplication.shared.open(browserURL, options: [:], completionHandler: nil) + } + } + + /// If the user has uninstalled a browser this will reset the browser back to the in-app default. + private func resetBrowserPreferenceIfRequired() { + if availableBrowsers.filter({ $0.browserID == currentBrowserPreference }).count == 0 { + currentBrowserPreference = Browser.inApp.browserID + } + } + + /// The currently selected browser. + /// - Returns: `Browser` + func currentBrowser() -> Browser { + guard let browser = Browser.allCases.filter({ $0.browserID == currentBrowserPreference }).first else { + resetBrowserPreferenceIfRequired() + return currentBrowser() + } + return browser + } +} diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 393853548..06b125e1b 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -1,9 +1,9 @@ - + - + @@ -21,7 +21,7 @@ - + @@ -42,14 +42,14 @@ - + - +