diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65cff4bd6..7ec5b8ce8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,13 @@ name: CI -on: [push] +on: + push: + branches: + - master + - mac-candidate + - mac-release + - ios-candidate + - ios-release jobs: build: diff --git a/Appcasts/netnewswire-beta.xml b/Appcasts/netnewswire-beta.xml index 4f5465294..acd2dce0a 100755 --- a/Appcasts/netnewswire-beta.xml +++ b/Appcasts/netnewswire-beta.xml @@ -6,27 +6,17 @@ Most recent NetNewsWire changes with links to updates. en - + NetNewsWire 5.0.3 Significantly enhanced performance during syncs and refreshes.

- -

When running for the first time, and the user previously used NetNewsWire 3, it will automatically import NetNewsWire 3 subscriptions instead of the defaults for new users.

- -

You can also import NetNewsWire 3 subscriptions via the new File > Import NNW3 Subscriptions… command.

- -

Fixed the space bar when running on Catalina. It wouldn’t advance to the next unread — now it will. (This was due to a change in JavaScript in Catalina.)

- -

Fixed a crashing bug having to do with async database fetches for the timeline.

- -

Periodically empties the articles cache that was added in 5.0.3b1, so its memory use doesn’t just keep expanding.

- ]]>
- Tue, 22 Oct 2019 09:20:00 -0700 - +

Same as 5.0.3b2 — just bumped the version number to 5.0.3.

+ ]]> + Tue, 22 Oct 2019 13:00:00 -0700 + 10.14.4
- + NetNewsWire 5.0.3b2 Significantly enhanced performance during syncs and refreshes.

diff --git a/Appcasts/netnewswire-release.xml b/Appcasts/netnewswire-release.xml index 38abd7a04..cb77af68b 100755 --- a/Appcasts/netnewswire-release.xml +++ b/Appcasts/netnewswire-release.xml @@ -6,6 +6,39 @@ Most recent NetNewsWire releases (not test builds). Well, we’re including test builds up until 5.0 ships, but after that it won’t be test builds. en + + NetNewsWire 5.0.3 + Significantly enhanced performance during syncs and refreshes. Fetching articles from the database is also faster.

+ +

When running for the first time, and the user previously used NetNewsWire 3, it will automatically import NetNewsWire 3 subscriptions instead of the defaults for new users.

+ +

You can also import NetNewsWire 3 subscriptions via the new File > Import NNW3 Subscriptions… command.

+ +

Keyboard shortcuts: the 's' key toggles starred status. The 'r' and 'u' keys now both toggle read status (instead of setting read and unread status, respectively).

+ +

Articles view: articles where the feed icon is quite large would be slow to render — now they render as fast as other articles.

+ +

Articles view: a bug where keyboard shortcuts wouldn’t work after giving the articles view focus has been fixed.

+ +

Articles view: YouTube videos could end up small. Fixed.

+ +

Articles view: fixed a bug scaling images to fit in the view.

+ +

Fixed the space bar when running on Catalina. It wouldn’t advance to the next unread — now it will. (This was due to a change in JavaScript in Catalina.)

+ +

Fixed a crashing bug having to do with async database fetches for the timeline.

+ +

Feedbin syncing: fixed a bug where renaming a tag on the Feedbin site would result in feeds in NNW ending up at the top level.

+ +

Help menu: fixed the expired Slack link.

+ + ]]>
+ Tue, 22 Oct 2019 13:00:00 -0700 + + 10.14.4 +
+ NetNewsWire 5.0.2 String { - let localizedText = NSLocalizedString("An error occurred while processing the \"%@\" account: %@", comment: "Unknown error") + let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error") return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay, error.localizedDescription) as String } } diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegateError.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegateError.swift index ed23a71cc..a77fcfe02 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegateError.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegateError.swift @@ -22,37 +22,37 @@ enum FeedlyAccountDelegateError: LocalizedError { var errorDescription: String? { switch self { case .notLoggedIn: - return NSLocalizedString("Please add the Feedly account again.", comment: "Feedly - Credentials not found.") + return NSLocalizedString("Please add the Feedly account again.", comment: "Feedly – Credentials not found.") case .unableToAddFolder(let name): - let template = NSLocalizedString("Could not create a folder named \"%@\".", comment: "Feedly - Could not create a folder/collection.") + let template = NSLocalizedString("Could not create a folder named “%@”.", comment: "Feedly – Could not create a folder/collection.") return String(format: template, name) case .unableToRenameFolder(let from, let to): - let template = NSLocalizedString("Could not rename \"%@\" to \"%@\".", comment: "Feedly - Could not rename a folder/collection.") + let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly – Could not rename a folder/collection.") return String(format: template, from, to) case .unableToRemoveFolder(let name): - let template = NSLocalizedString("Could not remove the folder named \"%@\".", comment: "Feedly - Could not remove a folder/collection.") + let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly – Could not remove a folder/collection.") return String(format: template, name) case .unableToMoveFeedBetweenFolders(let feed, _, let to): - let template = NSLocalizedString("Could not move \"%@\" to \"%@\".", comment: "Feedly - Could not move a feed between folders/collections.") + let template = NSLocalizedString("Could not move “%@” to “%@”.", comment: "Feedly – Could not move a feed between folders/collections.") return String(format: template, feed.nameForDisplay, to.nameForDisplay) case .addFeedChooseFolder: - return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly - Feed can only be added to folders.") + return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly – Feed can only be added to folders.") case .addFeedInvalidFolder(let invalidFolder): - let template = NSLocalizedString("Feeds cannot be added to the \"%@\" folder.", comment: "Feedly - Feed can only be added to folders.") + let template = NSLocalizedString("Feeds cannot be added to the “%@” folder.", comment: "Feedly – Feed can only be added to folders.") return String(format: template, invalidFolder.nameForDisplay) case .unableToRenameFeed(let from, let to): - let template = NSLocalizedString("Could not rename \"%@\" to \"%@\".", comment: "Feedly - Could not rename a feed.") + let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly – Could not rename a feed.") return String(format: template, from, to) case .unableToRemoveFeed(let feed): - let template = NSLocalizedString("Could not remove \"%@\".", comment: "Feedly - Could not remove a feed.") + let template = NSLocalizedString("Could not remove “%@”.", comment: "Feedly – Could not remove a feed.") return String(format: template, feed.nameForDisplay) } } @@ -72,14 +72,14 @@ enum FeedlyAccountDelegateError: LocalizedError { return nil case .unableToMoveFeedBetweenFolders(let feed, let from, let to): - let template = NSLocalizedString("\"%@\" may be in both \"%@\" and \"%@\".", comment: "Feedly - Could not move a feed between folders/collections.") + let template = NSLocalizedString("“%@” may be in both “%@” and “%@”.", comment: "Feedly – Could not move a feed between folders/collections.") return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay) case .addFeedChooseFolder: return nil case .addFeedInvalidFolder: - return NSLocalizedString("Please choose a different folder to contain the feed.", comment: "Feedly - Feed can only be added to folders recovery suggestion.") + return NSLocalizedString("Please choose a different folder to contain the feed.", comment: "Feedly – Feed can only be added to folders recovery suggestion.") case .unableToRemoveFeed: return nil diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 117285763..52eedd75e 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -13,14 +13,21 @@ import RSTree import RSWeb import Account import RSCore -#if TEST + +// If we're not going to import Sparkle, provide dummy protocols to make it easy +// for AppDelegate to comply +#if MAC_APP_STORE || TEST +protocol SPUStandardUserDriverDelegate {} +protocol SPUUpdaterDelegate {} +#else import Sparkle #endif var appDelegate: AppDelegate! @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider { +class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate +{ var userNotificationManager: UserNotificationManager! var faviconDownloader: FaviconDownloader! @@ -70,6 +77,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, private let log = Log() private let appNewsURLString = "https://nnw.ranchero.com/feed.json" private let appMovementMonitor = RSAppMovementMonitor() + #if !MAC_APP_STORE && !TEST + private var softwareUpdater: SPUUpdater! + #endif override init() { NSWindow.allowsAutomaticWindowTabbing = false @@ -117,16 +127,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, func applicationWillFinishLaunching(_ notification: Notification) { installAppleEventHandlers() - #if TEST - // Don't prompt for updates while running automated tests - SUUpdater.shared()?.automaticallyChecksForUpdates = false - #endif } func applicationDidFinishLaunching(_ note: Notification) { - #if MAC_APP_STORE + #if MAC_APP_STORE || TEST checkForUpdatesMenuItem.isHidden = true + #else + // Initialize Sparkle... + let hostBundle = Bundle.main + let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self) + self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self) + + do { + try self.softwareUpdater.start() + } + catch { + NSLog("Failed to start software updater with error: \(error)") + } #endif appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String) @@ -571,7 +589,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @IBAction func groupByFeedToggled(_ sender: NSMenuItem) { AppDefaults.timelineGroupByFeed.toggle() } - + + @IBAction func checkForUpdates(_ sender: Any?) { + #if !MAC_APP_STORE && !TEST + self.softwareUpdater.checkForUpdates() + #endif + } + } // MARK: - Debug Menu diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index 498fdd35f..ba4a9a4de 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -30,7 +30,7 @@ - + @@ -623,7 +623,6 @@ - diff --git a/Mac/Base.lproj/Preferences.storyboard b/Mac/Base.lproj/Preferences.storyboard index 46d049133..6b2ac07d5 100644 --- a/Mac/Base.lproj/Preferences.storyboard +++ b/Mac/Base.lproj/Preferences.storyboard @@ -234,17 +234,7 @@ - - - - - - - - - - - + @@ -293,7 +283,7 @@ - + @@ -382,8 +372,8 @@ - + diff --git a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift index cfc540d09..5c5022610 100644 --- a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift +++ b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift @@ -52,8 +52,8 @@ final class AccountsPreferencesViewController: NSViewController { let alert = NSAlert() alert.alertStyle = .warning let deletePrompt = NSLocalizedString("Delete", comment: "Delete") - alert.messageText = "\(deletePrompt) \"\(acctName)\"?" - alert.informativeText = NSLocalizedString("Are you sure you want to delete the account \"\(acctName)\"? This can not be undone.", comment: "Delete text") + alert.messageText = "\(deletePrompt) “\(acctName)”?" + alert.informativeText = NSLocalizedString("Are you sure you want to delete the account “\(acctName)”? This can not be undone.", comment: "Delete text") alert.addButton(withTitle: NSLocalizedString("Delete", comment: "Delete Account")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Delete Account")) diff --git a/Mac/Resources/Assets.xcassets/accountLocal.imageset/Contents.json b/Mac/Resources/Assets.xcassets/accountLocal.imageset/Contents.json index 4b7dc7acc..1d99e9ded 100644 --- a/Mac/Resources/Assets.xcassets/accountLocal.imageset/Contents.json +++ b/Mac/Resources/Assets.xcassets/accountLocal.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "accountLocal.pdf" + "filename" : "localAccountMac.pdf" } ], "info" : { diff --git a/Mac/Resources/Assets.xcassets/accountLocal.imageset/accountLocal.pdf b/Mac/Resources/Assets.xcassets/accountLocal.imageset/localAccountMac.pdf similarity index 75% rename from Mac/Resources/Assets.xcassets/accountLocal.imageset/accountLocal.pdf rename to Mac/Resources/Assets.xcassets/accountLocal.imageset/localAccountMac.pdf index c32cc3b89..5c3292ce0 100644 Binary files a/Mac/Resources/Assets.xcassets/accountLocal.imageset/accountLocal.pdf and b/Mac/Resources/Assets.xcassets/accountLocal.imageset/localAccountMac.pdf differ diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 4ec1df2d8..4102a510f 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 49F40DF82335B71000552BF4 /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; }; 49F40DF92335B71000552BF4 /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; }; - 510BD15D232D765D002692E4 /* SettingsReaderAPIAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */; }; 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */; }; 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; 5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; }; @@ -43,15 +42,8 @@ 513146C5235A8FDB00387FDC /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; }; 51314704235C41FC00387FDC /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 51314707235C41FC00387FDC /* Intents.intentdefinition */; }; 51314705235C41FC00387FDC /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 51314707235C41FC00387FDC /* Intents.intentdefinition */; }; - 51314716235C862200387FDC /* SettingsSubscriptionsImportAccountPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51314715235C862200387FDC /* SettingsSubscriptionsImportAccountPickerView.swift */; }; - 51314718235C89ED00387FDC /* SettingsSubscriptionsExportAccountPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51314717235C89ED00387FDC /* SettingsSubscriptionsExportAccountPickerView.swift */; }; - 51322855232EED360033D4ED /* VibrantSelectAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51322854232EED360033D4ED /* VibrantSelectAction.swift */; }; - 51322859232FDDB80033D4ED /* VibrantButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51322858232FDDB80033D4ED /* VibrantButtonStyle.swift */; }; - 5132285B232FF2C40033D4ED /* SettingsRefreshSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132285A232FF2C40033D4ED /* SettingsRefreshSelectionView.swift */; }; 513228FB233037630033D4ED /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513228F2233037620033D4ED /* Reachability.swift */; }; 513228FC233037630033D4ED /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513228F2233037620033D4ED /* Reachability.swift */; }; - 513229312330523F0033D4ED /* AttributedStringView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513229302330523F0033D4ED /* AttributedStringView.swift */; }; - 5132293B23305D4C0033D4ED /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132293A23305D4C0033D4ED /* SettingsAboutView.swift */; }; 513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513C5CE8232571C2003D4054 /* ShareViewController.swift */; }; 513C5CEC232571C2003D4054 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 513C5CEA232571C2003D4054 /* MainInterface.storyboard */; }; 513C5CF0232571C2003D4054 /* NetNewsWire iOS Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -81,8 +73,6 @@ 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */; }; 514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */; }; 514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7D1E23219F3C00BAC947 /* AddControllerType.swift */; }; - 5152E0F923248F6200E5C7AD /* SettingsLocalAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */; }; - 5152E1022324900D00E5C7AD /* SettingsAddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */; }; 5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; }; 51554C24228B71910055115A /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; }; 51554C25228B71910055115A /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -91,6 +81,11 @@ 515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; 515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; }; 515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; }; + 516A093723609A3600EAE89B /* SettingsAccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */; }; + 516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */; }; + 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; }; + 516A09402361240900EAE89B /* Account.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 516A093F2361240900EAE89B /* Account.storyboard */; }; + 516A09422361248000EAE89B /* Inspector.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 516A09412361248000EAE89B /* Inspector.storyboard */; }; 51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; }; 5170743A232AABFC00A461A3 /* FlattenedAccountFolderPickerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */; }; 517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; @@ -106,20 +101,23 @@ 5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; }; 518651B223555EB20078E021 /* NNW3Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518651AB23555EB20078E021 /* NNW3Document.swift */; }; 518651DA235621840078E021 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518651D9235621840078E021 /* ImageTransition.swift */; }; + 5186A635235EF3A800C97195 /* VibrantLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5186A634235EF3A800C97195 /* VibrantLabel.swift */; }; 518B2EE82351B45600400001 /* NetNewsWire_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */; }; 51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CC1230F5963006127BE /* ThemedNavigationController.swift */; }; 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; 51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; 51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; }; - 519D73FB2323FF35008BB345 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F35D0822AFD4760003CE1B /* SettingsView.swift */; }; 519D740623243CC0008BB345 /* RefreshInterval-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519D740523243CC0008BB345 /* RefreshInterval-Extensions.swift */; }; - 519D740723243FE7008BB345 /* SettingsSubscriptionsExportDocumentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194B5F122B69FCC00144881 /* SettingsSubscriptionsExportDocumentPickerView.swift */; }; - 519D740823243FEA008BB345 /* SettingsSubscriptionsImportDocumentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194B5ED22B6965300144881 /* SettingsSubscriptionsImportDocumentPickerView.swift */; }; 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; }; - 51AF45E123246731001742EF /* SettingsAccountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */; }; - 51AF460323247321001742EF /* SettingsDetailAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */; }; - 51AF460C23247F11001742EF /* SettingsFeedbinAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */; }; + 51A16997235E10D700EB091F /* RefreshIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */; }; + 51A16999235E10D700EB091F /* LocalAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */; }; + 51A1699A235E10D700EB091F /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51A16990235E10D600EB091F /* Settings.storyboard */; }; + 51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */; }; + 51A1699C235E10D700EB091F /* AddAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16992235E10D600EB091F /* AddAccountViewController.swift */; }; + 51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16993235E10D600EB091F /* SettingsViewController.swift */; }; + 51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; }; + 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; }; 51AF460E232488C6001742EF /* Account-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF460D232488C6001742EF /* Account-Extensions.swift */; }; 51B62E68233186730085F949 /* MasterTimelineAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B62E67233186730085F949 /* MasterTimelineAvatarView.swift */; }; 51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; }; @@ -199,9 +197,6 @@ 51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */; }; 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; - 51E149B3234D82E40004F7A5 /* PasswordField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E149B2234D82E40004F7A5 /* PasswordField.swift */; }; - 51E149C0234D839E0004F7A5 /* ShowHidePasswordView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */; }; - 51E149C2234D852F0004F7A5 /* ShowHidePasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */; }; 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; }; 51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* ErrorHandler.swift */; }; 51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; }; @@ -239,9 +234,10 @@ 51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; 51FE10092346739D0056195D /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; 51FE100A234673A00056195D /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; + 51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; - 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; }; + 5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */; }; 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; }; 6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */; }; 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */; }; @@ -622,7 +618,6 @@ D5F4EDB720074D6500B9E363 /* Feed+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB620074D6500B9E363 /* Feed+Scriptability.swift */; }; D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */; }; DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82AB09231003F6002269DF /* SharingTests.swift */; }; - DF999FF722B5AEFA0064B687 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF999FF622B5AEFA0064B687 /* SafariView.swift */; }; FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleSorterTests.swift */; }; FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; @@ -1200,10 +1195,6 @@ /* Begin PBXFileReference section */ 49F40DEF2335B71000552BF4 /* newsfoot.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = newsfoot.js; sourceTree = ""; }; - 510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddAccountView.swift; sourceTree = ""; }; - 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLocalAccountView.swift; sourceTree = ""; }; - 510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFeedbinAccountView.swift; sourceTree = ""; }; - 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountLabelView.swift; sourceTree = ""; }; 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; 51121AA12265430A00BC0EC1 /* NetNewsWire_iOSapp_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSapp_target.xcconfig; sourceTree = ""; }; 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = ""; }; @@ -1224,14 +1215,7 @@ 513146B1235A81A400387FDC /* AddFeedIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedIntentHandler.swift; sourceTree = ""; }; 51314706235C41FC00387FDC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; 51314714235C420900387FDC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intents.strings; sourceTree = ""; }; - 51314715235C862200387FDC /* SettingsSubscriptionsImportAccountPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsImportAccountPickerView.swift; sourceTree = ""; }; - 51314717235C89ED00387FDC /* SettingsSubscriptionsExportAccountPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsExportAccountPickerView.swift; sourceTree = ""; }; - 51322854232EED360033D4ED /* VibrantSelectAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantSelectAction.swift; sourceTree = ""; }; - 51322858232FDDB80033D4ED /* VibrantButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButtonStyle.swift; sourceTree = ""; }; - 5132285A232FF2C40033D4ED /* SettingsRefreshSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRefreshSelectionView.swift; sourceTree = ""; }; 513228F2233037620033D4ED /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; - 513229302330523F0033D4ED /* AttributedStringView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringView.swift; sourceTree = ""; }; - 5132293A23305D4C0033D4ED /* SettingsAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = ""; }; 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 513C5CE8232571C2003D4054 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; 513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; @@ -1257,6 +1241,11 @@ 515D4FCB2325815A00EE1167 /* SafariExt.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SafariExt.js; sourceTree = ""; }; 515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_ShareExtension.entitlements; sourceTree = ""; }; 515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = ""; }; + 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsAccountTableViewCell.xib; sourceTree = ""; }; + 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountTableViewCell.swift; sourceTree = ""; }; + 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = ""; }; + 516A093F2361240900EAE89B /* Account.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Account.storyboard; sourceTree = ""; }; + 516A09412361248000EAE89B /* Inspector.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Inspector.storyboard; sourceTree = ""; }; 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = ""; }; 517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = ""; }; 517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewControllerWebViewProvider.swift; sourceTree = ""; }; @@ -1268,16 +1257,23 @@ 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRefreshTimer.swift; sourceTree = ""; }; 518651AB23555EB20078E021 /* NNW3Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NNW3Document.swift; sourceTree = ""; }; 518651D9235621840078E021 /* ImageTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTransition.swift; sourceTree = ""; }; + 5186A634235EF3A800C97195 /* VibrantLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantLabel.swift; sourceTree = ""; }; 518B2ED22351B3DD00400001 /* NetNewsWire-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "NetNewsWire-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 518B2EE92351B4C200400001 /* NetNewsWire_iOSTests_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSTests_target.xcconfig; sourceTree = ""; }; 51934CC1230F5963006127BE /* ThemedNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedNavigationController.swift; sourceTree = ""; }; 51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = ""; }; 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = ""; }; - 5194B5ED22B6965300144881 /* SettingsSubscriptionsImportDocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsImportDocumentPickerView.swift; sourceTree = ""; }; - 5194B5F122B69FCC00144881 /* SettingsSubscriptionsExportDocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsExportDocumentPickerView.swift; sourceTree = ""; }; 519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = ""; }; 519D740523243CC0008BB345 /* RefreshInterval-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RefreshInterval-Extensions.swift"; sourceTree = ""; }; 519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = ""; }; + 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalAccountViewController.swift; sourceTree = ""; }; + 51A16990235E10D600EB091F /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; + 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountInspectorViewController.swift; sourceTree = ""; }; + 51A16992235E10D600EB091F /* AddAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddAccountViewController.swift; sourceTree = ""; }; + 51A16993235E10D600EB091F /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; + 51A16995235E10D600EB091F /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; + 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = ""; }; 51AF460D232488C6001742EF /* Account-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account-Extensions.swift"; sourceTree = ""; }; 51B62E67233186730085F949 /* MasterTimelineAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineAvatarView.swift; sourceTree = ""; }; 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = ""; }; @@ -1306,9 +1302,6 @@ 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; - 51E149B2234D82E40004F7A5 /* PasswordField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordField.swift; sourceTree = ""; }; - 51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowHidePasswordView.xib; sourceTree = ""; }; - 51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowHidePasswordView.swift; sourceTree = ""; }; 51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; 51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleStatusSyncTimer.swift; sourceTree = ""; }; @@ -1322,8 +1315,6 @@ 51EF0F8D2279C9260050506E /* AccountsAdd.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsAdd.xib; sourceTree = ""; }; 51EF0F8F2279C9500050506E /* AccountsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddViewController.swift; sourceTree = ""; }; 51EF0F912279CA620050506E /* AccountsAddTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddTableCellView.swift; sourceTree = ""; }; - 51F35D0822AFD4760003CE1B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDetailAccountView.swift; sourceTree = ""; }; 51F85BEA22724CB600C787DC /* About.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = ""; }; 51F85BEC227251DF00C787DC /* Acknowledgments.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Acknowledgments.rtf; sourceTree = ""; }; 51F85BEE2272520B00C787DC /* Thanks.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Thanks.rtf; sourceTree = ""; }; @@ -1341,10 +1332,10 @@ 51FD40BD2341555600880194 /* UIImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage-Extensions.swift"; sourceTree = ""; }; 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineUnreadCountView.swift; sourceTree = ""; }; 51FE10022345529D0056195D /* UserNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationManager.swift; sourceTree = ""; }; - 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsReaderAPIAccountView.swift; sourceTree = ""; }; + 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = ""; }; 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = ""; }; 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = ""; }; - 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NNWTableViewCell.swift; sourceTree = ""; }; + 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantTableViewCell.swift; sourceTree = ""; }; 6543108B2322D90900658221 /* common */ = {isa = PBXFileReference; lastKnownFileType = folder; path = common; sourceTree = ""; }; 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 6581C73420CED60100F4AD34 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; @@ -1547,7 +1538,6 @@ D5F4EDB620074D6500B9E363 /* Feed+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feed+Scriptability.swift"; sourceTree = ""; }; D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = ""; }; DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = ""; }; - DF999FF622B5AEFA0064B687 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = ""; }; FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = ""; }; FFD43E372340F320009E5CA3 /* UndoAvailableAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoAvailableAlertController.swift; sourceTree = ""; }; @@ -1682,6 +1672,8 @@ 5123DB95233EC69300282CC9 /* Inspector */ = { isa = PBXGroup; children = ( + 516A09412361248000EAE89B /* Inspector.storyboard */, + 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */, 5123DB9E233EC6FD00282CC9 /* FeedInspectorView.swift */, ); path = Inspector; @@ -1764,15 +1756,12 @@ name = Products; sourceTree = ""; }; - 515E4F06232506240057B0E7 /* Account */ = { + 516A093E236123A800EAE89B /* Account */ = { isa = PBXGroup; children = ( - 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */, - 510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */, - 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */, - 510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */, - 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */, - 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */, + 516A093F2361240900EAE89B /* Account.storyboard */, + 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */, + 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */, ); path = Account; sourceTree = ""; @@ -1799,14 +1788,14 @@ 5183CCEB227117C70010922C /* Settings */ = { isa = PBXGroup; children = ( - 51F35D0822AFD4760003CE1B /* SettingsView.swift */, - 5132293A23305D4C0033D4ED /* SettingsAboutView.swift */, - 5132285A232FF2C40033D4ED /* SettingsRefreshSelectionView.swift */, - 51314717235C89ED00387FDC /* SettingsSubscriptionsExportAccountPickerView.swift */, - 5194B5F122B69FCC00144881 /* SettingsSubscriptionsExportDocumentPickerView.swift */, - 51314715235C862200387FDC /* SettingsSubscriptionsImportAccountPickerView.swift */, - 5194B5ED22B6965300144881 /* SettingsSubscriptionsImportDocumentPickerView.swift */, - 515E4F06232506240057B0E7 /* Account */, + 51A16990235E10D600EB091F /* Settings.storyboard */, + 51A16995235E10D600EB091F /* AboutViewController.swift */, + 51A16992235E10D600EB091F /* AddAccountViewController.swift */, + 51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */, + 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */, + 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */, + 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */, + 51A16993235E10D600EB091F /* SettingsViewController.swift */, ); path = Settings; sourceTree = ""; @@ -1831,20 +1820,6 @@ path = Activity; sourceTree = ""; }; - 5194B5E222B693EC00144881 /* SwiftUI Extensions */ = { - isa = PBXGroup; - children = ( - 513229302330523F0033D4ED /* AttributedStringView.swift */, - DF999FF622B5AEFA0064B687 /* SafariView.swift */, - 51322858232FDDB80033D4ED /* VibrantButtonStyle.swift */, - 51322854232EED360033D4ED /* VibrantSelectAction.swift */, - 51E149B2234D82E40004F7A5 /* PasswordField.swift */, - 51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */, - 51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */, - ); - path = "SwiftUI Extensions"; - sourceTree = ""; - }; 519D740423243C68008BB345 /* Model Extensions */ = { isa = PBXGroup; children = ( @@ -1859,7 +1834,6 @@ children = ( 51F85BFA2275D85000C787DC /* Array-Extensions.swift */, 51F85BF42273625800C787DC /* Bundle-Extensions.swift */, - 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */, 51EAED95231363EF00A9EEE3 /* NonIntrinsicButton.swift */, 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */, 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */, @@ -1870,6 +1844,9 @@ 51FD40BD2341555600880194 /* UIImage-Extensions.swift */, 512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */, 51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */, + 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */, + 5186A634235EF3A800C97195 /* VibrantLabel.swift */, + 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */, ); path = "UIKit Extensions"; sourceTree = ""; @@ -2557,13 +2534,13 @@ 51C4525D226508F600C03939 /* MasterFeed */, 51C4526D2265091600C03939 /* MasterTimeline */, 51C4527D2265092C00C03939 /* Article */, + 516A093E236123A800EAE89B /* Account */, 51C452802265093600C03939 /* Add */, 5123DB95233EC69300282CC9 /* Inspector */, 513145F9235A55A700387FDC /* Intents */, 5183CCDB226F1EEB0010922C /* Progress */, 5183CCEB227117C70010922C /* Settings */, 519D740423243C68008BB345 /* Model Extensions */, - 5194B5E222B693EC00144881 /* SwiftUI Extensions */, 51C45245226506C800C03939 /* UIKit Extensions */, 513C5CE7232571C2003D4054 /* ShareExtension */, 51314643235A7C2300387FDC /* IntentsExtension */, @@ -2953,6 +2930,14 @@ DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Automatic; }; + 65ED3FA2235DEF6C0081F399 = { + DevelopmentTeam = SHJK2V3AJG; + ProvisioningStyle = Automatic; + }; + 65ED4090235DEF770081F399 = { + DevelopmentTeam = SHJK2V3AJG; + ProvisioningStyle = Automatic; + }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; DevelopmentTeam = SHJK2V3AJG; @@ -3402,16 +3387,20 @@ 511D43D2231FA62C00FB1562 /* GlobalKeyboardShortcuts.plist in Resources */, 84C9FCA12262A1B300D921D6 /* Main.storyboard in Resources */, 51BB7C312335ACDE008E8144 /* page.html in Resources */, + 516A093723609A3600EAE89B /* SettingsAccountTableViewCell.xib in Resources */, 51F85BF32272531500C787DC /* Dedication.rtf in Resources */, + 516A09422361248000EAE89B /* Inspector.storyboard in Resources */, 84C9FCA42262A1B800D921D6 /* LaunchScreenPhone.storyboard in Resources */, 51F85BEB22724CB600C787DC /* About.rtf in Resources */, 51F85BED227251DF00C787DC /* Acknowledgments.rtf in Resources */, + 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */, 511D43D1231FA62800FB1562 /* SidebarKeyboardShortcuts.plist in Resources */, + 516A09402361240900EAE89B /* Account.storyboard in Resources */, 51C452AB22650DC600C03939 /* template.html in Resources */, - 51E149C0234D839E0004F7A5 /* ShowHidePasswordView.xib in Resources */, 51F85BF12272524100C787DC /* Credits.rtf in Resources */, 84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */, 511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */, + 51A1699A235E10D700EB091F /* Settings.storyboard in Resources */, 49F40DF92335B71000552BF4 /* newsfoot.js in Resources */, 51F85BEF2272520B00C787DC /* Thanks.rtf in Resources */, 84C9FC9D2262A1A900D921D6 /* Assets.xcassets in Resources */, @@ -3881,14 +3870,13 @@ buildActionMask = 2147483647; files = ( 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */, - 51E149B3234D82E40004F7A5 /* PasswordField.swift in Sources */, 512E08E72268801200BDCFDD /* FeedTreeControllerDelegate.swift in Sources */, 51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */, 51EF0F79227716380050506E /* ColorHash.swift in Sources */, 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */, 51EAED96231363EF00A9EEE3 /* NonIntrinsicButton.swift in Sources */, 51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */, - 5152E1022324900D00E5C7AD /* SettingsAddAccountView.swift in Sources */, + 5186A635235EF3A800C97195 /* VibrantLabel.swift in Sources */, 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */, 51B62E68233186730085F949 /* MasterTimelineAvatarView.swift in Sources */, 51C45296226509D300C03939 /* OPMLExporter.swift in Sources */, @@ -3902,40 +3890,34 @@ 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */, 513146B2235A81A400387FDC /* AddFeedIntentHandler.swift in Sources */, 5183CCDD226F1F5C0010922C /* NavigationProgressView.swift in Sources */, - 51AF45E123246731001742EF /* SettingsAccountLabelView.swift in Sources */, 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */, 51C452772265091600C03939 /* MultilineUILabelSizer.swift in Sources */, 51C452A522650A2D00C03939 /* SmallIconProvider.swift in Sources */, + 516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */, 51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */, - 51322859232FDDB80033D4ED /* VibrantButtonStyle.swift in Sources */, + 51A1699C235E10D700EB091F /* AddAccountViewController.swift in Sources */, + 51A16999235E10D700EB091F /* LocalAccountViewController.swift in Sources */, 514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */, - 5152E0F923248F6200E5C7AD /* SettingsLocalAccountView.swift in Sources */, 51FA73A52332BE110090D516 /* ArticleExtractor.swift in Sources */, 51314704235C41FC00387FDC /* Intents.intentdefinition in Sources */, FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */, - 510BD15D232D765D002692E4 /* SettingsReaderAPIAccountView.swift in Sources */, 51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */, 51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */, 51FA73AB2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */, - 5132285B232FF2C40033D4ED /* SettingsRefreshSelectionView.swift in Sources */, 51C452852265093600C03939 /* FlattenedAccountFolderPickerData.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, 5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */, 51FD40C72341555A00880194 /* UIImage-Extensions.swift in Sources */, - 5132293B23305D4C0033D4ED /* SettingsAboutView.swift in Sources */, 84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */, 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */, 51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */, 51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */, - 519D740823243FEA008BB345 /* SettingsSubscriptionsImportDocumentPickerView.swift in Sources */, 51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */, - 51AF460C23247F11001742EF /* SettingsFeedbinAccountView.swift in Sources */, 51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */, - 519D740723243FE7008BB345 /* SettingsSubscriptionsExportDocumentPickerView.swift in Sources */, 51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */, 5183CCDF226F1FCC0010922C /* UINavigationController+Progress.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, - 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */, + 5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */, 512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */, 51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */, 51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */, @@ -3953,7 +3935,6 @@ 51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */, 51C4527F2265092C00C03939 /* ArticleViewController.swift in Sources */, 51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */, - 51E149C2234D852F0004F7A5 /* ShowHidePasswordView.swift in Sources */, 51C452AE2265104D00C03939 /* ArticleStringFormatter.swift in Sources */, 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */, 51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */, @@ -3966,28 +3947,28 @@ 84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, + 51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */, 51C45290226509C100C03939 /* PseudoFeed.swift in Sources */, 51C452A922650DC600C03939 /* ArticleRenderer.swift in Sources */, - 51AF460323247321001742EF /* SettingsDetailAccountView.swift in Sources */, 5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */, 51C45297226509E300C03939 /* DefaultFeedsImporter.swift in Sources */, 512E094D2268B8AB00BDCFDD /* DeleteCommand.swift in Sources */, 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */, 51C452AC22650FD200C03939 /* AppNotifications.swift in Sources */, 51EF0F7E2277A57D0050506E /* MasterTimelineAccessibilityCellLayout.swift in Sources */, + 51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */, 51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */, - 51314718235C89ED00387FDC /* SettingsSubscriptionsExportAccountPickerView.swift in Sources */, 5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */, 51C452882265093600C03939 /* AddFeedViewController.swift in Sources */, + 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */, 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */, 518651DA235621840078E021 /* ImageTransition.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, - DF999FF722B5AEFA0064B687 /* SafariView.swift in Sources */, + 51A16997235E10D700EB091F /* RefreshIntervalViewController.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */, 519D740623243CC0008BB345 /* RefreshInterval-Extensions.swift in Sources */, - 51322855232EED360033D4ED /* VibrantSelectAction.swift in Sources */, 51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */, 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */, 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, @@ -3998,15 +3979,14 @@ 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */, + 51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */, 513228FC233037630033D4ED /* Reachability.swift in Sources */, 51C45259226508D300C03939 /* AppDefaults.swift in Sources */, - 519D73FB2323FF35008BB345 /* SettingsView.swift in Sources */, 511D4419231FC02D00FB1562 /* KeyboardManager.swift in Sources */, + 51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */, 51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */, - 513229312330523F0033D4ED /* AttributedStringView.swift in Sources */, 51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */, 51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */, - 51314716235C862200387FDC /* SettingsSubscriptionsImportAccountPickerView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/README.md b/README.md index 4cf518fe3..b8f9fe4d7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ See the [Contributing](CONTRIBUTING.md) page for our process. It’s pretty stra ```bash git clone https://github.com/brentsimmons/NetNewsWire.git cd NetNewsWire -git submodule update --init +git submodule update --init --recursive ``` You can locally override the Xcode settings for code signing diff --git a/iOS/Account/Account.storyboard b/iOS/Account/Account.storyboard new file mode 100644 index 000000000..b5f05cc65 --- /dev/null +++ b/iOS/Account/Account.storyboard @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/Account/FeedbinAccountViewController.swift b/iOS/Account/FeedbinAccountViewController.swift new file mode 100644 index 000000000..3196b5880 --- /dev/null +++ b/iOS/Account/FeedbinAccountViewController.swift @@ -0,0 +1,156 @@ +// +// FeedbinAccountViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 5/19/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import Account +import RSWeb + +class FeedbinAccountViewController: UITableViewController { + + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + @IBOutlet weak var cancelBarButtonItem: UIBarButtonItem! + @IBOutlet weak var emailTextField: UITextField! + @IBOutlet weak var passwordTextField: UITextField! + @IBOutlet weak var showHideButton: UIButton! + @IBOutlet weak var actionButton: UIButton! + + weak var account: Account? + weak var delegate: AddAccountDismissDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + activityIndicator.isHidden = true + emailTextField.delegate = self + passwordTextField.delegate = self + + if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) { + actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) + emailTextField.text = credentials.username + passwordTextField.text = credentials.secret + } else { + actionButton.setTitle(NSLocalizedString("Add Account", comment: "Update Credentials"), for: .normal) + } + + NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: emailTextField) + NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField) + } + + @IBAction func cancel(_ sender: Any) { + dismiss(animated: true, completion: nil) + delegate?.dismiss() + } + + @IBAction func showHidePassword(_ sender: Any) { + if passwordTextField.isSecureTextEntry { + passwordTextField.isSecureTextEntry = false + showHideButton.setTitle("Hide", for: .normal) + } else { + passwordTextField.isSecureTextEntry = true + showHideButton.setTitle("Show", for: .normal) + } + } + + @IBAction func action(_ sender: Any) { + + guard let email = emailTextField.text, let password = passwordTextField.text else { + showError(NSLocalizedString("Username & password required.", comment: "Credentials Error")) + return + } + + startAnimatingActivityIndicator() + disableNavigation() + + // When you fill in the email address via auto-complete it adds extra whitespace + let trimmedEmail = email.trimmingCharacters(in: .whitespaces) + let credentials = Credentials(type: .basic, username: trimmedEmail, secret: password) + Account.validateCredentials(type: .feedbin, credentials: credentials) { result in + + self.stopAnimtatingActivityIndicator() + self.enableNavigation() + + switch result { + case .success(let credentials): + if let credentials = credentials { + var newAccount = false + if self.account == nil { + self.account = AccountManager.shared.createAccount(type: .feedbin) + newAccount = true + } + + do { + + do { + try self.account?.removeCredentials(type: .basic) + } catch {} + try self.account?.storeCredentials(credentials) + + if newAccount { + self.account?.refreshAll() { result in + switch result { + case .success: + break + case .failure(let error): + self.presentError(error) + } + } + } + + self.dismiss(animated: true, completion: nil) + self.delegate?.dismiss() + } catch { + self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")) + } + } else { + self.showError(NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error")) + } + case .failure: + self.showError(NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")) + } + + } + } + + @objc func textDidChange(_ note: Notification) { + actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false) + } + + private func showError(_ message: String) { + presentError(title: "Error", message: message) + } + + private func enableNavigation() { + self.cancelBarButtonItem.isEnabled = true + self.actionButton.isEnabled = true + } + + private func disableNavigation() { + cancelBarButtonItem.isEnabled = false + actionButton.isEnabled = false + } + + private func startAnimatingActivityIndicator() { + activityIndicator.isHidden = false + activityIndicator.startAnimating() + } + + private func stopAnimtatingActivityIndicator() { + self.activityIndicator.isHidden = true + self.activityIndicator.stopAnimating() + } + +} + +extension FeedbinAccountViewController: UITextFieldDelegate { + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + +} diff --git a/iOS/Account/LocalAccountViewController.swift b/iOS/Account/LocalAccountViewController.swift new file mode 100644 index 000000000..5abf1f436 --- /dev/null +++ b/iOS/Account/LocalAccountViewController.swift @@ -0,0 +1,45 @@ +// +// LocalAccountViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 5/19/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import Account + +class LocalAccountViewController: UITableViewController { + + @IBOutlet weak var nameTextField: UITextField! + + weak var delegate: AddAccountDismissDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = Account.defaultLocalAccountName + nameTextField.delegate = self + } + + @IBAction func cancel(_ sender: Any) { + dismiss(animated: true, completion: nil) + delegate?.dismiss() + } + + @IBAction func add(_ sender: Any) { + let account = AccountManager.shared.createAccount(type: .onMyMac) + account.name = nameTextField.text + dismiss(animated: true, completion: nil) + delegate?.dismiss() + } + +} + +extension LocalAccountViewController: UITextFieldDelegate { + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + +} diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 2ca4fda10..ca3806fd9 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -7,9 +7,26 @@ // import UIKit import RSCore +import Account struct AppAssets { + static var accountLocalPadImage: UIImage = { + return UIImage(named: "accountLocalPad")! + }() + + static var accountLocalPhoneImage: UIImage = { + return UIImage(named: "accountLocalPhone")! + }() + + static var accountFeedbinImage: UIImage = { + return UIImage(named: "accountFeedbin")! + }() + + static var accountFreshRSSImage: UIImage = { + return UIImage(named: "accountFreshRSS")! + }() + static var articleExtractorError: UIImage = { return UIImage(named: "articleExtractorError")! }() @@ -60,6 +77,10 @@ struct AppAssets { return UIImage(systemName: "doc.on.doc")! }() + static var deactivateImage: UIImage = { + UIImage(systemName: "minus.circle")! + }() + static var editImage: UIImage = { UIImage(systemName: "square.and.pencil")! }() @@ -140,10 +161,6 @@ struct AppAssets { return UIImage(systemName: "star.fill")! }() - static var tableViewCellHighlightedTextColor: UIColor = { - return UIColor(named: "tableViewCellHighlightedTextColor")! - }() - static var timelineStarImage: UIImage = { let image = UIImage(systemName: "star.fill")! return image.withTintColor(AppAssets.starColor, renderingMode: .alwaysOriginal) @@ -161,4 +178,25 @@ struct AppAssets { return UIImage(systemName: "largecircle.fill.circle")! }() + static var vibrantTextColor: UIColor = { + return UIColor(named: "vibrantTextColor")! + }() + + static func image(for accountType: AccountType) -> UIImage? { + switch accountType { + case .onMyMac: + if UIDevice.current.userInterfaceIdiom == .pad { + return AppAssets.accountLocalPadImage + } else { + return AppAssets.accountLocalPhoneImage + } + case .feedbin: + return AppAssets.accountFeedbinImage + case .freshRSS: + return AppAssets.accountFreshRSSImage + default: + return nil + } + } + } diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 81a58a09c..31d006641 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -106,7 +106,7 @@ struct AppDefaults { let defaults: [String : Any] = [Key.lastImageCacheFlushDate: Date(), Key.refreshInterval: RefreshInterval.everyHour.rawValue, Key.timelineGroupByFeed: false, - Key.timelineNumberOfLines: 3, + Key.timelineNumberOfLines: 2, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, Key.displayUndoAvailableTip: true] AppDefaults.shared.register(defaults: defaults) diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index df20db9ac..994202c66 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -121,7 +121,7 @@ - + diff --git a/iOS/Inspector/AccountInspectorViewController.swift b/iOS/Inspector/AccountInspectorViewController.swift new file mode 100644 index 000000000..853670755 --- /dev/null +++ b/iOS/Inspector/AccountInspectorViewController.swift @@ -0,0 +1,132 @@ +// +// AccountInspectorViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 5/17/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import Account + +class AccountInspectorViewController: UITableViewController { + + static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) + + @IBOutlet weak var nameTextField: UITextField! + @IBOutlet weak var activeSwitch: UISwitch! + + var isModal = false + weak var account: Account? + + override func viewDidLoad() { + super.viewDidLoad() + + guard let account = account else { return } + + nameTextField.placeholder = account.defaultName + nameTextField.text = account.name + nameTextField.delegate = self + activeSwitch.isOn = account.isActive + + navigationItem.title = account.nameForDisplay + + if isModal { + let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done)) + navigationItem.leftBarButtonItem = doneBarButtonItem + } + + } + + override func viewWillDisappear(_ animated: Bool) { + account?.name = nameTextField.text + account?.isActive = activeSwitch.isOn + } + + @objc func done() { + dismiss(animated: true) + } + + @IBAction func credentials(_ sender: Any) { + guard let account = account else { return } + switch account.type { + case .feedbin: + let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController + let addViewController = navController.topViewController as! FeedbinAccountViewController + addViewController.account = account + navController.modalPresentationStyle = .currentContext + present(navController, animated: true) + default: + break + } + } + + @IBAction func deleteAccount(_ sender: Any) { + let title = NSLocalizedString("Delete Account", comment: "Delete Account") + let message = NSLocalizedString("Are you sure you want to delete this account? This can not be undone.", comment: "Delete Account") + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") + let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) + alertController.addAction(cancelAction) + + let markTitle = NSLocalizedString("Delete", comment: "Delete") + let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in + guard let account = self?.account else { return } + AccountManager.shared.deleteAccount(account) + self?.navigationController?.popViewController(animated: true) + } + alertController.addAction(markAction) + + present(alertController, animated: true) + } + +} + +extension AccountInspectorViewController { + + override func numberOfSections(in tableView: UITableView) -> Int { + guard let account = account else { return 0 } + + if account == AccountManager.shared.defaultAccount { + return 1 + } else if account.type == .onMyMac { + return 2 + } else { + return super.numberOfSections(in: tableView) + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell + + if indexPath.section == 1, let account = account, account.type == .onMyMac { + cell = super.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 2)) + } else { + cell = super.tableView(tableView, cellForRowAt: indexPath) + } + + return cell + } + + override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + if indexPath.section > 0 { + return true + } + return false + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + } + +} + +extension AccountInspectorViewController: UITextFieldDelegate { + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + +} diff --git a/iOS/Inspector/Inspector.storyboard b/iOS/Inspector/Inspector.storyboard new file mode 100644 index 000000000..e87fb1179 --- /dev/null +++ b/iOS/Inspector/Inspector.storyboard @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift index 5f684ce8f..5dc3e3c3f 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCell.swift @@ -15,7 +15,7 @@ protocol MasterFeedTableViewCellDelegate: class { func disclosureSelected(_ sender: MasterFeedTableViewCell, expanding: Bool) } -class MasterFeedTableViewCell : NNWTableViewCell { +class MasterFeedTableViewCell : VibrantTableViewCell { weak var delegate: MasterFeedTableViewCellDelegate? @@ -128,23 +128,17 @@ class MasterFeedTableViewCell : NNWTableViewCell { override func applyThemeProperties() { super.applyThemeProperties() - titleView.highlightedTextColor = AppAssets.tableViewCellHighlightedTextColor + titleView.highlightedTextColor = AppAssets.vibrantTextColor } override func setHighlighted(_ highlighted: Bool, animated: Bool) { super.setHighlighted(highlighted, animated: animated) - - let tintColor = isHighlighted || isSelected ? AppAssets.tableViewCellHighlightedTextColor : AppAssets.secondaryAccentColor - disclosureButton?.tintColor = tintColor - faviconImageView.tintColor = tintColor + updateVibrancy(animated: animated) } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) - - let tintColor = isHighlighted || isSelected ? AppAssets.tableViewCellHighlightedTextColor : AppAssets.secondaryAccentColor - disclosureButton?.tintColor = tintColor - faviconImageView.tintColor = tintColor + updateVibrancy(animated: animated) } override func willTransition(to state: UITableViewCell.StateMask) { @@ -201,5 +195,14 @@ private extension MasterFeedTableViewCell { disclosureButton?.isHidden = !isDisclosureAvailable separatorInset = layout.separatorInsets } + + func updateVibrancy(animated: Bool) { + let tintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : AppAssets.secondaryAccentColor + let duration = animated ? 0.6 : 0.0 + UIView.animate(withDuration: duration) { + self.disclosureButton?.tintColor = tintColor + self.faviconImageView.tintColor = tintColor + } + } } diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift index 2df98fe02..b60177efb 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewCellLayout.swift @@ -17,6 +17,7 @@ struct MasterFeedTableViewCellLayout { private static let unreadCountMarginLeft = CGFloat(integerLiteral: 8) private static let unreadCountMarginRight = CGFloat(integerLiteral: 16) private static let disclosureButtonSize = CGSize(width: 44, height: 44) + private static let verticalPadding = CGFloat(integerLiteral: 11) private static let minRowHeight = CGFloat(integerLiteral: 44) @@ -53,7 +54,7 @@ struct MasterFeedTableViewCellLayout { var rFavicon = CGRect.zero if !shouldShowDisclosure { let x = bounds.origin.x + ((MasterFeedTableViewCellLayout.disclosureButtonSize.width - MasterFeedTableViewCellLayout.imageSize.width) / 2) - let y = UIFontMetrics.default.scaledValue(for: CGFloat(integerLiteral: 4)) + let y = UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding) rFavicon = CGRect(x: x, y: y, width: MasterFeedTableViewCellLayout.imageSize.width, height: MasterFeedTableViewCellLayout.imageSize.height) } @@ -82,31 +83,26 @@ struct MasterFeedTableViewCellLayout { let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth))) let rLabelx = bounds.minX + MasterFeedTableViewCellLayout.disclosureButtonSize.width - var rLabel = CGRect(x: rLabelx, y: 0.0, width: labelSizeInfo.size.width, height: labelSizeInfo.size.height) + let rLabely = UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding) + let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelSizeInfo.size.width, height: labelSizeInfo.size.height) // Determine cell height - var cellHeight = [rFavicon, rLabel, rUnread, rDisclosure].maxY() + let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding) + let maxGraphicsHeight = [rFavicon, rUnread, rDisclosure].maxY() + var cellHeight = max(paddedLabelHeight, maxGraphicsHeight) if cellHeight < MasterFeedTableViewCellLayout.minRowHeight { cellHeight = MasterFeedTableViewCellLayout.minRowHeight } // Center in Cell let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight) - - if !shouldShowDisclosure && labelSizeInfo.numberOfLinesUsed == 1 { - rFavicon = MasterFeedTableViewCellLayout.centerVertically(rFavicon, newBounds) - } - if !unreadCountIsHidden { rUnread = MasterFeedTableViewCellLayout.centerVertically(rUnread, newBounds) } - if shouldShowDisclosure { rDisclosure = MasterFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds) } - rLabel = MasterFeedTableViewCellLayout.centerVertically(rLabel, newBounds) - // Assign the properties self.height = cellHeight self.faviconRect = rFavicon diff --git a/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeader.swift b/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeader.swift index 901184ec6..07d6ab35c 100644 --- a/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeader.swift +++ b/iOS/MasterFeed/Cell/MasterFeedTableViewSectionHeader.swift @@ -29,7 +29,7 @@ class MasterFeedTableViewSectionHeader: UITableViewHeaderFooterView { set { if unreadCountView.unreadCount != newValue { unreadCountView.unreadCount = newValue - unreadCountView.isHidden = (newValue < 1) + updateUnreadCountView() setNeedsLayout() } } @@ -51,6 +51,7 @@ class MasterFeedTableViewSectionHeader: UITableViewHeaderFooterView { var disclosureExpanded = false { didSet { updateDisclosureImage() + updateUnreadCountView() } } @@ -141,6 +142,14 @@ private extension MasterFeedTableViewSectionHeader { } } } + + func updateUnreadCountView() { + if !disclosureExpanded && unreadCount > 0 { + unreadCountView.isHidden = false + } else { + self.unreadCountView.isHidden = true + } + } func addSubviewAtInit(_ view: UIView) { addSubview(view) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 7022f7a7f..15a86ba80 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -11,7 +11,6 @@ import Account import Articles import RSCore import RSTree -import SwiftUI class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @@ -68,7 +67,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { override func viewWillAppear(_ animated: Bool) { navigationController?.title = NSLocalizedString("Feeds", comment: "Feeds") - clearsSelectionOnViewWillAppear = coordinator.isRootSplitCollapsed applyChanges(animate: false) super.viewWillAppear(animated) } @@ -184,6 +182,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:))) headerView.addGestureRecognizer(tap) + if section != 0 { + headerView.addInteraction(UIContextMenuInteraction(delegate: self)) + } + return headerView } @@ -451,7 +453,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { func restoreSelectionIfNecessary(adjustScroll: Bool) { if let indexPath = coordinator.masterFeedIndexPathForCurrentTimeline() { if adjustScroll { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false, deselect: coordinator.isRootSplitCollapsed) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false) } else { tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) } @@ -462,7 +464,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { if dataSource.snapshot().numberOfItems > 0 { if let indexPath = coordinator.currentFeedIndexPath { if tableView.indexPathForSelectedRow != indexPath { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true, deselect: coordinator.isRootSplitCollapsed) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true) } } else { tableView.selectRow(at: nil, animated: true, scrollPosition: .none) @@ -533,6 +535,26 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } +// MARK: UIContextMenuInteractionDelegate + +extension MasterFeedViewController: UIContextMenuInteractionDelegate { + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { + + guard let sectionIndex = interaction.view?.tag, + let sectionNode = coordinator.rootNode.childAtIndex(sectionIndex), + let account = sectionNode.representedObject as? Account + else { + return nil + } + + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in + let accountInfoAction = self.getAccountInfoAction(account: account) + let deactivateAction = self.deactivateAccountAction(account: account) + return UIMenu(title: "", children: [accountInfoAction, deactivateAction]) + } + } +} + // MARK: MasterTableViewCellDelegate extension MasterFeedViewController: MasterFeedTableViewCellDelegate { @@ -848,6 +870,22 @@ private extension MasterFeedViewController { return action } + func getAccountInfoAction(account: Account) -> UIAction { + let title = NSLocalizedString("Get Info", comment: "Get Info") + let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in + self?.coordinator.showAccountInspector(for: account) + } + return action + } + + func deactivateAccountAction(account: Account) -> UIAction { + let title = NSLocalizedString("Deactivate", comment: "Deactivate") + let action = UIAction(title: title, image: AppAssets.deactivateImage) { action in + account.isActive = false + } + return action + } + func getInfoAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? { guard let node = dataSource.itemIdentifier(for: indexPath), let feed = node.representedObject as? Feed else { return nil diff --git a/iOS/MasterTimeline/Cell/MasterTimelineDefaultCellLayout.swift b/iOS/MasterTimeline/Cell/MasterTimelineDefaultCellLayout.swift index b1070a2eb..f7ca315c5 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineDefaultCellLayout.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineDefaultCellLayout.swift @@ -21,7 +21,7 @@ struct MasterTimelineDefaultCellLayout: MasterTimelineCellLayout { static let starDimension = CGFloat(integerLiteral: 16) static let starSize = CGSize(width: MasterTimelineDefaultCellLayout.starDimension, height: MasterTimelineDefaultCellLayout.starDimension) - static let avatarSize = CGSize(width: 48.0, height: 48.0) + static let avatarSize = CGSize(width: 32.0, height: 32.0) static let avatarMarginRight = CGFloat(integerLiteral: 8) static let avatarCornerRadius = CGFloat(integerLiteral: 4) diff --git a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift index 7e55f3dfd..0a240370e 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift @@ -9,7 +9,7 @@ import UIKit import RSCore -class MasterTimelineTableViewCell: NNWTableViewCell { +class MasterTimelineTableViewCell: VibrantTableViewCell { private let titleView = MasterTimelineTableViewCell.multiLineUILabel() private let summaryView = MasterTimelineTableViewCell.multiLineUILabel() @@ -37,7 +37,7 @@ class MasterTimelineTableViewCell: NNWTableViewCell { override func applyThemeProperties() { super.applyThemeProperties() - let highlightedTextColor = AppAssets.tableViewCellHighlightedTextColor + let highlightedTextColor = AppAssets.vibrantTextColor titleView.highlightedTextColor = highlightedTextColor summaryView.highlightedTextColor = highlightedTextColor @@ -187,30 +187,12 @@ private extension MasterTimelineTableViewCell { } func updateUnreadIndicator() { - let hide = cellData.read || cellData.starred - self.unreadIndicatorView.isHidden = hide - self.unreadIndicatorView.frame.size = !hide ? CGSize.zero : MasterTimelineDefaultCellLayout.unreadCircleSize - UIView.animate( - withDuration: 0.5, - delay: 0.0, - usingSpringWithDamping: 0.5, - initialSpringVelocity: 0.2, - animations: { - self.unreadIndicatorView.frame.size = !hide ? MasterTimelineDefaultCellLayout.unreadCircleSize : CGSize.zero - }) + showOrHideView(unreadIndicatorView, cellData.read || cellData.starred) + unreadIndicatorView.setNeedsDisplay() } func updateStarView() { - self.starView.isHidden = !self.cellData.starred - self.starView.frame.size = self.cellData.starred ? CGSize.zero : MasterTimelineDefaultCellLayout.starSize - UIView.animate( - withDuration: 0.5, - delay: 0.0, - usingSpringWithDamping: 0.5, - initialSpringVelocity: 0.2, - animations: { - self.starView.frame.size = self.cellData.starred ? MasterTimelineDefaultCellLayout.starSize : CGSize.zero - }) + showOrHideView(starView, !cellData.starred) } func updateAvatar() { @@ -251,6 +233,10 @@ private extension MasterTimelineTableViewCell { } } + func showOrHideView(_ view: UIView, _ shouldHide: Bool) { + shouldHide ? hideView(view) : showView(view) + } + func updateSubviews() { updateTitleView() updateSummaryView() diff --git a/iOS/MasterTimeline/Cell/MasterUnreadIndicatorView.swift b/iOS/MasterTimeline/Cell/MasterUnreadIndicatorView.swift index 3c64e084e..88cb190f0 100644 --- a/iOS/MasterTimeline/Cell/MasterUnreadIndicatorView.swift +++ b/iOS/MasterTimeline/Cell/MasterUnreadIndicatorView.swift @@ -32,7 +32,7 @@ class MasterUnreadIndicatorView: UIView { }() override func draw(_ dirtyRect: CGRect) { - let color = isSelected ? AppAssets.tableViewCellHighlightedTextColor : AppAssets.secondaryAccentColor + let color = isSelected ? AppAssets.vibrantTextColor : AppAssets.secondaryAccentColor color.setFill() MasterUnreadIndicatorView.bezierPath.fill() } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 8c74296ab..2dd9a9a2a 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -76,7 +76,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } override func viewWillAppear(_ animated: Bool) { - clearsSelectionOnViewWillAppear = coordinator.isRootSplitCollapsed applyChanges(animate: false) super.viewWillAppear(animated) } @@ -132,7 +131,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner func restoreSelectionIfNecessary(adjustScroll: Bool) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if adjustScroll { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false, deselect: coordinator.isRootSplitCollapsed) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false) } else { tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) } @@ -150,7 +149,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner func updateArticleSelection(animated: Bool) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if tableView.indexPathForSelectedRow != indexPath { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true, deselect: coordinator.isRootSplitCollapsed) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true) } } else { tableView.selectRow(at: nil, animated: animated, scrollPosition: .none) diff --git a/iOS/Resources/Assets.xcassets/accountLocal.imageset/Contents.json b/iOS/Resources/Assets.xcassets/accountLocalPad.imageset/Contents.json similarity index 85% rename from iOS/Resources/Assets.xcassets/accountLocal.imageset/Contents.json rename to iOS/Resources/Assets.xcassets/accountLocalPad.imageset/Contents.json index c48efa3f2..7fe4e483c 100644 --- a/iOS/Resources/Assets.xcassets/accountLocal.imageset/Contents.json +++ b/iOS/Resources/Assets.xcassets/accountLocalPad.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "accountLocal.pdf" + "filename" : "localAccountPad.pdf" } ], "info" : { diff --git a/iOS/Resources/Assets.xcassets/accountLocal.imageset/accountLocal.pdf b/iOS/Resources/Assets.xcassets/accountLocalPad.imageset/localAccountPad.pdf similarity index 75% rename from iOS/Resources/Assets.xcassets/accountLocal.imageset/accountLocal.pdf rename to iOS/Resources/Assets.xcassets/accountLocalPad.imageset/localAccountPad.pdf index c32cc3b89..cfc6ca9f0 100644 Binary files a/iOS/Resources/Assets.xcassets/accountLocal.imageset/accountLocal.pdf and b/iOS/Resources/Assets.xcassets/accountLocalPad.imageset/localAccountPad.pdf differ diff --git a/iOS/Resources/Assets.xcassets/accountLocalPhone.imageset/Contents.json b/iOS/Resources/Assets.xcassets/accountLocalPhone.imageset/Contents.json new file mode 100644 index 000000000..05bb5e471 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/accountLocalPhone.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "localAccountPhone.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/iOS/Resources/Assets.xcassets/accountLocalPhone.imageset/localAccountPhone.pdf b/iOS/Resources/Assets.xcassets/accountLocalPhone.imageset/localAccountPhone.pdf new file mode 100644 index 000000000..c807edece Binary files /dev/null and b/iOS/Resources/Assets.xcassets/accountLocalPhone.imageset/localAccountPhone.pdf differ diff --git a/iOS/Resources/Assets.xcassets/tableViewCellHighlightedTextColor.colorset/Contents.json b/iOS/Resources/Assets.xcassets/vibrantTextColor.colorset/Contents.json similarity index 100% rename from iOS/Resources/Assets.xcassets/tableViewCellHighlightedTextColor.colorset/Contents.json rename to iOS/Resources/Assets.xcassets/vibrantTextColor.colorset/Contents.json diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 7d9598c8c..bfd636ccd 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -8,7 +8,6 @@ import UIKit import UserNotifications -import SwiftUI import Account import Articles import RSCore @@ -786,9 +785,23 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func showSettings() { - rootSplitViewController.present(style: .formSheet) { - SettingsView(viewModel: SettingsView.ViewModel()).environment(\.sceneCoordinator, self) - } + let settingsNavController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController + let settingsViewController = settingsNavController.topViewController as! SettingsViewController + settingsNavController.modalPresentationStyle = .formSheet + settingsNavController.preferredContentSize = SettingsViewController.preferredContentSizeForFormSheetDisplay + settingsViewController.presentingParentController = rootSplitViewController + rootSplitViewController.present(settingsNavController, animated: true) + } + + func showAccountInspector(for account: Account) { + let accountInspectorNavController = + UIStoryboard.inspector.instantiateViewController(identifier: "AccountInspectorNavigationViewController") as! UINavigationController + let accountInspectorController = accountInspectorNavController.topViewController as! AccountInspectorViewController + accountInspectorNavController.modalPresentationStyle = .formSheet + accountInspectorNavController.preferredContentSize = AccountInspectorViewController.preferredContentSizeForFormSheetDisplay + accountInspectorController.isModal = true + accountInspectorController.account = account + rootSplitViewController.present(accountInspectorNavController, animated: true) } func showFeedInspector() { @@ -948,6 +961,7 @@ extension SceneCoordinator: UINavigationControllerDelegate { if viewController === masterTimelineViewController && !isThreePanelMode && rootSplitViewController.isCollapsed && !isArticleViewControllerPending { stopArticleExtractor() currentArticle = nil + masterTimelineViewController?.updateArticleSelection(animated: animated) activityManager.invalidateReading() return } @@ -1696,20 +1710,3 @@ private extension SceneCoordinator { } } - -// MARK: SwiftUI - -struct SceneCoordinatorHolder { - weak var value: SceneCoordinator? -} - -struct SceneCoordinatorKey: EnvironmentKey { - static var defaultValue: SceneCoordinatorHolder { return SceneCoordinatorHolder(value: nil ) } -} - -extension EnvironmentValues { - var sceneCoordinator: SceneCoordinator? { - get { return self[SceneCoordinatorKey.self].value } - set { self[SceneCoordinatorKey.self].value = newValue } - } -} diff --git a/iOS/Settings/AboutViewController.swift b/iOS/Settings/AboutViewController.swift new file mode 100644 index 000000000..a472efd63 --- /dev/null +++ b/iOS/Settings/AboutViewController.swift @@ -0,0 +1,57 @@ +// +// AboutViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 4/25/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class AboutViewController: UITableViewController { + + @IBOutlet weak var aboutTextView: UITextView! + @IBOutlet weak var creditsTextView: UITextView! + @IBOutlet weak var acknowledgmentsTextView: UITextView! + @IBOutlet weak var thanksTextView: UITextView! + @IBOutlet weak var dedicationTextView: UITextView! + + override func viewDidLoad() { + + super.viewDidLoad() + + configureCell(file: "About", textView: aboutTextView) + configureCell(file: "Credits", textView: creditsTextView) + configureCell(file: "Acknowledgments", textView: acknowledgmentsTextView) + configureCell(file: "Thanks", textView: thanksTextView) + configureCell(file: "Dedication", textView: dedicationTextView) + + let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 20.0, y: 0.0, width: 0.0, height: 0.0)) + buildLabel.font = UIFont.systemFont(ofSize: 11.0) + buildLabel.textColor = UIColor.gray + buildLabel.text = NSLocalizedString("Copyright © 2002-2019 Ranchero Software", comment: "Copyright") + buildLabel.numberOfLines = 0 + buildLabel.sizeToFit() + buildLabel.translatesAutoresizingMaskIntoConstraints = false + tableView.tableFooterView = buildLabel + + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + +} + +private extension AboutViewController { + + func configureCell(file: String, textView: UITextView) { + let url = Bundle.main.url(forResource: file, withExtension: "rtf")! + let string = try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) + textView.attributedText = string + textView.textColor = UIColor.label + textView.adjustsFontForContentSizeCategory = true + textView.font = .preferredFont(forTextStyle: .body) + } + +} diff --git a/iOS/Settings/Account/SettingsAccountLabelView.swift b/iOS/Settings/Account/SettingsAccountLabelView.swift deleted file mode 100644 index 0d5ead2e3..000000000 --- a/iOS/Settings/Account/SettingsAccountLabelView.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// SettingsAccountLabelView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/11/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct SettingsAccountLabelView : View { - let accountImage: String - let accountLabel: String - - var body: some View { - HStack { - Image(accountImage) - .resizable() - .aspectRatio(1, contentMode: .fit) - .frame(height: 32) - Text(verbatim: accountLabel).font(.title) - } - .foregroundColor(.primary).padding(4.0) - } -} - -#if DEBUG -struct SettingsAccountLabelView_Previews : PreviewProvider { - static var previews: some View { - SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "On My Device") - .previewLayout(.fixed(width: 300, height: 44)) - } -} -#endif diff --git a/iOS/Settings/Account/SettingsAddAccountView.swift b/iOS/Settings/Account/SettingsAddAccountView.swift deleted file mode 100644 index f26769971..000000000 --- a/iOS/Settings/Account/SettingsAddAccountView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SettingsAddAccountView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/11/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsAddAccountView : View { - @Environment(\.presentationMode) var presentation - @State private var accountAddAction: Int? = nil - - var body: some View { - Form { - - NavigationLink(destination: SettingsLocalAccountView(name: ""), tag: 1, selection: $accountAddAction) { - SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName) - } - .modifier(VibrantSelectAction(action: { - self.accountAddAction = 1 - })).padding(.vertical, 16) - - NavigationLink(destination: SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel()), tag: 2, selection: $accountAddAction) { - SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin") - - } - .modifier(VibrantSelectAction(action: { - self.accountAddAction = 2 - })).padding(.vertical, 16) - -// NavigationLink(destination: SettingsReaderAPIAccountView(viewModel: SettingsReaderAPIAccountView.ViewModel(accountType: .freshRSS)), tag: 3, selection: $accountAddAction) { -// SettingsAccountLabelView(accountImage: "accountFreshRSS", accountLabel: "Fresh RSS") -// } -// .modifier(VibrantSelectAction(action: { -// self.accountAddAction = 3 -// })) - - } - .navigationBarTitle(Text("Add Account"), displayMode: .inline) - } -} - -#if DEBUG -struct AddAccountView_Previews : PreviewProvider { - static var previews: some View { - SettingsAddAccountView() - } -} -#endif diff --git a/iOS/Settings/Account/SettingsDetailAccountView.swift b/iOS/Settings/Account/SettingsDetailAccountView.swift deleted file mode 100644 index f67b29833..000000000 --- a/iOS/Settings/Account/SettingsDetailAccountView.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// SettingsDetailAccountView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/13/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine -import Account -import RSWeb - -struct SettingsDetailAccountView : View { - @Environment(\.presentationMode) var presentation - @ObservedObject var viewModel: ViewModel - @State private var credentialsAction: Int? = nil - @State private var isDeleteAlertPresented = false - - var body: some View { - Form { - Section { - HStack { - TextField("Name", text: $viewModel.name) - } - Toggle(isOn: $viewModel.isActive) { - Text("Active") - } - } - if viewModel.isCreditialsAvailable { - if viewModel.account.type == .feedbin { - NavigationLink(destination: self.settingsFeedbinAccountView, tag: 1, selection: $credentialsAction) { - Text("Credentials") - } - .modifier(VibrantSelectAction(action: { - self.credentialsAction = 1 - })) - } - if viewModel.account.type == .freshRSS { - NavigationLink(destination: self.settingsReaderAPIAccountView, tag: 1, selection: $credentialsAction) { - Text("Credentials") - } - .modifier(VibrantSelectAction(action: { - self.credentialsAction = 1 - })) - } - } - if viewModel.isDeletable { - Section { - Button(action: { - self.isDeleteAlertPresented.toggle() - }) { - Text("Delete Account").foregroundColor(.red) - } - .alert(isPresented: $isDeleteAlertPresented) { - Alert(title: Text("Are you sure you want to delete \"\(viewModel.nameForDisplay)\"?"), - primaryButton: Alert.Button.default(Text("Delete"), action: { - self.viewModel.delete() - self.presentation.wrappedValue.dismiss() - }), - secondaryButton: Alert.Button.cancel()) - } - } - } - } - .buttonStyle(VibrantButtonStyle(alignment: .center)) - .navigationBarTitle(Text(verbatim: viewModel.nameForDisplay), displayMode: .inline) - - } - - var settingsFeedbinAccountView: SettingsFeedbinAccountView { - let feedbinViewModel = SettingsFeedbinAccountView.ViewModel(account: viewModel.account) - return SettingsFeedbinAccountView(viewModel: feedbinViewModel) - } - - var settingsReaderAPIAccountView: SettingsReaderAPIAccountView { - let readerAPIModel = SettingsReaderAPIAccountView.ViewModel(account: viewModel.account) - return SettingsReaderAPIAccountView(viewModel: readerAPIModel) - } - - class ViewModel: ObservableObject { - - let objectWillChange = ObservableObjectPublisher() - - let account: Account - - init(_ account: Account) { - self.account = account - } - - var nameForDisplay: String { - account.nameForDisplay - } - - var name: String { - get { - account.name ?? "" - } - set { - objectWillChange.send() - account.name = newValue.isEmpty ? nil : newValue - } - } - - var isActive: Bool { - get { - account.isActive - } - set { - objectWillChange.send() - account.isActive = newValue - } - } - - var isCreditialsAvailable: Bool { - return account.type != .onMyMac - } - - var isDeletable: Bool { - return AccountManager.shared.defaultAccount != account - } - - func delete() { - AccountManager.shared.deleteAccount(account) - ActivityManager.cleanUp(account) - } - } -} - -#if DEBUG -struct SettingsDetailAccountView_Previews : PreviewProvider { - static var previews: some View { - let viewModel = SettingsDetailAccountView.ViewModel(AccountManager.shared.defaultAccount) - return SettingsDetailAccountView(viewModel: viewModel) - } -} -#endif diff --git a/iOS/Settings/Account/SettingsFeedbinAccountView.swift b/iOS/Settings/Account/SettingsFeedbinAccountView.swift deleted file mode 100644 index a685a8ada..000000000 --- a/iOS/Settings/Account/SettingsFeedbinAccountView.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// SettingsFeedbinAccountView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/11/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine -import Account -import RSWeb - -struct SettingsFeedbinAccountView : View { - @Environment(\.presentationMode) var presentation - @ObservedObject var viewModel: ViewModel - @State var busy: Bool = false - @State var error: String = "" - - var body: some View { - Form { - Section(header: - HStack { - Spacer() - SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin") - .padding() - .layoutPriority(1.0) - Spacer() - } - ) { - TextField("Email", text: $viewModel.email) - .keyboardType(.emailAddress) - .textContentType(.emailAddress) - PasswordField(password: $viewModel.password) - } - Section(footer: - HStack { - Spacer() - Text(verbatim: error).foregroundColor(.red) - Spacer() - } - ) { - Button(action: { self.addAccount() }) { - if viewModel.isUpdate { - Text("Update Account") - } else { - Text("Add Account") - } - } - .buttonStyle(VibrantButtonStyle(alignment: .center)) - .disabled(!viewModel.isValid) - } - } -// .disabled(busy) // Maybe someday we can do this, but right now it crashes on the iPad - .navigationBarTitle(Text(""), displayMode: .inline) - } - - private func addAccount() { - - busy = true - error = "" - - let emailAddress = viewModel.email.trimmingCharacters(in: .whitespaces) - let credentials = Credentials(type: .basic, username: emailAddress, secret: viewModel.password) - - Account.validateCredentials(type: .feedbin, credentials: credentials) { result in - - self.busy = false - - switch result { - case .success(let authenticated): - - if (authenticated != nil) { - - var newAccount = false - let workAccount: Account - if self.viewModel.account == nil { - workAccount = AccountManager.shared.createAccount(type: .feedbin) - newAccount = true - } else { - workAccount = self.viewModel.account! - } - - do { - - do { - try workAccount.removeCredentials(type: .basic) - } catch {} - try workAccount.storeCredentials(credentials) - - if newAccount { - workAccount.refreshAll() { result in } - } - - self.dismiss() - - } catch { - self.error = "Keychain error while storing credentials." - } - - } else { - self.error = "Invalid email/password combination." - } - - case .failure: - self.error = "Network error. Try again later." - } - - } - - } - - private func dismiss() { - presentation.wrappedValue.dismiss() - } - - class ViewModel: ObservableObject { - - let objectWillChange = ObservableObjectPublisher() - var account: Account? = nil - - init() { - } - - init(account: Account) { - self.account = account - if let credentials = try? account.retrieveCredentials(type: .basic) { - self.email = credentials.username - } - } - - var email: String = "" { - willSet { - objectWillChange.send() - } - } - - var password: String = "" { - willSet { - objectWillChange.send() - } - } - - var isUpdate: Bool { - return account != nil - } - - var isValid: Bool { - return !email.isEmpty && !password.isEmpty - } - } - -} - -#if DEBUG -struct SettingsFeedbinAccountView_Previews : PreviewProvider { - static var previews: some View { - SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel()) - } -} -#endif diff --git a/iOS/Settings/Account/SettingsLocalAccountView.swift b/iOS/Settings/Account/SettingsLocalAccountView.swift deleted file mode 100644 index d92cd6f9b..000000000 --- a/iOS/Settings/Account/SettingsLocalAccountView.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// SettingsLocalAccountView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/11/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsLocalAccountView : View { - @Environment(\.presentationMode) var presentation - @State var name: String - - var body: some View { - Form { - Section(header: - HStack { - Spacer() - SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName) - .padding() - .layoutPriority(1.0) - Spacer() - } - ) { - HStack { - TextField("Name", text: $name) - } - } - Section { - Button(action: { self.addAccount() }) { - Text("Add Account") - } - .buttonStyle(VibrantButtonStyle(alignment: .center)) - } - } - .navigationBarTitle(Text(""), displayMode: .inline) - } - - private func addAccount() { - let account = AccountManager.shared.createAccount(type: .onMyMac) - account.name = name - dismiss() - } - - private func dismiss() { - presentation.wrappedValue.dismiss() - } - -} - -#if DEBUG -struct SettingsLocalAccountView_Previews : PreviewProvider { - static var previews: some View { - SettingsLocalAccountView(name: "") - } -} -#endif diff --git a/iOS/Settings/Account/SettingsReaderAPIAccountView.swift b/iOS/Settings/Account/SettingsReaderAPIAccountView.swift deleted file mode 100644 index 9e4218e9a..000000000 --- a/iOS/Settings/Account/SettingsReaderAPIAccountView.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// SettingsReaderAPIAccountView.swift -// NetNewsWire-iOS -// -// Created by Jeremy Beker on 5/28/2019. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine -import Account -import RSWeb - -struct SettingsReaderAPIAccountView : View { - @Environment(\.presentationMode) var presentation - @ObservedObject var viewModel: ViewModel - - @State var busy: Bool = false - @State var error: String = "" - - var body: some View { - Form { - Section(header: - HStack { - Spacer() - SettingsAccountLabelView(accountImage: "accountFreshRSS", accountLabel: "FreshRSS") - .padding() - .layoutPriority(1.0) - Spacer() - } - ) { - TextField("Email", text: $viewModel.email) - .keyboardType(.emailAddress) - .textContentType(.emailAddress) - SecureField("Password", text: $viewModel.password) - TextField("API URL:", text: $viewModel.apiURL).textContentType(.URL) - } - - Section(footer: - HStack { - Spacer() - Text(verbatim: error).foregroundColor(.red) - Spacer() - } - ) { - Button(action: { self.addAccount() }) { - if viewModel.isUpdate { - Text("Update Account") - } else { - Text("Add Account") - } - } - .buttonStyle(VibrantButtonStyle(alignment: .center)) - .disabled(!viewModel.isValid) - } - } -// .disabled(busy) - } - - private func addAccount() { - - busy = true - error = "" - - let emailAddress = viewModel.email.trimmingCharacters(in: .whitespaces) - let credentials = Credentials(type: .readerBasic, username: emailAddress, secret: viewModel.password) - guard let apiURL = URL(string: viewModel.apiURL) else { - self.error = "Invalid API URL." - return - } - - Account.validateCredentials(type: viewModel.accountType, credentials: credentials, endpoint: apiURL) { result in - - self.busy = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.error = "Invalid email/password combination." - return - } - - var newAccount = false - let workAccount: Account - if self.viewModel.account == nil { - workAccount = AccountManager.shared.createAccount(type: self.viewModel.accountType) - newAccount = true - } else { - workAccount = self.viewModel.account! - } - - do { - - do { - try workAccount.removeCredentials(type: .readerBasic) - try workAccount.removeCredentials(type: .readerAPIKey) - } catch {} - - workAccount.endpointURL = apiURL - - try workAccount.storeCredentials(credentials) - try workAccount.storeCredentials(validatedCredentials) - - if newAccount { - workAccount.refreshAll() { result in } - } - - self.dismiss() - - } catch { - self.error = "Keychain error while storing credentials." - } - - case .failure: - self.error = "Network error. Try again later." - } - - } - - } - - private func dismiss() { - presentation.wrappedValue.dismiss() - } - - class ViewModel: ObservableObject { - - let objectWillChange = ObservableObjectPublisher() - var accountType: AccountType - var account: Account? = nil - - init(accountType: AccountType) { - self.accountType = accountType - } - - init(account: Account) { - self.account = account - self.accountType = account.type - if let credentials = try? account.retrieveCredentials(type: .readerBasic) { - self.email = credentials.username - self.apiURL = account.endpointURL?.absoluteString ?? "" - } - } - - var email: String = "" { - willSet { - objectWillChange.send() - } - } - var password: String = "" { - willSet { - objectWillChange.send() - } - } - var apiURL: String = "" { - willSet { - objectWillChange.send() - } - } - var isUpdate: Bool { - return account != nil - } - - var isValid: Bool { - return !email.isEmpty && !password.isEmpty - } - } - -} - -#if DEBUG -struct SettingsReaderAPIAccountView_Previews : PreviewProvider { - static var previews: some View { - SettingsReaderAPIAccountView(viewModel: SettingsReaderAPIAccountView.ViewModel(accountType: .freshRSS)) - } -} -#endif diff --git a/iOS/Settings/AddAccountViewController.swift b/iOS/Settings/AddAccountViewController.swift new file mode 100644 index 000000000..f53d46a55 --- /dev/null +++ b/iOS/Settings/AddAccountViewController.swift @@ -0,0 +1,50 @@ +// +// AddAccountViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 5/16/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Account +import UIKit + +protocol AddAccountDismissDelegate: UIViewController { + func dismiss() +} + +class AddAccountViewController: UITableViewController, AddAccountDismissDelegate { + + @IBOutlet private weak var localAccountImageView: UIImageView! + @IBOutlet private weak var localAccountNameLabel: UILabel! + + override func viewDidLoad() { + super.viewDidLoad() + localAccountImageView.image = AppAssets.image(for: .onMyMac) + localAccountNameLabel.text = Account.defaultLocalAccountName + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch indexPath.row { + case 0: + let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "AddLocalAccountNavigationViewController") as! UINavigationController + navController.modalPresentationStyle = .currentContext + let addViewController = navController.topViewController as! LocalAccountViewController + addViewController.delegate = self + present(navController, animated: true) + case 1: + let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController + navController.modalPresentationStyle = .currentContext + let addViewController = navController.topViewController as! FeedbinAccountViewController + addViewController.delegate = self + present(navController, animated: true) + default: + break + } + } + + func dismiss() { + navigationController?.popViewController(animated: false) + } + +} diff --git a/iOS/Settings/RefreshIntervalViewController.swift b/iOS/Settings/RefreshIntervalViewController.swift new file mode 100644 index 000000000..8f3d89ba9 --- /dev/null +++ b/iOS/Settings/RefreshIntervalViewController.swift @@ -0,0 +1,111 @@ +// +// RefreshIntervalViewController.swift +// NetNewsWire +// +// Created by Maurice Parker on 4/25/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class RefreshIntervalViewController: UITableViewController { + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 7 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + + cell.textLabel?.adjustsFontForContentSizeCategory = true + + let userRefreshInterval = AppDefaults.refreshInterval + + switch indexPath.row { + case 0: + cell.textLabel?.text = RefreshInterval.manually.description() + if userRefreshInterval == RefreshInterval.manually { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + case 1: + cell.textLabel?.text = RefreshInterval.every10Minutes.description() + if userRefreshInterval == RefreshInterval.every10Minutes { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + case 2: + cell.textLabel?.text = RefreshInterval.every30Minutes.description() + if userRefreshInterval == RefreshInterval.every30Minutes { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + case 3: + cell.textLabel?.text = RefreshInterval.everyHour.description() + if userRefreshInterval == RefreshInterval.everyHour { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + case 4: + cell.textLabel?.text = RefreshInterval.every2Hours.description() + if userRefreshInterval == RefreshInterval.every2Hours { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + case 5: + cell.textLabel?.text = RefreshInterval.every4Hours.description() + if userRefreshInterval == RefreshInterval.every4Hours { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + default: + cell.textLabel?.text = RefreshInterval.every8Hours.description() + if userRefreshInterval == RefreshInterval.every8Hours { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + } + + return cell + + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + let refreshInterval: RefreshInterval + + switch indexPath.row { + case 0: + refreshInterval = RefreshInterval.manually + case 1: + refreshInterval = RefreshInterval.every10Minutes + case 2: + refreshInterval = RefreshInterval.every30Minutes + case 3: + refreshInterval = RefreshInterval.everyHour + case 4: + refreshInterval = RefreshInterval.every2Hours + case 5: + refreshInterval = RefreshInterval.every4Hours + default: + refreshInterval = RefreshInterval.every8Hours + } + + AppDefaults.refreshInterval = refreshInterval + self.navigationController?.popViewController(animated: true) + + } + +} diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard new file mode 100644 index 000000000..3afc9391c --- /dev/null +++ b/iOS/Settings/Settings.storyboard @@ -0,0 +1,689 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/Settings/SettingsAboutView.swift b/iOS/Settings/SettingsAboutView.swift deleted file mode 100644 index e76fce4ec..000000000 --- a/iOS/Settings/SettingsAboutView.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// SettingsAboutView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/16/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine - -struct SettingsAboutView: View { - - @ObservedObject var viewModel: ViewModel - - var body: some View { - GeometryReader { geometry in - List { - Text("NetNewsWire").font(.largeTitle) - AttributedStringView(string: self.viewModel.about, preferredMaxLayoutWidth: geometry.size.width - 20) - Section(header: Text("CREDITS")) { - AttributedStringView(string: self.viewModel.credits, preferredMaxLayoutWidth: geometry.size.width - 20) - } - Section(header: Text("ACKNOWLEDGEMENTS")) { - AttributedStringView(string: self.viewModel.acknowledgements, preferredMaxLayoutWidth: geometry.size.width - 20) - } - Section(header: Text("THANKS")) { - AttributedStringView(string: self.viewModel.thanks, preferredMaxLayoutWidth: geometry.size.width - 20) - } - Section(header: Text("DEDICATION"), footer: Text("Copyright © 2002-2019 Ranchero Software").font(.footnote)) { - AttributedStringView(string: self.viewModel.dedication, preferredMaxLayoutWidth: geometry.size.width - 20) - } - } - } - } - - class ViewModel: ObservableObject { - let objectWillChange = ObservableObjectPublisher() - - var about: NSAttributedString - var credits: NSAttributedString - var acknowledgements: NSAttributedString - var thanks: NSAttributedString - var dedication: NSAttributedString - - init() { - about = ViewModel.loadResource("About") - credits = ViewModel.loadResource("Credits") - acknowledgements = ViewModel.loadResource("Acknowledgments") - thanks = ViewModel.loadResource("Thanks") - dedication = ViewModel.loadResource("Dedication") - } - - private static func loadResource(_ resource: String) -> NSAttributedString { - let url = Bundle.main.url(forResource: resource, withExtension: "rtf")! - return try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) - - } - - } -} - -struct SettingsAboutView_Previews: PreviewProvider { - static var previews: some View { - SettingsAboutView(viewModel: SettingsAboutView.ViewModel()) - } -} diff --git a/iOS/Settings/SettingsAccountTableViewCell.swift b/iOS/Settings/SettingsAccountTableViewCell.swift new file mode 100644 index 000000000..0ae9a6ec2 --- /dev/null +++ b/iOS/Settings/SettingsAccountTableViewCell.swift @@ -0,0 +1,39 @@ +// +// SettingsAccountTableViewCell.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 10/23/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class SettingsAccountTableViewCell: VibrantTableViewCell { + + @IBOutlet weak var accountImage: UIImageView! + @IBOutlet weak var accountNameLabel: UILabel! + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + updateVibrancy(animated: animated) + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + updateVibrancy(animated: animated) + } + + override func applyThemeProperties() { + super.applyThemeProperties() + accountNameLabel?.highlightedTextColor = AppAssets.vibrantTextColor + } + + func updateVibrancy(animated: Bool) { + let tintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label + let duration = animated ? 0.6 : 0.0 + UIView.animate(withDuration: duration) { + self.accountImage?.tintColor = tintColor + } + } + +} diff --git a/iOS/Settings/SettingsAccountTableViewCell.xib b/iOS/Settings/SettingsAccountTableViewCell.xib new file mode 100644 index 000000000..1b4d94da3 --- /dev/null +++ b/iOS/Settings/SettingsAccountTableViewCell.xib @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/Settings/SettingsRefreshSelectionView.swift b/iOS/Settings/SettingsRefreshSelectionView.swift deleted file mode 100644 index 22fcce129..000000000 --- a/iOS/Settings/SettingsRefreshSelectionView.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// SettingsRefreshSelectionView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/16/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct SettingsRefreshSelectionView: View { - - @Environment(\.presentationMode) var presentation - @Binding var selectedInterval: RefreshInterval - - var body: some View { - Form { - ForEach(RefreshInterval.allCases) { interval in - Button(action: { - self.selectedInterval = interval - self.presentation.wrappedValue.dismiss() - }) { - HStack { - Text(interval.description()) - Spacer() - if interval == self.selectedInterval { - Image(systemName: "checkmark") - } - } - }.buttonStyle(VibrantButtonStyle(alignment: .leading)) - } - } - } - -} diff --git a/iOS/Settings/SettingsSubscriptionsExportAccountPickerView.swift b/iOS/Settings/SettingsSubscriptionsExportAccountPickerView.swift deleted file mode 100644 index 33bc3ea40..000000000 --- a/iOS/Settings/SettingsSubscriptionsExportAccountPickerView.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// SettingsSubscriptionsExportAccountPickerView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 10/20/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsSubscriptionsExportAccountPickerView: View { - - @Environment(\.presentationMode) var presentation - @State private var selectedAccount: Account? - @State private var isOPMLExportDocPickerPresented: Bool = false - - var body: some View { - Form { - ForEach(AccountManager.shared.sortedAccounts) { account in - Button(action: { - self.selectedAccount = account - self.isOPMLExportDocPickerPresented = true - }) { - Text(verbatim: account.nameForDisplay) - }.buttonStyle(VibrantButtonStyle(alignment: .leading)) - } - }.sheet(isPresented: $isOPMLExportDocPickerPresented, onDismiss: { self.presentation.wrappedValue.dismiss() }) { - SettingsSubscriptionsExportDocumentPickerView(account: self.selectedAccount!) - } - .navigationBarTitle(Text("Select Account"), displayMode: .inline) - } - -} diff --git a/iOS/Settings/SettingsSubscriptionsExportDocumentPickerView.swift b/iOS/Settings/SettingsSubscriptionsExportDocumentPickerView.swift deleted file mode 100644 index a471c7d27..000000000 --- a/iOS/Settings/SettingsSubscriptionsExportDocumentPickerView.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// SettingsSubscriptionsExportDocumentPickerView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/16/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsSubscriptionsExportDocumentPickerView : UIViewControllerRepresentable { - var account: Account - - func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIDocumentPickerViewController { - - 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) - try? opmlString.write(to: tempFile, atomically: true, encoding: String.Encoding.utf8) - - return UIDocumentPickerViewController(url: tempFile, in: .exportToService) - } - - func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext) { - // - } - -} diff --git a/iOS/Settings/SettingsSubscriptionsImportAccountPickerView.swift b/iOS/Settings/SettingsSubscriptionsImportAccountPickerView.swift deleted file mode 100644 index 0ce3fbdc6..000000000 --- a/iOS/Settings/SettingsSubscriptionsImportAccountPickerView.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// SettingsSubscriptionsImportAccountPickerView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 10/20/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsSubscriptionsImportAccountPickerView: View { - - @Environment(\.presentationMode) var presentation - @State private var selectedAccount: Account? - @State private var isOPMLImportDocPickerPresented: Bool = false - - var body: some View { - Form { - ForEach(AccountManager.shared.sortedActiveAccounts) { account in - Button(action: { - self.selectedAccount = account - self.isOPMLImportDocPickerPresented = true - }) { - Text(verbatim: account.nameForDisplay) - }.buttonStyle(VibrantButtonStyle(alignment: .leading)) - } - }.sheet(isPresented: $isOPMLImportDocPickerPresented, onDismiss: { self.presentation.wrappedValue.dismiss() }) { - SettingsSubscriptionsImportDocumentPickerView(account: self.selectedAccount!) - } - .navigationBarTitle(Text("Select Account"), displayMode: .inline) - } - -} diff --git a/iOS/Settings/SettingsSubscriptionsImportDocumentPickerView.swift b/iOS/Settings/SettingsSubscriptionsImportDocumentPickerView.swift deleted file mode 100644 index 4f4043a92..000000000 --- a/iOS/Settings/SettingsSubscriptionsImportDocumentPickerView.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// SettingsSubscriptionsImportDocumentPickerView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/16/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsSubscriptionsImportDocumentPickerView : UIViewControllerRepresentable { - var account: Account - - func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIDocumentPickerViewController { - let docPicker = UIDocumentPickerViewController(documentTypes: ["public.xml", "org.opml.opml"], in: .import) - docPicker.delegate = context.coordinator - return docPicker - } - - func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext) { - // - } - - func makeCoordinator() -> Coordinator { - return Coordinator(self) - } - - class Coordinator : NSObject, UIDocumentPickerDelegate { - var parent: SettingsSubscriptionsImportDocumentPickerView - - init(_ view: SettingsSubscriptionsImportDocumentPickerView) { - self.parent = view - } - - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - for url in urls { - parent.account.importOPML(url) { result in} - } - } - - } -} diff --git a/iOS/Settings/SettingsTableViewCell.xib b/iOS/Settings/SettingsTableViewCell.xib new file mode 100644 index 000000000..71f516107 --- /dev/null +++ b/iOS/Settings/SettingsTableViewCell.xib @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/Settings/SettingsView.swift b/iOS/Settings/SettingsView.swift deleted file mode 100644 index 40b6929f0..000000000 --- a/iOS/Settings/SettingsView.swift +++ /dev/null @@ -1,270 +0,0 @@ -// -// SettingsView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/11/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine -import Account - -struct SettingsView : View { - - @ObservedObject var viewModel: ViewModel - - @Environment(\.viewController) private var viewController: UIViewController? - @Environment(\.sceneCoordinator) private var coordinator: SceneCoordinator? - - @State private var accountAction: Int? = nil - @State private var refreshAction: Int? = nil - @State private var importOPMLAction: Int? = nil - @State private var exportOPMLAction: Int? = nil - @State private var aboutAction: Int? = nil - - @State private var isWebsitePresented: Bool = false - @State private var website: String? = nil - - @State private var isOPMLImportPresented: Bool = false - @State private var isOPMLImportDocPickerPresented: Bool = false - @State private var isOPMLExportPresented: Bool = false - @State private var isOPMLExportDocPickerPresented: Bool = false - @State private var opmlAccount: Account? = nil - - var body: some View { - NavigationView { - Form { - buildAccountsSection() - buildTimelineSection() - buildDatabaseSection() - buildAboutSection() - } - .buttonStyle(VibrantButtonStyle(alignment: .leading)) - .navigationBarTitle(Text("Settings"), displayMode: .inline) - .navigationBarItems(leading: Button(action: { self.viewController?.dismiss(animated: true) }) { Text("Done") } ) - } - } - - func buildAccountsSection() -> some View { - Section(header: Text("ACCOUNTS").padding(.top, 22.0)) { - ForEach(viewModel.accounts.indices, id: \.self) { index in - NavigationLink(destination: SettingsDetailAccountView(viewModel: SettingsDetailAccountView.ViewModel(self.viewModel.accounts[index])), tag: index, selection: self.$accountAction) { - Text(verbatim: self.viewModel.accounts[index].nameForDisplay) - } - .modifier(VibrantSelectAction(action: { - self.accountAction = index - })) - } - NavigationLink(destination: SettingsAddAccountView(), tag: 1000, selection: $accountAction) { - Text("Add Account") - } - .modifier(VibrantSelectAction(action: { - self.accountAction = 1000 - })) - } - } - - func buildTimelineSection() -> some View { - Section(header: Text("TIMELINE")) { - Toggle(isOn: $viewModel.sortOldestToNewest) { - Text("Sort Newest to Oldest") - } - Toggle(isOn: $viewModel.groupByFeed) { - Text("Group By Feed") - } - Stepper(value: $viewModel.timelineNumberOfLines, in: 2...6) { - Text("Number of Text Lines: \(viewModel.timelineNumberOfLines)") - } - } - } - - func buildDatabaseSection() -> some View { - Section(header: Text("DATABASE")) { - - NavigationLink(destination: SettingsRefreshSelectionView(selectedInterval: $viewModel.refreshInterval), tag: 1, selection: $refreshAction) { - HStack { - Text("Refresh Interval") - Spacer() - Text(verbatim: self.viewModel.refreshInterval.description()).foregroundColor(.secondary) - } - } - .modifier(VibrantSelectAction(action: { - self.refreshAction = 1 - })) - - NavigationLink(destination: SettingsSubscriptionsImportAccountPickerView(), tag: 1, selection: $importOPMLAction) { - Text("Import Subscriptions") - } - .modifier(VibrantSelectAction(action: { - self.importOPMLAction = 1 - })) - - NavigationLink(destination: SettingsSubscriptionsExportAccountPickerView(), tag: 1, selection: $exportOPMLAction) { - Text("Export Subscriptions") - } - .modifier(VibrantSelectAction(action: { - self.exportOPMLAction = 1 - })) - - } - } - - func buildAboutSection() -> some View { - Section(header: Text("ABOUT"), footer: buildFooter()) { - - NavigationLink(destination: SettingsAboutView(viewModel: SettingsAboutView.ViewModel()), tag: 1, selection: $aboutAction) { - Text("About NetNewsWire") - } - .modifier(VibrantSelectAction(action: { - self.aboutAction = 1 - })) - - Button(action: { - self.isWebsitePresented.toggle() - self.website = "https://ranchero.com/netnewswire/" - }) { - Text("Website") - } - - Button(action: { - self.isWebsitePresented.toggle() - self.website = "https://github.com/brentsimmons/NetNewsWire" - }) { - Text("Github Repository") - } - - Button(action: { - self.isWebsitePresented.toggle() - self.website = "https://github.com/brentsimmons/NetNewsWire/issues" - }) { - Text("Bug Tracker") - } - - Button(action: { - self.isWebsitePresented.toggle() - self.website = "https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes" - }) { - Text("Technotes") - } - - Button(action: { - self.isWebsitePresented.toggle() - self.website = "https://github.com/brentsimmons/NetNewsWire/blob/master/Technotes/HowToSupportNetNewsWire.markdown" - }) { - Text("How To Support NetNewsWire") - } - - if !AccountManager.shared.anyAccountHasFeedWithURL("https://nnw.ranchero.com/feed.json") { - Button(action: { - self.viewController?.dismiss(animated: true) { - let feedName = NSLocalizedString("NetNewsWire News", comment: "NetNewsWire News") - self.coordinator?.showAdd(.feed, initialFeed: "https://nnw.ranchero.com/feed.json", initialFeedName: feedName) - } - }) { - Text("Add NetNewsWire News Feed") - } - } - - }.sheet(isPresented: $isWebsitePresented) { - SafariView(url: URL(string: self.website!)!) - } - } - - func buildFooter() -> some View { - return Text(verbatim: "\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))") - .font(.footnote) - .foregroundColor(.secondary) - } - - // MARK: ViewModel - - class ViewModel: ObservableObject { - - let objectWillChange = ObservableObjectPublisher() - - init() { - NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidAddAccount, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil) - } - - var accounts: [Account] { - get { - return AccountManager.shared.sortedAccounts - } - set { - } - } - - var activeAccounts: [Account] { - get { - return AccountManager.shared.sortedActiveAccounts - } - set { - } - } - - var sortOldestToNewest: Bool { - get { - return AppDefaults.timelineSortDirection == .orderedDescending - } - set { - objectWillChange.send() - if newValue == true { - AppDefaults.timelineSortDirection = .orderedDescending - } else { - AppDefaults.timelineSortDirection = .orderedAscending - } - } - } - - var groupByFeed: Bool { - get { - return AppDefaults.timelineGroupByFeed - } - set { - objectWillChange.send() - AppDefaults.timelineGroupByFeed = newValue - } - } - - var timelineNumberOfLines: Int { - get { - return AppDefaults.timelineNumberOfLines - } - set { - objectWillChange.send() - AppDefaults.timelineNumberOfLines = newValue - } - } - - var refreshInterval: RefreshInterval { - get { - return AppDefaults.refreshInterval - } - set { - objectWillChange.send() - AppDefaults.refreshInterval = newValue - } - } - - @objc func accountsDidChange(_ notification: Notification) { - objectWillChange.send() - } - - @objc func displayNameDidChange(_ notification: Notification) { - objectWillChange.send() - } - - } - -} - -#if DEBUG -struct SettingsView_Previews : PreviewProvider { - static var previews: some View { - SettingsView(viewModel: SettingsView.ViewModel()) - } -} -#endif diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift new file mode 100644 index 000000000..cab947682 --- /dev/null +++ b/iOS/Settings/SettingsViewController.swift @@ -0,0 +1,393 @@ +// +// SettingsViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 4/24/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import Account +import SafariServices + +class SettingsViewController: UITableViewController { + + private let appNewsURLString = "https://nnw.ranchero.com/feed.json" + private weak var opmlAccount: Account? + + static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) + + @IBOutlet weak var refreshIntervalLabel: UILabel! + @IBOutlet weak var timelineSortOrderSwitch: UISwitch! + @IBOutlet weak var groupByFeedSwitch: UISwitch! + @IBOutlet weak var numberOfTextLinesLabel: UILabel! + @IBOutlet weak var numberOfTextLinesSteppper: UIStepper! + + weak var presentingParentController: UIViewController? + + override func viewDidLoad() { + // This hack mostly works around a bug in static tables with dynamic type. See: https://spin.atomicobject.com/2018/10/15/dynamic-type-static-uitableview/ + NotificationCenter.default.removeObserver(tableView!, name: UIContentSizeCategory.didChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange), name: .UserDidAddAccount, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange), name: .UserDidDeleteAccount, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil) + + tableView.register(UINib(nibName: "SettingsAccountTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsAccountTableViewCell") + tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell") + + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if AppDefaults.timelineSortDirection == .orderedAscending { + timelineSortOrderSwitch.isOn = true + } else { + timelineSortOrderSwitch.isOn = false + } + + if AppDefaults.timelineGroupByFeed { + groupByFeedSwitch.isOn = true + } else { + groupByFeedSwitch.isOn = false + } + + refreshIntervalLabel.text = AppDefaults.refreshInterval.description() + + let numberOfTextLines = AppDefaults.timelineNumberOfLines + numberOfTextLinesSteppper.value = Double(numberOfTextLines) + updateNumberOfTextLinesLabel(value: numberOfTextLines) + + let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 20.0, y: 0.0, width: 0.0, height: 0.0)) + buildLabel.font = UIFont.systemFont(ofSize: 11.0) + buildLabel.textColor = UIColor.gray + buildLabel.text = "\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))" + buildLabel.sizeToFit() + buildLabel.translatesAutoresizingMaskIntoConstraints = false + tableView.tableFooterView = buildLabel + + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + } + + // MARK: UITableView + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 1: + return AccountManager.shared.accounts.count + 1 + case 4: + let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section) + if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) { + return defaultNumberOfRows - 1 + } + return defaultNumberOfRows + default: + return super.tableView(tableView, numberOfRowsInSection: section) + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell + switch indexPath.section { + case 1: + + let sortedAccounts = AccountManager.shared.sortedAccounts + if indexPath.row == sortedAccounts.count { + cell = tableView.dequeueReusableCell(withIdentifier: "SettingsTableViewCell", for: indexPath) + cell.textLabel?.adjustsFontForContentSizeCategory = true + cell.textLabel?.text = NSLocalizedString("Add Account", comment: "Accounts") + } else { + let acctCell = tableView.dequeueReusableCell(withIdentifier: "SettingsAccountTableViewCell", for: indexPath) as! SettingsAccountTableViewCell + acctCell.applyThemeProperties() + let account = sortedAccounts[indexPath.row] + acctCell.accountImage?.image = AppAssets.image(for: account.type) + acctCell.accountNameLabel?.text = account.nameForDisplay + cell = acctCell + } + + default: + + cell = super.tableView(tableView, cellForRowAt: indexPath) + + } + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch indexPath.section { + case 0: + UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!) + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + case 1: + let sortedAccounts = AccountManager.shared.sortedAccounts + if indexPath.row == sortedAccounts.count { + let controller = UIStoryboard.settings.instantiateController(ofType: AddAccountViewController.self) + self.navigationController?.pushViewController(controller, animated: true) + } else { + let controller = UIStoryboard.inspector.instantiateController(ofType: AccountInspectorViewController.self) + controller.account = sortedAccounts[indexPath.row] + self.navigationController?.pushViewController(controller, animated: true) + } + case 3: + switch indexPath.row { + case 0: + let timeline = UIStoryboard.settings.instantiateController(ofType: RefreshIntervalViewController.self) + self.navigationController?.pushViewController(timeline, animated: true) + case 1: + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + if let sourceView = tableView.cellForRow(at: indexPath) { + let sourceRect = tableView.rectForRow(at: indexPath) + importOPML(sourceView: sourceView, sourceRect: sourceRect) + } + case 2: + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + if let sourceView = tableView.cellForRow(at: indexPath) { + let sourceRect = tableView.rectForRow(at: indexPath) + exportOPML(sourceView: sourceView, sourceRect: sourceRect) + } + default: + break + } + case 4: + switch indexPath.row { + case 0: + let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self) + self.navigationController?.pushViewController(timeline, animated: true) + case 1: + openURL("https://ranchero.com/netnewswire/") + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + case 2: + openURL("https://github.com/brentsimmons/NetNewsWire/blob/master/Technotes/HowToSupportNetNewsWire.markdown") + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + case 3: + openURL("https://github.com/brentsimmons/NetNewsWire") + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + case 4: + openURL("https://github.com/brentsimmons/NetNewsWire/issues") + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + case 5: + openURL("https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes") + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + case 6: + addFeed() + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + default: + break + } + default: + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + } + } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return false + } + + override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { + return false + } + + override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + return .none + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if indexPath.section == 1 { + return super.tableView(tableView, heightForRowAt: IndexPath(row: 0, section: 1)) + } else { + return super.tableView(tableView, heightForRowAt: indexPath) + } + } + + override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int { + if indexPath.section == 1 { + return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1)) + } else { + return super.tableView(tableView, indentationLevelForRowAt: indexPath) + } + } + + // MARK: Actions + + @IBAction func done(_ sender: Any) { + dismiss(animated: true) + } + + @IBAction func switchTimelineOrder(_ sender: Any) { + if timelineSortOrderSwitch.isOn { + AppDefaults.timelineSortDirection = .orderedAscending + } else { + AppDefaults.timelineSortDirection = .orderedDescending + } + } + + @IBAction func switchGroupByFeed(_ sender: Any) { + if groupByFeedSwitch.isOn { + AppDefaults.timelineGroupByFeed = true + } else { + AppDefaults.timelineGroupByFeed = false + } + } + + @IBAction func stepNumberOfTextLines(_ sender: UIStepper) { + let numberOfLines = Int(sender.value) + AppDefaults.timelineNumberOfLines = numberOfLines + updateNumberOfTextLinesLabel(value: numberOfLines) + } + + // MARK: Notifications + + @objc func contentSizeCategoryDidChange() { + tableView.reloadData() + } + + @objc func accountsDidChange() { + tableView.reloadData() + } + + @objc func displayNameDidChange() { + tableView.reloadData() + } + +} + +// MARK: OPML Document Picker + +extension SettingsViewController: UIDocumentPickerDelegate { + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + for url in urls { + opmlAccount?.importOPML(url) { result in} + } + } + +} + +// MARK: Private + +private extension SettingsViewController { + + func updateNumberOfTextLinesLabel(value: Int) { + let localizedText = NSLocalizedString("Number of Text Lines: %d", comment: "Number of Text Lines") + numberOfTextLinesLabel.text = NSString.localizedStringWithFormat(localizedText as NSString, value) as String + } + + func addFeed() { + self.dismiss(animated: true) + + let addNavViewController = UIStoryboard.add.instantiateInitialViewController() as! UINavigationController + let addViewController = addNavViewController.topViewController as! AddContainerViewController + addNavViewController.modalPresentationStyle = .formSheet + addNavViewController.preferredContentSize = AddContainerViewController.preferredContentSizeForFormSheetDisplay + addViewController.initialControllerType = .feed + addViewController.initialFeed = appNewsURLString + addViewController.initialFeedName = "NetNewsWire News" + + presentingParentController?.present(addNavViewController, animated: true) + } + + func importOPML(sourceView: UIView, sourceRect: CGRect) { + switch AccountManager.shared.activeAccounts.count { + case 0: + presentError(title: "Error", message: NSLocalizedString("You must have at least one active account.", comment: "Missing active account")) + case 1: + opmlAccount = AccountManager.shared.activeAccounts.first + importOPMLDocumentPicker() + default: + importOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect) + } + } + + func importOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) { + let title = NSLocalizedString("Select an Import Account", comment: "Select an Import Account") + let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) + + if let popoverController = alert.popoverPresentationController { + popoverController.sourceView = view + popoverController.sourceRect = sourceRect + } + + for account in AccountManager.shared.sortedActiveAccounts { + let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] action in + self?.opmlAccount = account + self?.importOPMLDocumentPicker() + } + alert.addAction(action) + } + + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") + alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) + + self.present(alert, animated: true) + } + + func importOPMLDocumentPicker() { + let docPicker = UIDocumentPickerViewController(documentTypes: ["public.xml", "org.opml.opml"], in: .import) + docPicker.delegate = self + docPicker.modalPresentationStyle = .formSheet + self.present(docPicker, animated: true) + } + + func exportOPML(sourceView: UIView, sourceRect: CGRect) { + if AccountManager.shared.accounts.count == 1 { + exportOPMLDocumentPicker() + } else { + exportOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect) + } + } + + func exportOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) { + let title = NSLocalizedString("Select an Export Account", comment: "Select an Export Account") + let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) + + if let popoverController = alert.popoverPresentationController { + popoverController.sourceView = view + popoverController.sourceRect = sourceRect + } + + for account in AccountManager.shared.sortedAccounts { + let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] action in + self?.opmlAccount = account + self?.exportOPMLDocumentPicker() + } + alert.addAction(action) + } + + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") + alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) + + self.present(alert, animated: true) + } + + func exportOPMLDocumentPicker() { + guard let account = opmlAccount else { return } + + 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 { + self.presentError(title: "OPML Export Error", message: error.localizedDescription) + } + + let docPicker = UIDocumentPickerViewController(url: tempFile, in: .exportToService) + docPicker.modalPresentationStyle = .formSheet + self.present(docPicker, animated: true) + } + + func openURL(_ urlString: String) { + let vc = SFSafariViewController(url: URL(string: urlString)!) + vc.modalPresentationStyle = .pageSheet + present(vc, animated: true) + } + +} diff --git a/iOS/SwiftUI Extensions/AttributedStringView.swift b/iOS/SwiftUI Extensions/AttributedStringView.swift deleted file mode 100644 index 1cfe55719..000000000 --- a/iOS/SwiftUI Extensions/AttributedStringView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// AttributedStringView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/16/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct AttributedStringView: UIViewRepresentable { - - let string: NSAttributedString - let preferredMaxLayoutWidth: CGFloat - - func makeUIView(context: Context) -> HackedTextView { - return HackedTextView() - } - - func updateUIView(_ view: HackedTextView, context: Context) { - view.attributedText = string - - view.preferredMaxLayoutWidth = preferredMaxLayoutWidth - view.isScrollEnabled = false - view.textContainer.lineBreakMode = .byWordWrapping - - view.isUserInteractionEnabled = true - view.adjustsFontForContentSizeCategory = true - view.font = .preferredFont(forTextStyle: .body) - view.textColor = UIColor.label - view.tintColor = AppAssets.secondaryAccentColor - view.backgroundColor = UIColor.secondarySystemGroupedBackground - - view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - view.setContentCompressionResistancePriority(.required, for: .vertical) - } - -} - -class HackedTextView: UITextView { - var preferredMaxLayoutWidth = CGFloat.zero - override var intrinsicContentSize: CGSize { - return sizeThatFits(CGSize(width: preferredMaxLayoutWidth, height: .infinity)) - } -} diff --git a/iOS/SwiftUI Extensions/PasswordField.swift b/iOS/SwiftUI Extensions/PasswordField.swift deleted file mode 100644 index 8875837ae..000000000 --- a/iOS/SwiftUI Extensions/PasswordField.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// PasswordField.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 10/8/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct PasswordField: UIViewRepresentable { - - let password: Binding - - func makeUIView(context: Context) -> ShowHidePasswordView { - let showHideView = Bundle.main.loadNibNamed("ShowHidePasswordView", owner: Self.self, options: nil)?[0] as! ShowHidePasswordView - showHideView.passwordTextField.bindingString = password - return showHideView - } - - func updateUIView(_ showHideView: ShowHidePasswordView, context: Context) { - showHideView.passwordTextField.bindingString = password - } - -} diff --git a/iOS/SwiftUI Extensions/SafariView.swift b/iOS/SwiftUI Extensions/SafariView.swift deleted file mode 100644 index 6b5d78334..000000000 --- a/iOS/SwiftUI Extensions/SafariView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SafariView.swift -// NetNewsWire-iOS -// -// Created by Stuart Breckenridge on 16/6/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import SafariServices - -struct SafariView : UIViewControllerRepresentable { - - let url: URL - - func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { - let safari = SFSafariViewController(url: url) - safari.delegate = context.coordinator - return safari - } - - func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { - // - } - - func makeCoordinator() -> Coordinator { - return Coordinator(self) - } - - class Coordinator : NSObject, SFSafariViewControllerDelegate { - var parent: SafariView - - init(_ safariView: SafariView) { - self.parent = safariView - } - - // MARK: SFSafariViewControllerDelegate - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { - - } - - func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) { - - } - - func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - - } - } -} - - diff --git a/iOS/SwiftUI Extensions/ShowHidePasswordView.swift b/iOS/SwiftUI Extensions/ShowHidePasswordView.swift deleted file mode 100644 index fe6b10d02..000000000 --- a/iOS/SwiftUI Extensions/ShowHidePasswordView.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ShowHidePasswordView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 10/8/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import UIKit -import SwiftUI - -class ShowHidePasswordView: UIView { - - @IBOutlet weak var passwordTextField: BindingTextField! - @IBOutlet weak var showHideButton: UIButton! - - @IBAction func toggleShowHideButton(_ sender: Any) { - if passwordTextField.isSecureTextEntry { - passwordTextField.isSecureTextEntry = false - showHideButton.setTitle(NSLocalizedString("Hide", comment: "Hide"), for: .normal) - } else { - passwordTextField.isSecureTextEntry = true - showHideButton.setTitle(NSLocalizedString("Show", comment: "Show"), for: .normal) - } - } - -} - -class BindingTextField: UITextField, UITextFieldDelegate { - - var bindingString: Binding? = nil - - override init(frame: CGRect) { - super.init(frame: frame) - delegate = self - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - delegate = self - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if let currentValue = textField.text as NSString? { - let proposedValue = currentValue.replacingCharacters(in: range, with: string) - bindingString?.wrappedValue = proposedValue - } - return true - } - -} diff --git a/iOS/SwiftUI Extensions/ShowHidePasswordView.xib b/iOS/SwiftUI Extensions/ShowHidePasswordView.xib deleted file mode 100644 index d68686200..000000000 --- a/iOS/SwiftUI Extensions/ShowHidePasswordView.xib +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iOS/SwiftUI Extensions/VibrantButtonStyle.swift b/iOS/SwiftUI Extensions/VibrantButtonStyle.swift deleted file mode 100644 index 61ffd9517..000000000 --- a/iOS/SwiftUI Extensions/VibrantButtonStyle.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// VibrantButtonStyle.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/16/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct VibrantButtonStyle: ButtonStyle { - - let alignment: Alignment - - func makeBody(configuration: Configuration) -> some View { - GeometryReader { geometry in - configuration.label - .frame(width: geometry.size.width, height: geometry.size.height, alignment: self.alignment) - } - .foregroundColor(configuration.isPressed ? Color(AppAssets.tableViewCellHighlightedTextColor) : .primary) - .listRowBackground(configuration.isPressed ? Color(AppAssets.primaryAccentColor) : Color(.secondarySystemGroupedBackground)) - .background(configuration.isPressed ? Color(AppAssets.primaryAccentColor) : Color(.secondarySystemGroupedBackground)) - } - -} diff --git a/iOS/SwiftUI Extensions/VibrantSelectAction.swift b/iOS/SwiftUI Extensions/VibrantSelectAction.swift deleted file mode 100644 index 45471d5e1..000000000 --- a/iOS/SwiftUI Extensions/VibrantSelectAction.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// VibrantSelectAction.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/15/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct VibrantSelectAction: ViewModifier { - - let action: () -> Void - @State var isTapped = false - @GestureState var isLongPressed = false - - func body(content: Content) -> some View { - GeometryReader { geometry in - content - .frame(width: geometry.size.width, height: geometry.size.height, alignment: .leading) - .background(self.isLongPressed || self.isTapped ? Color(AppAssets.primaryAccentColor) : Color(.secondarySystemGroupedBackground)) - } - .foregroundColor(isLongPressed || isTapped ? Color(AppAssets.tableViewCellHighlightedTextColor) : .primary) - .listRowBackground(isLongPressed || isTapped ? Color(AppAssets.primaryAccentColor) : nil) - .gesture( - LongPressGesture().onEnded( { _ in self.action() }) - .updating($isLongPressed) { value, state, transcation in state = value } - .simultaneously(with: - TapGesture().onEnded( { - self.isTapped = true - self.action() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - self.isTapped = false - } - } - )) - ) - } - -} diff --git a/iOS/UIKit Extensions/ThemedNavigationController.swift b/iOS/UIKit Extensions/ThemedNavigationController.swift index 979d83b78..058e6afa5 100644 --- a/iOS/UIKit Extensions/ThemedNavigationController.swift +++ b/iOS/UIKit Extensions/ThemedNavigationController.swift @@ -34,10 +34,10 @@ class ThemedNavigationController: UINavigationController { if traitCollection.userInterfaceStyle == .dark { navigationBar.standardAppearance = UINavigationBarAppearance() - navigationBar.tintColor = view.tintColor + navigationBar.tintColor = AppAssets.primaryAccentColor toolbar.standardAppearance = UIToolbarAppearance() toolbar.compactAppearance = UIToolbarAppearance() - toolbar.tintColor = view.tintColor + toolbar.tintColor = AppAssets.primaryAccentColor } else { let navigationAppearance = UINavigationBarAppearance() navigationAppearance.backgroundColor = AppAssets.barBackgroundColor diff --git a/iOS/UIKit Extensions/UIStoryboard-Extensions.swift b/iOS/UIKit Extensions/UIStoryboard-Extensions.swift index f0b67d6ff..ef48e061d 100644 --- a/iOS/UIKit Extensions/UIStoryboard-Extensions.swift +++ b/iOS/UIKit Extensions/UIStoryboard-Extensions.swift @@ -10,6 +10,8 @@ import UIKit extension UIStoryboard { + static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) + static var main: UIStoryboard { return UIStoryboard(name: "Main", bundle: nil) } @@ -22,6 +24,14 @@ extension UIStoryboard { return UIStoryboard(name: "Settings", bundle: nil) } + static var inspector: UIStoryboard { + return UIStoryboard(name: "Inspector", bundle: nil) + } + + static var account: UIStoryboard { + return UIStoryboard(name: "Account", bundle: nil) + } + func instantiateController(ofType type: T.Type = T.self) -> T where T: UIViewController { let storyboardId = String(describing: type) diff --git a/iOS/UIKit Extensions/VibrantButton.swift b/iOS/UIKit Extensions/VibrantButton.swift new file mode 100644 index 000000000..dc311d549 --- /dev/null +++ b/iOS/UIKit Extensions/VibrantButton.swift @@ -0,0 +1,48 @@ +// +// VibrantButton.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 10/22/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class VibrantButton: UIButton { + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + setTitleColor(AppAssets.vibrantTextColor, for: .highlighted) + } + + override var isHighlighted: Bool { + didSet { + backgroundColor = isHighlighted ? AppAssets.secondaryAccentColor : nil + titleLabel?.alpha = 1 + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + isHighlighted = true + super.touchesBegan(touches, with: event) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + isHighlighted = false + super.touchesEnded(touches, with: event) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + isHighlighted = false + super.touchesCancelled(touches, with: event) + } + +} diff --git a/iOS/UIKit Extensions/VibrantLabel.swift b/iOS/UIKit Extensions/VibrantLabel.swift new file mode 100644 index 000000000..9e480a7c4 --- /dev/null +++ b/iOS/UIKit Extensions/VibrantLabel.swift @@ -0,0 +1,27 @@ +// +// VibrantLabel.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 10/22/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class VibrantLabel: UILabel { + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + highlightedTextColor = AppAssets.vibrantTextColor + } + +} diff --git a/iOS/UIKit Extensions/NNWTableViewCell.swift b/iOS/UIKit Extensions/VibrantTableViewCell.swift similarity index 74% rename from iOS/UIKit Extensions/NNWTableViewCell.swift rename to iOS/UIKit Extensions/VibrantTableViewCell.swift index 1cea8c90c..bd6998bca 100644 --- a/iOS/UIKit Extensions/NNWTableViewCell.swift +++ b/iOS/UIKit Extensions/VibrantTableViewCell.swift @@ -1,5 +1,5 @@ // -// NNWTableViewCell.swift +// VibrantTableViewCell.swift // NetNewsWire-iOS // // Created by Jim Correia on 9/2/19. @@ -8,7 +8,8 @@ import UIKit -class NNWTableViewCell: UITableViewCell { +class VibrantTableViewCell: UITableViewCell { + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonInit() @@ -26,7 +27,9 @@ class NNWTableViewCell: UITableViewCell { /// Subclass overrides should call super func applyThemeProperties() { let selectedBackgroundView = UIView(frame: .zero) - selectedBackgroundView.backgroundColor = AppAssets.primaryAccentColor + selectedBackgroundView.backgroundColor = AppAssets.secondaryAccentColor self.selectedBackgroundView = selectedBackgroundView + + textLabel?.highlightedTextColor = AppAssets.vibrantTextColor } } diff --git a/submodules/RSCore b/submodules/RSCore index 29dc34284..fa16a5b1a 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit 29dc34284b64af4a399d1cf3927c3469851ec0ad +Subproject commit fa16a5b1a0bc45bb6aee6145e1095446a84f386b diff --git a/submodules/Sparkle b/submodules/Sparkle index d5222353e..67819be18 160000 --- a/submodules/Sparkle +++ b/submodules/Sparkle @@ -1 +1 @@ -Subproject commit d5222353e6d6ee7baf4c18711e5b897fe2ed886a +Subproject commit 67819be18a4ef48e85ea30dbbf8de582f5ef405c diff --git a/xcconfig/NetNewsWire_project_test.xcconfig b/xcconfig/NetNewsWire_project_test.xcconfig index ad10a9ae1..fa08e7ae8 100644 --- a/xcconfig/NetNewsWire_project_test.xcconfig +++ b/xcconfig/NetNewsWire_project_test.xcconfig @@ -1,3 +1,4 @@ #include "./NetNewsWire_project_debug.xcconfig" OTHER_SWIFT_FLAGS = -DTEST $(inherited) +FRAMEWORK_SEARCH_PATHS = $(inherited) $(SYMROOT)/Release$(EFFECTIVE_PLATFORM_NAME) diff --git a/xcconfig/NetNewsWire_safariextension_target_macappstore.xcconfig b/xcconfig/NetNewsWire_safariextension_target_macappstore.xcconfig new file mode 100644 index 000000000..c9f4822a1 --- /dev/null +++ b/xcconfig/NetNewsWire_safariextension_target_macappstore.xcconfig @@ -0,0 +1,44 @@ +CODE_SIGN_IDENTITY[config=Release] = 3rd Party Mac Developer Application +CODE_SIGN_IDENTITY[config=Debug] = - +DEVELOPMENT_TEAM = M8L2WTLA8W +CODE_SIGN_STYLE = Manual +ORGANIZATION_IDENTIFIER = com.ranchero +PROVISIONING_PROFILE_SPECIFIER = + +// developers can locally override the Xcode settings for code signing +// by creating a DeveloperSettings.xcconfig file locally at the appropriate path +// This allows a pristine project to have code signing set up with the appropriate +// developer ID and certificates, and for dev to be able to have local settings +// without needing to check in anything into source control +// +// As an example, make a ../../SharedXcodeSettings/DeveloperSettings.xcconfig file and +// give it the contents +// +// CODE_SIGN_IDENTITY[sdk=macosx*] = Mac Developer +// CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer +// CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = iPhone Developer +// DEVELOPMENT_TEAM = +// ORGANIZATION_IDENTIFIER = +// CODE_SIGN_STYLE = Automatic +// PROVISIONING_PROFILE_SPECIFIER = +// +// And you should be able to build without code signing errors and without modifying +// the NetNewsWire Xcode project. +// +// Example: if your NetNewsWire Xcode project file is at +// /Users/Shared/git/NetNewsWire/NetNewsWire.xcodeproj +// create your DeveloperSettings.xcconfig file at +// /Users/Shared/git/SharedXcodeSettings/DeveloperSettings.xcconfig +// + +#include? "../../SharedXcodeSettings/DeveloperSettings.xcconfig" +#include "./common/NetNewsWire_mac_target_common.xcconfig" + +CODE_SIGN_ENTITLEMENTS = Mac/SafariExtension/Subscribe_to_Feed.entitlements +INFOPLIST_FILE = Mac/SafariExtension/Info.plist +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks +PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire-Evergreen.MAS.Subscribe-to-Feed +PRODUCT_NAME = $(TARGET_NAME) +OTHER_SWIFT_FLAGS = -DMAC_APP_STORE $(inherited) + +SDKROOT = macosx