Added initial POC version of NetNewsWire for iOS to use as a starting point for the actual app.

This commit is contained in:
Maurice Parker 2019-04-15 15:03:05 -05:00
parent 8f1f153e98
commit 8526db8b4c
47 changed files with 4454 additions and 220 deletions

View File

@ -7,6 +7,7 @@
//
import AppKit
import RSCore
extension NSImage.Name {
static let star = NSImage.Name("star")
@ -15,13 +16,13 @@ extension NSImage.Name {
struct AppImages {
static var genericFeedImage: NSImage? = {
static var genericFeedImage: RSImage? = {
let path = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/BookmarkIcon.icns"
let image = NSImage(contentsOfFile: path)
let image = RSImage(contentsOfFile: path)
return image
}()
static var timelineStar: NSImage! = {
return NSImage(named: .timelineStar)
static var timelineStar: RSImage! = {
return RSImage(named: .timelineStar)
}()
}

View File

@ -33,6 +33,65 @@
51C451F52264C83900C03939 /* Articles.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 840716732262A60F00344432 /* Articles.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
51C451F82264C83E00C03939 /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407166A2262A60D00344432 /* Account.framework */; };
51C451F92264C83E00C03939 /* Account.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8407166A2262A60D00344432 /* Account.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
51C45258226508CF00C03939 /* AppAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45254226507D200C03939 /* AppAssets.swift */; };
51C45259226508D300C03939 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; };
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */; };
51C4525B226508DA00C03939 /* UIImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4524F226506F400C03939 /* UIImage-Extensions.swift */; };
51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45250226506F400C03939 /* String-Extensions.swift */; };
51C45266226508F600C03939 /* MasterSecondaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4525E226508F600C03939 /* MasterSecondaryViewController.swift */; };
51C45267226508F600C03939 /* MasterPrimaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4525F226508F600C03939 /* MasterPrimaryViewController.swift */; };
51C45268226508F600C03939 /* MasterUnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45261226508F600C03939 /* MasterUnreadCountView.swift */; };
51C45269226508F600C03939 /* MasterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45262226508F600C03939 /* MasterTableViewCell.swift */; };
51C4526A226508F600C03939 /* MasterTableViewCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45263226508F600C03939 /* MasterTableViewCellLayout.swift */; };
51C4526B226508F600C03939 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45264226508F600C03939 /* MasterViewController.swift */; };
51C4526C226508F600C03939 /* MasterTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45265226508F600C03939 /* MasterTreeControllerDelegate.swift */; };
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4526E2265091600C03939 /* MasterTimelineViewController.swift */; };
51C452772265091600C03939 /* MultilineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452702265091600C03939 /* MultilineUILabelSizer.swift */; };
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452712265091600C03939 /* MasterTimelineCellData.swift */; };
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452722265091600C03939 /* MasterTimelineTableViewCell.swift */; };
51C4527A2265091600C03939 /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452732265091600C03939 /* SingleLineUILabelSizer.swift */; };
51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452742265091600C03939 /* MasterUnreadIndicatorView.swift */; };
51C4527C2265091600C03939 /* MasterTimelineCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452752265091600C03939 /* MasterTimelineCellLayout.swift */; };
51C4527F2265092C00C03939 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4527E2265092C00C03939 /* DetailViewController.swift */; };
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452812265093600C03939 /* AddFeedFolderPickerData.swift */; };
51C452862265093600C03939 /* AddFeed.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C452822265093600C03939 /* AddFeed.storyboard */; };
51C452872265093600C03939 /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452832265093600C03939 /* FolderTreeControllerDelegate.swift */; };
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452842265093600C03939 /* AddFeedViewController.swift */; };
51C4528C2265095F00C03939 /* AddFolder.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C4528A2265095F00C03939 /* AddFolder.storyboard */; };
51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4528B2265095F00C03939 /* AddFolderViewController.swift */; };
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; };
51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5391FC2308B00998D64 /* UnreadFeed.swift */; };
51C45290226509C100C03939 /* PseudoFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */; };
51C45291226509C800C03939 /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; };
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */; };
51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */; };
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; };
51C45296226509D300C03939 /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; };
51C45297226509E300C03939 /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; };
51C45298226509E600C03939 /* OPMLImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */; };
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; };
51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; };
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; };
51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; };
51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; };
51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; };
51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611891FCB67AA0086A189 /* FeedIconDownloader.swift */; };
51C452A122650A1900C03939 /* FeaturedImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */; };
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; };
51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */; };
51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97581ED9EB0D007D329B /* ArticleUtilities.swift */; };
51C452A522650A2D00C03939 /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; };
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; };
51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; };
51C452A922650DC600C03939 /* ArticleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */; };
51C452AB22650DC600C03939 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; };
51C452AC22650FD200C03939 /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */; };
51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* TimelineStringFormatter.swift */; };
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; };
51C452B1226510E600C03939 /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A01ED9F180007D329B /* InitialFeedDownloader.swift */; };
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
51EC114C2149FE3300B296E3 /* FolderTreeMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EC114B2149FE3300B296E3 /* FolderTreeMenu.swift */; };
6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; };
6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */; };
@ -47,8 +106,6 @@
840958632201629A002C1579 /* Subscribe to Feed.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
840BEE4121D70E64009BBAFA /* CrashReportWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840BEE4021D70E64009BBAFA /* CrashReportWindowController.swift */; };
840D617F2029031C009BC708 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D617E2029031C009BC708 /* AppDelegate.swift */; };
840D61812029031C009BC708 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61802029031C009BC708 /* MasterViewController.swift */; };
840D61832029031C009BC708 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61822029031C009BC708 /* DetailViewController.swift */; };
840D61962029031D009BC708 /* NetNewsWire_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */; };
84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */; };
841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */; };
@ -543,6 +600,34 @@
5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailKeyboardDelegate.swift; sourceTree = "<group>"; };
5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; sourceTree = "<group>"; };
519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard-Extensions.swift"; sourceTree = "<group>"; };
51C4524F226506F400C03939 /* UIImage-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage-Extensions.swift"; sourceTree = "<group>"; };
51C45250226506F400C03939 /* String-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String-Extensions.swift"; sourceTree = "<group>"; };
51C45254226507D200C03939 /* AppAssets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppAssets.swift; sourceTree = "<group>"; };
51C45255226507D200C03939 /* AppDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = "<group>"; };
51C4525E226508F600C03939 /* MasterSecondaryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterSecondaryViewController.swift; sourceTree = "<group>"; };
51C4525F226508F600C03939 /* MasterPrimaryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterPrimaryViewController.swift; sourceTree = "<group>"; };
51C45261226508F600C03939 /* MasterUnreadCountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterUnreadCountView.swift; sourceTree = "<group>"; };
51C45262226508F600C03939 /* MasterTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTableViewCell.swift; sourceTree = "<group>"; };
51C45263226508F600C03939 /* MasterTableViewCellLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTableViewCellLayout.swift; sourceTree = "<group>"; };
51C45264226508F600C03939 /* MasterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = "<group>"; };
51C45265226508F600C03939 /* MasterTreeControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTreeControllerDelegate.swift; sourceTree = "<group>"; };
51C4526E2265091600C03939 /* MasterTimelineViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTimelineViewController.swift; sourceTree = "<group>"; };
51C452702265091600C03939 /* MultilineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultilineUILabelSizer.swift; sourceTree = "<group>"; };
51C452712265091600C03939 /* MasterTimelineCellData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTimelineCellData.swift; sourceTree = "<group>"; };
51C452722265091600C03939 /* MasterTimelineTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTimelineTableViewCell.swift; sourceTree = "<group>"; };
51C452732265091600C03939 /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = "<group>"; };
51C452742265091600C03939 /* MasterUnreadIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterUnreadIndicatorView.swift; sourceTree = "<group>"; };
51C452752265091600C03939 /* MasterTimelineCellLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTimelineCellLayout.swift; sourceTree = "<group>"; };
51C4527E2265092C00C03939 /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
51C452812265093600C03939 /* AddFeedFolderPickerData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFeedFolderPickerData.swift; sourceTree = "<group>"; };
51C452822265093600C03939 /* AddFeed.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = AddFeed.storyboard; sourceTree = "<group>"; };
51C452832265093600C03939 /* FolderTreeControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FolderTreeControllerDelegate.swift; sourceTree = "<group>"; };
51C452842265093600C03939 /* AddFeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFeedViewController.swift; sourceTree = "<group>"; };
51C4528A2265095F00C03939 /* AddFolder.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = AddFolder.storyboard; sourceTree = "<group>"; };
51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = "<group>"; };
51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
51EC114B2149FE3300B296E3 /* FolderTreeMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FolderTreeMenu.swift; path = AddFeed/FolderTreeMenu.swift; sourceTree = "<group>"; };
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; };
@ -561,8 +646,6 @@
840BEE4021D70E64009BBAFA /* CrashReportWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportWindowController.swift; sourceTree = "<group>"; };
840D617C2029031C009BC708 /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; };
840D617E2029031C009BC708 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
840D61802029031C009BC708 /* MasterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = "<group>"; };
840D61822029031C009BC708 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
840D61912029031D009BC708 /* NetNewsWire-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "NetNewsWire-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetNewsWire_iOSTests.swift; sourceTree = "<group>"; };
840D61972029031D009BC708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -636,7 +719,7 @@
849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleStylesManager.swift; sourceTree = "<group>"; };
849A97971ED9EFAA007D329B /* Node-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Node-Extensions.swift"; sourceTree = "<group>"; };
849A979E1ED9F130007D329B /* SidebarCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarCell.swift; sourceTree = "<group>"; };
849A97A01ED9F180007D329B /* InitialFeedDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InitialFeedDownloader.swift; path = AddFeed/InitialFeedDownloader.swift; sourceTree = "<group>"; };
849A97A01ED9F180007D329B /* InitialFeedDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialFeedDownloader.swift; sourceTree = "<group>"; };
849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FolderTreeControllerDelegate.swift; path = AddFeed/FolderTreeControllerDelegate.swift; sourceTree = "<group>"; };
849C64601ED37A5D003D8FC0 /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; };
849C64671ED37A5D003D8FC0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -750,6 +833,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */,
51C451D22264C7F200C03939 /* RSWeb.framework in Frameworks */,
51C451E02264C7F900C03939 /* RSTree.framework in Frameworks */,
51C451F82264C83E00C03939 /* Account.framework in Frameworks */,
@ -803,6 +887,122 @@
path = Keyboard;
sourceTree = "<group>";
};
51C45245226506C800C03939 /* Extensions */ = {
isa = PBXGroup;
children = (
51C45250226506F400C03939 /* String-Extensions.swift */,
51C4524F226506F400C03939 /* UIImage-Extensions.swift */,
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
51C4525D226508F600C03939 /* Master */ = {
isa = PBXGroup;
children = (
51C4525E226508F600C03939 /* MasterSecondaryViewController.swift */,
51C4525F226508F600C03939 /* MasterPrimaryViewController.swift */,
51C45264226508F600C03939 /* MasterViewController.swift */,
51C45265226508F600C03939 /* MasterTreeControllerDelegate.swift */,
51C45260226508F600C03939 /* Cell */,
);
path = Master;
sourceTree = "<group>";
};
51C45260226508F600C03939 /* Cell */ = {
isa = PBXGroup;
children = (
51C45261226508F600C03939 /* MasterUnreadCountView.swift */,
51C45262226508F600C03939 /* MasterTableViewCell.swift */,
51C45263226508F600C03939 /* MasterTableViewCellLayout.swift */,
);
path = Cell;
sourceTree = "<group>";
};
51C4526D2265091600C03939 /* Timeline */ = {
isa = PBXGroup;
children = (
51C4526E2265091600C03939 /* MasterTimelineViewController.swift */,
51C4526F2265091600C03939 /* Cell */,
);
path = Timeline;
sourceTree = "<group>";
};
51C4526F2265091600C03939 /* Cell */ = {
isa = PBXGroup;
children = (
51C452702265091600C03939 /* MultilineUILabelSizer.swift */,
51C452712265091600C03939 /* MasterTimelineCellData.swift */,
51C452722265091600C03939 /* MasterTimelineTableViewCell.swift */,
51C452732265091600C03939 /* SingleLineUILabelSizer.swift */,
51C452742265091600C03939 /* MasterUnreadIndicatorView.swift */,
51C452752265091600C03939 /* MasterTimelineCellLayout.swift */,
);
path = Cell;
sourceTree = "<group>";
};
51C4527D2265092C00C03939 /* Detail */ = {
isa = PBXGroup;
children = (
51C4527E2265092C00C03939 /* DetailViewController.swift */,
);
path = Detail;
sourceTree = "<group>";
};
51C452802265093600C03939 /* Add Feed */ = {
isa = PBXGroup;
children = (
51C452812265093600C03939 /* AddFeedFolderPickerData.swift */,
51C452822265093600C03939 /* AddFeed.storyboard */,
51C452832265093600C03939 /* FolderTreeControllerDelegate.swift */,
51C452842265093600C03939 /* AddFeedViewController.swift */,
);
path = "Add Feed";
sourceTree = "<group>";
};
51C452892265095F00C03939 /* Add Folder */ = {
isa = PBXGroup;
children = (
51C4528A2265095F00C03939 /* AddFolder.storyboard */,
51C4528B2265095F00C03939 /* AddFolderViewController.swift */,
);
path = "Add Folder";
sourceTree = "<group>";
};
51C452A822650DA100C03939 /* Article Rendering */ = {
isa = PBXGroup;
children = (
849A977D1ED9EC42007D329B /* ArticleRenderer.swift */,
848362FE2262A30E00DA1D35 /* template.html */,
);
path = "Article Rendering";
sourceTree = "<group>";
};
51C452AD2265102800C03939 /* Timeline */ = {
isa = PBXGroup;
children = (
84F204DF1FAACBB30076E152 /* ArticleArray.swift */,
849A97731ED9EC04007D329B /* TimelineStringFormatter.swift */,
);
path = Timeline;
sourceTree = "<group>";
};
51C452B0226510CF00C03939 /* Add Feed */ = {
isa = PBXGroup;
children = (
849A97A01ED9F180007D329B /* InitialFeedDownloader.swift */,
);
path = "Add Feed";
sourceTree = "<group>";
};
51C452B22265141B00C03939 /* Frameworks */ = {
isa = PBXGroup;
children = (
51C452B32265141B00C03939 /* WebKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
6581C73620CED60100F4AD34 /* SafariExtension */ = {
isa = PBXGroup;
children = (
@ -983,7 +1183,6 @@
848363002262A3BC00DA1D35 /* AddFeedSheet.xib */,
849A97511ED9EAC0007D329B /* AddFeedController.swift */,
849A97521ED9EAC0007D329B /* AddFeedWindowController.swift */,
849A97A01ED9F180007D329B /* InitialFeedDownloader.swift */,
51EC114B2149FE3300B296E3 /* FolderTreeMenu.swift */,
849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */,
);
@ -1026,7 +1225,6 @@
8405DDA122168920008CE1BF /* TimelineTableView.xib */,
849A976B1ED9EBC8007D329B /* TimelineViewController.swift */,
84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */,
84F204DF1FAACBB30076E152 /* ArticleArray.swift */,
849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */,
849A976A1ED9EBC8007D329B /* TimelineTableView.swift */,
844B5B6C1FEA282400C7C76A /* Keyboard */,
@ -1045,7 +1243,6 @@
84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */,
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */,
849A97711ED9EC04007D329B /* TimelineCellData.swift */,
849A97731ED9EC04007D329B /* TimelineStringFormatter.swift */,
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */,
);
path = Cell;
@ -1058,10 +1255,8 @@
8405DD892213E0E3008CE1BF /* DetailContainerView.swift */,
84216D0222128B9D0049B9B9 /* DetailWebViewController.swift */,
84E8E0EA202F693600562D8F /* DetailWebView.swift */,
849A977D1ED9EC42007D329B /* ArticleRenderer.swift */,
84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */,
848362FC2262A30800DA1D35 /* styleSheet.css */,
848362FE2262A30E00DA1D35 /* template.html */,
5127B235222B4849006D641D /* Keyboard */,
);
path = Detail;
@ -1103,6 +1298,7 @@
84C37F8620DD8CF800CA8CF5 /* RSParser.xcodeproj */,
84C37F8F20DD8CFD00CA8CF5 /* RSTree.xcodeproj */,
84C37F9820DD8D0400CA8CF5 /* RSWeb.xcodeproj */,
51C452B22265141B00C03939 /* Frameworks */,
);
sourceTree = "<group>";
usesTabs = 1;
@ -1195,7 +1391,6 @@
848363062262A3DD00DA1D35 /* Main.storyboard */,
84C9FC6622629B3900D921D6 /* AppDelegate.swift */,
84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */,
842E45CD1ED8C308000A8B52 /* AppNotifications.swift */,
849EE70E203919360082A1EA /* AppImages.swift */,
842E45DC1ED8C54B000A8B52 /* Browser.swift */,
842E45E11ED8C681000A8B52 /* MainWindow */,
@ -1216,7 +1411,11 @@
846E77301F6EF5D600A165E2 /* Account.xcodeproj */,
841D4D542106B3D500DD04E6 /* Articles.xcodeproj */,
841D4D5E2106B3E100DD04E6 /* ArticlesDatabase.xcodeproj */,
842E45CD1ED8C308000A8B52 /* AppNotifications.swift */,
51C452AD2265102800C03939 /* Timeline */,
51C452B0226510CF00C03939 /* Add Feed */,
84702AB31FA27AE8006B8943 /* Commands */,
51C452A822650DA100C03939 /* Article Rendering */,
849A97861ED9ECEF007D329B /* Article Styles */,
84DAEE201F86CAE00058304B /* Importers */,
8444C9011FED81880051386C /* Exporters */,
@ -1308,8 +1507,14 @@
84C9FCA22262A1B800D921D6 /* LaunchScreen.storyboard */,
84C9FC9F2262A1B300D921D6 /* Main.storyboard */,
840D617E2029031C009BC708 /* AppDelegate.swift */,
840D61802029031C009BC708 /* MasterViewController.swift */,
840D61822029031C009BC708 /* DetailViewController.swift */,
51C45254226507D200C03939 /* AppAssets.swift */,
51C45255226507D200C03939 /* AppDefaults.swift */,
51C4525D226508F600C03939 /* Master */,
51C4526D2265091600C03939 /* Timeline */,
51C4527D2265092C00C03939 /* Detail */,
51C452802265093600C03939 /* Add Feed */,
51C452892265095F00C03939 /* Add Folder */,
51C45245226506C800C03939 /* Extensions */,
84C9FC9A2262A1A900D921D6 /* Resources */,
);
path = iOS;
@ -1318,6 +1523,7 @@
84C9FC9A2262A1A900D921D6 /* Resources */ = {
isa = PBXGroup;
children = (
51C452B72265178500C03939 /* styleSheet.css */,
84C9FC9B2262A1A900D921D6 /* Assets.xcassets */,
84C9FC9C2262A1A900D921D6 /* Info.plist */,
);
@ -1815,10 +2021,14 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
51C452862265093600C03939 /* AddFeed.storyboard in Resources */,
84C9FCA12262A1B300D921D6 /* Main.storyboard in Resources */,
51C4528C2265095F00C03939 /* AddFolder.storyboard in Resources */,
84C9FCA42262A1B800D921D6 /* LaunchScreen.storyboard in Resources */,
51C452AB22650DC600C03939 /* template.html in Resources */,
84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */,
84C9FC9D2262A1A900D921D6 /* Assets.xcassets in Resources */,
51C452B82265178500C03939 /* styleSheet.css in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1915,12 +2125,64 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
840D61832029031C009BC708 /* DetailViewController.swift in Sources */,
840D61812029031C009BC708 /* MasterViewController.swift in Sources */,
840D617F2029031C009BC708 /* AppDelegate.swift in Sources */,
51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */,
51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */,
51C45296226509D300C03939 /* OPMLExporter.swift in Sources */,
51C4525B226508DA00C03939 /* UIImage-Extensions.swift in Sources */,
84F3EE1720DEC97E003FADEB /* FeedFinder.swift in Sources */,
51C452872265093600C03939 /* FolderTreeControllerDelegate.swift in Sources */,
51C45291226509C800C03939 /* SmartFeed.swift in Sources */,
51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */,
51C45269226508F600C03939 /* MasterTableViewCell.swift in Sources */,
51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */,
51C452772265091600C03939 /* MultilineUILabelSizer.swift in Sources */,
51C452A522650A2D00C03939 /* SmallIconProvider.swift in Sources */,
51C452A122650A1900C03939 /* FeaturedImageDownloader.swift in Sources */,
51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */,
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */,
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */,
51C4526B226508F600C03939 /* MasterViewController.swift in Sources */,
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
51C452A022650A1900C03939 /* FeedIconDownloader.swift in Sources */,
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
84F3EE1B20DEC97E003FADEB /* HTMLFeedFinder.swift in Sources */,
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */,
51C4526C226508F600C03939 /* MasterTreeControllerDelegate.swift in Sources */,
51C45258226508CF00C03939 /* AppAssets.swift in Sources */,
51C4527C2265091600C03939 /* MasterTimelineCellLayout.swift in Sources */,
51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */,
51C45267226508F600C03939 /* MasterPrimaryViewController.swift in Sources */,
51C4527F2265092C00C03939 /* DetailViewController.swift in Sources */,
51C4526A226508F600C03939 /* MasterTableViewCellLayout.swift in Sources */,
51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */,
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */,
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
84F3EE1920DEC97E003FADEB /* FeedSpecifier.swift in Sources */,
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */,
51C45290226509C100C03939 /* PseudoFeed.swift in Sources */,
51C452A922650DC600C03939 /* ArticleRenderer.swift in Sources */,
51C45297226509E300C03939 /* DefaultFeedsImporter.swift in Sources */,
51C452AC22650FD200C03939 /* AppNotifications.swift in Sources */,
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */,
51C4527A2265091600C03939 /* SingleLineUILabelSizer.swift in Sources */,
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
51C45268226508F600C03939 /* MasterUnreadCountView.swift in Sources */,
51C452B1226510E600C03939 /* InitialFeedDownloader.swift in Sources */,
51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */,
51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */,
51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */,
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
51C45259226508D300C03939 /* AppDefaults.swift in Sources */,
51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */,
51C45266226508F600C03939 /* MasterSecondaryViewController.swift in Sources */,
51C45298226509E600C03939 /* OPMLImporter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -6,7 +6,7 @@
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import AppKit
import Foundation
import Articles
extension Notification.Name {

View File

@ -196,7 +196,11 @@ private extension ArticleRenderer {
if let icon = appDelegate.feedIconDownloader.icon(for: feed) {
if let s = base64String(forImage: icon) {
#if os(macOS)
let imgTag = "<img src=\"data:image/tiff;base64, " + s + "\" height=48 width=48 />"
#else
let imgTag = "<img src=\"data:image/png;base64, " + s + "\" height=48 width=48 />"
#endif
ArticleRenderer.feedIconImgTagCache[feed] = imgTag
return imgTag
}
@ -205,8 +209,12 @@ private extension ArticleRenderer {
return nil
}
func base64String(forImage image: NSImage) -> String? {
func base64String(forImage image: RSImage) -> String? {
#if os(macOS)
return image.tiffRepresentation?.base64EncodedString()
#else
return image.pngData()?.base64EncodedString()
#endif
}
func singleArticleSpecifiedAuthor() -> Author? {
@ -308,6 +316,8 @@ private extension ArticleRenderer {
return dateFormatter.string(from: date)
}
#if os(macOS)
func renderHTML(withBody body: String) -> String {
var s = "<!DOCTYPE html><html><head>\n\n"
@ -349,6 +359,25 @@ private extension ArticleRenderer {
return s
}
#else
func renderHTML(withBody body: String) -> String {
var s = "<!DOCTYPE html><html><head>\n"
s += "<meta name=\"viewport\" content=\"width=device-width\">\n"
s += title.htmlBySurroundingWithTag("title")
s += styleString().htmlBySurroundingWithTag("style")
s += "\n\n</head><body>\n\n"
s += body
s += "\n\n</body></html>"
return s
}
#endif
}
// MARK: - Article extension

View File

@ -66,7 +66,11 @@ final class ArticleStylesManager {
updateStyleNames()
updateCurrentStyle()
#if os(macOS)
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: NSApplication.didBecomeActiveNotification, object: nil)
#else
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
#endif
}
// MARK: Notifications

View File

@ -6,28 +6,37 @@
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import AppKit
import Foundation
import Articles
import Account
import RSCore
protocol SmallIconProvider {
var smallIcon: NSImage? { get }
var smallIcon: RSImage? { get }
}
extension Feed: SmallIconProvider {
var smallIcon: NSImage? {
var smallIcon: RSImage? {
if let image = appDelegate.faviconDownloader.favicon(for: self) {
return image
}
#if os(macOS)
return AppImages.genericFeedImage
#else
return AppAssets.feedImage
#endif
}
}
extension Folder: SmallIconProvider {
var smallIcon: NSImage? {
return NSImage(named: NSImage.folderName)
var smallIcon: RSImage? {
#if os(macOS)
return RSImage(named: NSImage.folderName)
#else
return AppAssets.masterFolderImage
#endif
}
}

View File

@ -6,7 +6,7 @@
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import AppKit
import Foundation
import RSCore
import RSWeb

View File

@ -6,7 +6,7 @@
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import AppKit
import Foundation
import Articles
import Account
import RSCore

View File

@ -6,7 +6,9 @@
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
#if os(macOS)
import AppKit
import Articles
import Account
import RSCore
@ -15,14 +17,36 @@ protocol PseudoFeed: class, DisplayNameProvider, UnreadCountProvider, SmallIconP
}
private var smartFeedIcon: NSImage = {
private var smartFeedIcon: RSImage = {
return NSImage(named: NSImage.smartBadgeTemplateName)!
return RSImage(named: NSImage.smartBadgeTemplateName)!
}()
extension PseudoFeed {
var smallIcon: NSImage? {
var smallIcon: RSImage? {
return smartFeedIcon
}
}
#else
import Foundation
import Articles
import Account
import RSCore
protocol PseudoFeed: class, DisplayNameProvider, UnreadCountProvider, SmallIconProvider {
}
private var smartFeedIcon: UIImage = {
return AppAssets.cogImage
}()
extension PseudoFeed {
var smallIcon: UIImage? {
return smartFeedIcon
}
}
#endif

View File

@ -29,9 +29,11 @@ final class SmartFeed: PseudoFeed {
}
}
#if os(macOS)
var pasteboardWriter: NSPasteboardWriting {
return SmartFeedPasteboardWriter(smartFeed: self)
}
#endif
private let delegate: SmartFeedDelegate
private var unreadCounts = [Account: Int]()

View File

@ -6,7 +6,11 @@
// Copyright © 2017 Ranchero Software. All rights reserved.
//
#if os(macOS)
import AppKit
#else
import Foundation
#endif
import Account
import Articles
@ -24,9 +28,11 @@ final class UnreadFeed: PseudoFeed {
}
}
#if os(macOS)
var pasteboardWriter: NSPasteboardWriting {
return SmartFeedPasteboardWriter(smartFeed: self)
}
#endif
init() {

View File

@ -0,0 +1,190 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Add Feed-->
<scene sceneID="2Tc-JN-edX">
<objects>
<tableViewController storyboardIdentifier="AddFeedViewController" id="7aE-6a-iP7" customClass="AddFeedViewController" customModule="Solstone" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="D0S-TM-mtm">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<sections>
<tableViewSection headerTitle=" " id="3tl-Mb-Eno">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="44" id="lyJ-rf-8GA">
<rect key="frame" x="0.0" y="28" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="lyJ-rf-8GA" id="eNS-Rp-w0A">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="URL" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="eRp-AP-WFq">
<rect key="frame" x="20" y="4" width="374" height="35.5"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
<connections>
<outlet property="delegate" destination="7aE-6a-iP7" id="zCK-Sy-4Zr"/>
</connections>
</textField>
</subviews>
<constraints>
<constraint firstItem="eRp-AP-WFq" firstAttribute="top" secondItem="eNS-Rp-w0A" secondAttribute="top" constant="4" id="80p-a2-3NC"/>
<constraint firstItem="eRp-AP-WFq" firstAttribute="leading" secondItem="eNS-Rp-w0A" secondAttribute="leading" constant="20" symbolic="YES" id="bHJ-7l-Pl3"/>
<constraint firstAttribute="bottom" secondItem="eRp-AP-WFq" secondAttribute="bottom" constant="4" id="fs0-iw-zTo"/>
<constraint firstAttribute="trailing" secondItem="eRp-AP-WFq" secondAttribute="trailing" constant="20" symbolic="YES" id="xWD-54-Kdm"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="44" id="Pxz-fv-QhQ">
<rect key="frame" x="0.0" y="72" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Pxz-fv-QhQ" id="8aP-2A-8jc">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Title (Optional)" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="u7n-VL-Ho9">
<rect key="frame" x="20" y="4" width="374" height="35.5"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="u7n-VL-Ho9" secondAttribute="bottom" constant="4" id="CdB-LH-PJT"/>
<constraint firstItem="u7n-VL-Ho9" firstAttribute="leading" secondItem="8aP-2A-8jc" secondAttribute="leading" constant="20" symbolic="YES" id="RML-Iw-gsd"/>
<constraint firstItem="u7n-VL-Ho9" firstAttribute="top" secondItem="8aP-2A-8jc" secondAttribute="top" constant="4" id="dxi-LX-hFa"/>
<constraint firstAttribute="trailing" secondItem="u7n-VL-Ho9" secondAttribute="trailing" constant="20" symbolic="YES" id="kQl-v5-eVa"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle=" " id="qn9-7O-LoA">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="44" id="MGg-y2-M2D">
<rect key="frame" x="0.0" y="144" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MGg-y2-M2D" id="sZh-wI-IW4">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Folder" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="grZ-g6-qfm">
<rect key="frame" x="28" y="15" width="48.5" height="14"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vaV-kY-CaE">
<rect key="frame" x="360" y="15" width="42" height="14"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="vaV-kY-CaE" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="grZ-g6-qfm" secondAttribute="trailing" constant="8" id="1Dk-MH-7Hw"/>
<constraint firstItem="grZ-g6-qfm" firstAttribute="top" secondItem="sZh-wI-IW4" secondAttribute="topMargin" constant="4" id="9dF-Uj-lPA"/>
<constraint firstItem="grZ-g6-qfm" firstAttribute="leading" secondItem="sZh-wI-IW4" secondAttribute="leadingMargin" constant="8" id="NKz-GB-i4E"/>
<constraint firstAttribute="bottomMargin" secondItem="vaV-kY-CaE" secondAttribute="bottom" constant="4" id="Yfx-6j-UqX"/>
<constraint firstItem="vaV-kY-CaE" firstAttribute="trailing" secondItem="sZh-wI-IW4" secondAttribute="trailingMargin" constant="8" id="eCs-ob-UXo"/>
<constraint firstItem="vaV-kY-CaE" firstAttribute="top" secondItem="sZh-wI-IW4" secondAttribute="topMargin" constant="4" id="hUO-Ln-i29"/>
<constraint firstAttribute="bottomMargin" secondItem="grZ-g6-qfm" secondAttribute="bottom" constant="4" id="tu3-aF-XD6"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="250" id="PiN-2i-6Dj">
<rect key="frame" x="0.0" y="188" width="414" height="250"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="PiN-2i-6Dj" id="sZ4-hj-gua">
<rect key="frame" x="0.0" y="0.0" width="414" height="249.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v2n-nX-8jq">
<rect key="frame" x="0.0" y="0.0" width="414" height="249"/>
</pickerView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="v2n-nX-8jq" secondAttribute="trailing" id="IBi-SI-10J"/>
<constraint firstItem="v2n-nX-8jq" firstAttribute="top" secondItem="sZ4-hj-gua" secondAttribute="top" id="kmm-i3-6DB"/>
<constraint firstItem="v2n-nX-8jq" firstAttribute="leading" secondItem="sZ4-hj-gua" secondAttribute="leading" id="ksr-vY-KdS"/>
<constraint firstAttribute="bottom" secondItem="v2n-nX-8jq" secondAttribute="bottom" id="wf0-0Y-GNZ"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="7aE-6a-iP7" id="PAe-eu-KhR"/>
<outlet property="delegate" destination="7aE-6a-iP7" id="zYS-q2-iEf"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Add Feed" id="i1W-2z-PAk">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="vdU-kc-SkI">
<connections>
<action selector="cancel:" destination="7aE-6a-iP7" id="v9C-5Y-7Pf"/>
</connections>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem enabled="NO" title="Add" id="M4A-Uu-rC9">
<connections>
<action selector="add:" destination="7aE-6a-iP7" id="WZ4-RF-9k2"/>
</connections>
</barButtonItem>
<barButtonItem style="plain" id="r7V-oB-aHz">
<view key="customView" contentMode="scaleToFill" id="4in-Eb-Rxp">
<rect key="frame" x="335" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="3ZH-9O-T3i">
<rect key="frame" x="0.0" y="0.0" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="activityIndicatorView" destination="3ZH-9O-T3i" id="Z7G-Qb-tV9"/>
<outlet property="addButton" destination="M4A-Uu-rC9" id="HXl-Sg-Zgw"/>
<outlet property="cancelButton" destination="vdU-kc-SkI" id="AKF-i4-V5Q"/>
<outlet property="folderLabel" destination="vaV-kY-CaE" id="xeO-Ks-LIy"/>
<outlet property="folderPickerView" destination="v2n-nX-8jq" id="qwz-Gg-GdQ"/>
<outlet property="nameTextField" destination="u7n-VL-Ho9" id="YQV-Xq-f9q"/>
<outlet property="urlTextField" destination="eRp-AP-WFq" id="FG3-pH-2Fh"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="TO9-rb-MQ7" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-657.97101449275362" y="-61.607142857142854"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="hMp-bh-i2u">
<objects>
<navigationController storyboardIdentifier="AddFeedNavigationController" id="9he-b0-Wev" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="kcy-Ww-GZR">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="7aE-6a-iP7" kind="relationship" relationship="rootViewController" id="nOa-su-7Qa"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="gRU-xA-gWm" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-1575" y="-61"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,40 @@
//Copyright © 2019 Vincode, Inc. All rights reserved.
import Foundation
import Account
import RSCore
import RSTree
struct AddFeedFolderPickerData {
var containerNames = [String]()
var containers = [Container]()
init() {
let treeControllerDelegate = FolderTreeControllerDelegate()
let rootNode = Node(representedObject: AccountManager.shared.localAccount, parent: nil)
rootNode.canHaveChildNodes = true
let treeController = TreeController(delegate: treeControllerDelegate, rootNode: rootNode)
guard let rootNameProvider = treeController.rootNode.representedObject as? DisplayNameProvider else {
return
}
let rootName = rootNameProvider.nameForDisplay
containerNames.append(rootName)
containers.append(treeController.rootNode.representedObject as! Container)
treeController.rootNode.childNodes.forEach { node in
guard let childContainer = node.representedObject as? Container else {
return
}
let childName = (childContainer as! DisplayNameProvider).nameForDisplay
containerNames.append("\(rootName) / \(childName)")
containers.append(childContainer)
}
}
}

View File

@ -0,0 +1,248 @@
//Copyright © 2019 Vincode, Inc. All rights reserved.
import UIKit
import Account
import RSCore
import RSTree
import RSParser
class AddFeedViewController: UITableViewController {
@IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
@IBOutlet weak var cancelButton: UIBarButtonItem!
@IBOutlet weak var addButton: UIBarButtonItem!
@IBOutlet weak var urlTextField: UITextField!
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var folderPickerView: UIPickerView!
@IBOutlet weak var folderLabel: UILabel!
private var pickerData: AddFeedFolderPickerData!
private var feedFinder: FeedFinder?
private var userEnteredURL: URL?
private var userEnteredFolder: Folder?
private var userEnteredTitle: String?
private var userEnteredAccount: Account?
private var foundFeedURLString: String?
private var bestFeedSpecifier: FeedSpecifier?
private var titleFromFeed: String?
private var userCancelled = false
override func viewDidLoad() {
super.viewDidLoad()
activityIndicatorView.isHidden = true
urlTextField.autocorrectionType = .no
urlTextField.autocapitalizationType = .none
pickerData = AddFeedFolderPickerData()
folderPickerView.dataSource = self
folderPickerView.delegate = self
folderLabel.text = pickerData.containerNames[0]
}
@IBAction func cancel(_ sender: Any) {
userCancelled = true
dismiss(animated: true)
}
@IBAction func add(_ sender: Any) {
let urlString = urlTextField.text ?? ""
let normalizedURLString = (urlString as NSString).rs_normalizedURL()
guard !normalizedURLString.isEmpty, let url = URL(string: normalizedURLString) else {
dismiss(animated: true)
return
}
userEnteredURL = url
userEnteredTitle = nameTextField.text
let container = pickerData.containers[folderPickerView.selectedRow(inComponent: 0)]
if let account = container as? Account {
userEnteredAccount = account
}
if let folder = container as? Folder, let account = folder.account {
userEnteredAccount = account
userEnteredFolder = folder
}
guard let userEnteredAccount = userEnteredAccount else {
assertionFailure()
return
}
if userEnteredAccount.hasFeed(withURL: url.absoluteString) {
showAlreadySubscribedError()
return
}
beginShowingProgress()
feedFinder = FeedFinder(url: url, delegate: self)
}
}
extension AddFeedViewController: UIPickerViewDataSource, UIPickerViewDelegate {
func numberOfComponents(in pickerView: UIPickerView) ->Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return pickerData.containerNames.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return pickerData.containerNames[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
folderLabel.text = pickerData.containerNames[row]
}
}
extension AddFeedViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
updateUI()
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
updateUI()
}
}
extension AddFeedViewController: FeedFinderDelegate {
public func feedFinder(_ feedFinder: FeedFinder, didFindFeeds feedSpecifiers: Set<FeedSpecifier>) {
if userCancelled {
endShowingProgress()
return
}
if let error = feedFinder.initialDownloadError {
if feedFinder.initialDownloadStatusCode == 404 {
endShowingProgress()
showNoFeedsErrorMessage()
} else {
endShowingProgress()
showInitialDownloadError(error)
}
return
}
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers) else {
endShowingProgress()
showNoFeedsErrorMessage()
return
}
self.bestFeedSpecifier = bestFeedSpecifier
self.foundFeedURLString = bestFeedSpecifier.urlString
if let url = URL(string: bestFeedSpecifier.urlString) {
InitialFeedDownloader.download(url) { (parsedFeed) in
self.titleFromFeed = parsedFeed?.title
self.addFeedIfPossible(parsedFeed)
}
} else {
// Shouldn't happen.
endShowingProgress()
showNoFeedsErrorMessage()
}
}
}
private extension AddFeedViewController {
private func updateUI() {
addButton.isEnabled = urlTextField.text?.rs_stringMayBeURL() ?? false
}
private func beginShowingProgress() {
activityIndicatorView.isHidden = false
activityIndicatorView.startAnimating()
addButton.isEnabled = false
}
private func endShowingProgress() {
activityIndicatorView.isHidden = true
activityIndicatorView.stopAnimating()
addButton.isEnabled = true
}
private func showAlreadySubscribedError() {
let title = NSLocalizedString("Already subscribed", comment: "Feed finder")
let message = NSLocalizedString("Cant add this feed because youve already subscribed to it.", comment: "Feed finder")
presentError(title: title, message: message)
}
private func showNoFeedsErrorMessage() {
let title = NSLocalizedString("Feed not found", comment: "Feed finder")
let message = NSLocalizedString("Cant add a feed because no feed was found.", comment: "Feed finder")
presentError(title: title, message: message)
}
private func showInitialDownloadError(_ error: Error) {
let title = NSLocalizedString("Download Error", comment: "Feed finder")
let formatString = NSLocalizedString("Cant add this feed because of a download error: “%@”", comment: "Feed finder")
let message = NSString.localizedStringWithFormat(formatString as NSString, error.localizedDescription)
presentError(title: title, message: message as String)
}
func addFeedIfPossible(_ parsedFeed: ParsedFeed?) {
if userCancelled {
endShowingProgress()
return
}
guard let account = userEnteredAccount else {
assertionFailure("Expected account.")
return
}
guard let feedURLString = foundFeedURLString else {
assertionFailure("Expected feedURLString.")
return
}
if account.hasFeed(withURL: feedURLString) {
endShowingProgress()
showAlreadySubscribedError()
return
}
guard let feed = account.createFeed(with: titleFromFeed, editedName: userEnteredTitle, url: feedURLString) else {
endShowingProgress()
return
}
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed, {})
}
account.addFeed(feed, to: userEnteredFolder)
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
endShowingProgress()
dismiss(animated: true)
}
}

View File

@ -0,0 +1,43 @@
//
// FolderTreeControllerDelegate.swift
// NetNewsWire
//
// Created by Brent Simmons on 8/10/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSTree
import Articles
import Account
final class FolderTreeControllerDelegate: TreeControllerDelegate {
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
return node.isRoot ? childNodesForRootNode(node) : nil
}
}
private extension FolderTreeControllerDelegate {
func childNodesForRootNode(_ node: Node) -> [Node]? {
// Root node is Top Level and children are folders. Folders cant have subfolders.
// This will have to be revised later.
guard let folders = AccountManager.shared.localAccount.folders else {
return nil
}
let folderNodes = folders.map { createNode($0, parent: node) }
return folderNodes.sortedAlphabetically()
}
func createNode(_ folder: Folder, parent: Node) -> Node {
let node = Node(representedObject: folder, parent: parent)
node.canHaveChildNodes = false
return node
}
}

View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Add Folder-->
<scene sceneID="7xK-jd-hvG">
<objects>
<tableViewController storyboardIdentifier="AddFolderViewController" id="7B9-Xc-bgZ" customClass="AddFolderViewController" customModule="Solstone" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="xSh-ba-nue">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<sections>
<tableViewSection headerTitle=" " id="hoc-fH-XAL">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="44" id="LVo-lj-Dek">
<rect key="frame" x="0.0" y="28" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="LVo-lj-Dek" id="YgS-EN-1vc">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Name" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="naL-2S-Of5">
<rect key="frame" x="20" y="4" width="374" height="35.5"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
<connections>
<outlet property="delegate" destination="7B9-Xc-bgZ" id="FID-Y8-kRX"/>
</connections>
</textField>
</subviews>
<constraints>
<constraint firstItem="naL-2S-Of5" firstAttribute="leading" secondItem="YgS-EN-1vc" secondAttribute="leading" constant="20" symbolic="YES" id="EUe-PJ-1tu"/>
<constraint firstItem="naL-2S-Of5" firstAttribute="top" secondItem="YgS-EN-1vc" secondAttribute="top" constant="4" id="LsB-48-33D"/>
<constraint firstAttribute="bottom" secondItem="naL-2S-Of5" secondAttribute="bottom" constant="4" id="VoR-fE-od3"/>
<constraint firstAttribute="trailing" secondItem="naL-2S-Of5" secondAttribute="trailing" constant="20" symbolic="YES" id="l92-ae-Nlp"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle=" " id="ud6-07-bLH">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="44" id="neK-UK-2RN">
<rect key="frame" x="0.0" y="100" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="neK-UK-2RN" id="8ap-Wk-DKw">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vzh-ID-ROE">
<rect key="frame" x="20" y="4" width="64" height="35.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="S6R-WU-i8i">
<rect key="frame" x="360" y="15" width="42" height="14"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="Vzh-ID-ROE" secondAttribute="bottom" constant="4" id="9Io-uV-8m4"/>
<constraint firstItem="S6R-WU-i8i" firstAttribute="top" secondItem="8ap-Wk-DKw" secondAttribute="topMargin" constant="4" id="Eew-46-PxW"/>
<constraint firstAttribute="bottomMargin" secondItem="S6R-WU-i8i" secondAttribute="bottom" constant="4" id="Ns7-O1-CcO"/>
<constraint firstItem="Vzh-ID-ROE" firstAttribute="top" secondItem="8ap-Wk-DKw" secondAttribute="top" constant="4" id="Qvc-bP-7DT"/>
<constraint firstItem="Vzh-ID-ROE" firstAttribute="leading" secondItem="8ap-Wk-DKw" secondAttribute="leading" constant="20" symbolic="YES" id="nwb-J8-hUe"/>
<constraint firstItem="S6R-WU-i8i" firstAttribute="trailing" secondItem="8ap-Wk-DKw" secondAttribute="trailingMargin" constant="8" id="vW0-4T-XRF"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="250" id="98d-eb-yj6">
<rect key="frame" x="0.0" y="144" width="414" height="250"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="98d-eb-yj6" id="qyV-5R-b7P">
<rect key="frame" x="0.0" y="0.0" width="414" height="249.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9LL-XO-rhm">
<rect key="frame" x="0.0" y="0.0" width="414" height="249"/>
</pickerView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="9LL-XO-rhm" secondAttribute="trailing" id="1WN-No-33w"/>
<constraint firstItem="9LL-XO-rhm" firstAttribute="top" secondItem="qyV-5R-b7P" secondAttribute="top" id="QSr-Rv-2z1"/>
<constraint firstItem="9LL-XO-rhm" firstAttribute="leading" secondItem="qyV-5R-b7P" secondAttribute="leading" id="Vcr-Bs-VJo"/>
<constraint firstAttribute="bottom" secondItem="9LL-XO-rhm" secondAttribute="bottom" id="cx1-Pq-6HO"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="7B9-Xc-bgZ" id="SY5-L5-myg"/>
<outlet property="delegate" destination="7B9-Xc-bgZ" id="ihx-eH-96a"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Add Folder" id="LJm-Yn-7Ov">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="I7O-NZ-gcs">
<connections>
<action selector="cancel:" destination="7B9-Xc-bgZ" id="Rr9-PQ-4BO"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" enabled="NO" title="Add" id="XE6-0i-iRB">
<connections>
<action selector="add:" destination="7B9-Xc-bgZ" id="NDd-LD-usN"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="accountLabel" destination="S6R-WU-i8i" id="RlM-iu-LUi"/>
<outlet property="accountPickerView" destination="9LL-XO-rhm" id="k4N-7l-zHw"/>
<outlet property="addButton" destination="XE6-0i-iRB" id="Gd8-pK-3V7"/>
<outlet property="nameTextField" destination="naL-2S-Of5" id="fe2-h1-l72"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dKH-tT-2D6" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="560.86956521739137" y="177.45535714285714"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="393-Mj-9Bd">
<objects>
<navigationController storyboardIdentifier="AddFolderNavigationController" id="kdm-Ic-tap" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="9I9-mB-9xh">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="7B9-Xc-bgZ" kind="relationship" relationship="rootViewController" id="VjZ-Oe-NQO"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="MIw-jT-NLz" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-357" y="178"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,84 @@
//Copyright © 2019 Vincode, Inc. All rights reserved.
import UIKit
import Account
import RSCore
class AddFolderViewController: UITableViewController {
@IBOutlet weak var addButton: UIBarButtonItem!
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var accountLabel: UILabel!
@IBOutlet weak var accountPickerView: UIPickerView!
private var accounts: [Account]!
override func viewDidLoad() {
super.viewDidLoad()
accounts = AccountManager.shared.sortedAccounts
accountLabel.text = (accounts[0] as DisplayNameProvider).nameForDisplay
accountPickerView.dataSource = self
accountPickerView.delegate = self
}
@IBAction func cancel(_ sender: Any) {
dismiss(animated: true)
}
@IBAction func add(_ sender: Any) {
let account = accounts[accountPickerView.selectedRow(inComponent: 0)]
if let folderName = nameTextField.text {
account.ensureFolder(with: folderName)
}
dismiss(animated: true)
}
}
extension AddFolderViewController: UIPickerViewDataSource, UIPickerViewDelegate {
func numberOfComponents(in pickerView: UIPickerView) ->Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return accounts.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return (accounts[row] as DisplayNameProvider).nameForDisplay
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
accountLabel.text = (accounts[row] as DisplayNameProvider).nameForDisplay
}
}
extension AddFolderViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
updateUI()
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
updateUI()
}
}
private extension AddFolderViewController {
private func updateUI() {
addButton.isEnabled = !(nameTextField.text?.isEmpty ?? false)
}
}

71
iOS/AppAssets.swift Normal file
View File

@ -0,0 +1,71 @@
//
// AppAssets.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
struct AppAssets {
static var circleClosedImage: RSImage = {
return RSImage(named: "circleClosedImage")!
}()
static var circleOpenImage: RSImage = {
return RSImage(named: "circleOpenImage")!
}()
static var cogImage: RSImage = {
return RSImage(named: "cogImage")!
}()
static var feedImage: RSImage = {
return RSImage(named: "rssImage")!
}()
static var folderImage: RSImage = {
return RSImage(named: "folderImage")!
}()
static var masterFolderColor: UIColor = {
return UIColor(named: "masterFolderColor")!
}()
static var masterFolderImage: UIImage = {
let image = UIImage(named: "folderImage")!
return image.maskWithColor(color: AppAssets.masterFolderColor)!
}()
static var starColor: UIColor = {
return UIColor(named: "starColor")!
}()
static var starClosedImage: RSImage = {
return RSImage(named: "starClosedImage")!
}()
static var starOpenImage: RSImage = {
return RSImage(named: "starOpenImage")!
}()
static var timelineStarImage: RSImage = {
let image = RSImage(named: "starClosedImage")!
return image.maskWithColor(color: AppAssets.starColor)!
}()
static var timelineTextPrimaryColor: UIColor = {
return UIColor(named: "timelineTextPrimaryColor")!
}()
static var timelineTextSecondaryColor: UIColor = {
return UIColor(named: "timelineTextSecondaryColor")!
}()
static var timelineUnreadCircleColor: UIColor = {
return UIColor(named: "timelineUnreadCircleColor")!
}()
}

136
iOS/AppDefaults.swift Normal file
View File

@ -0,0 +1,136 @@
//
// AppDefaults.swift
// NetNewsWire
//
// Created by Brent Simmons on 9/22/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import UIKit
enum RefreshInterval: Int {
case manually = 1
case every10Minutes = 2
case every30Minutes = 3
case everyHour = 4
case every2Hours = 5
case every4Hours = 6
case every8Hours = 7
func inSeconds() -> TimeInterval {
switch self {
case .manually:
return 0
case .every10Minutes:
return 10 * 60
case .every30Minutes:
return 30 * 60
case .everyHour:
return 60 * 60
case .every2Hours:
return 2 * 60 * 60
case .every4Hours:
return 4 * 60 * 60
case .every8Hours:
return 8 * 60 * 60
}
}
}
struct AppDefaults {
struct Key {
static let firstRunDate = "firstRunDate"
static let timelineSortDirection = "timelineSortDirection"
static let refreshInterval = "refreshInterval"
}
static let isFirstRun: Bool = {
if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date {
return false
}
firstRunDate = Date()
return true
}()
static var refreshInterval: RefreshInterval {
get {
let rawValue = UserDefaults.standard.integer(forKey: Key.refreshInterval)
return RefreshInterval(rawValue: rawValue) ?? RefreshInterval.everyHour
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: Key.refreshInterval)
}
}
static var timelineSortDirection: ComparisonResult {
get {
return sortDirection(for: Key.timelineSortDirection)
}
set {
setSortDirection(for: Key.timelineSortDirection, newValue)
}
}
static func registerDefaults() {
let defaults: [String : Any] = [Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, Key.refreshInterval: RefreshInterval.everyHour.rawValue]
UserDefaults.standard.register(defaults: defaults)
}
}
private extension AppDefaults {
static var firstRunDate: Date? {
get {
return date(for: Key.firstRunDate)
}
set {
setDate(for: Key.firstRunDate, newValue)
}
}
static func bool(for key: String) -> Bool {
return UserDefaults.standard.bool(forKey: key)
}
static func setBool(for key: String, _ flag: Bool) {
UserDefaults.standard.set(flag, forKey: key)
}
static func int(for key: String) -> Int {
return UserDefaults.standard.integer(forKey: key)
}
static func setInt(for key: String, _ x: Int) {
UserDefaults.standard.set(x, forKey: key)
}
static func date(for key: String) -> Date? {
return UserDefaults.standard.object(forKey: key) as? Date
}
static func setDate(for key: String, _ date: Date?) {
UserDefaults.standard.set(date, forKey: key)
}
static func sortDirection(for key:String) -> ComparisonResult {
let rawInt = int(for: key)
if rawInt == ComparisonResult.orderedAscending.rawValue {
return .orderedAscending
}
return .orderedDescending
}
static func setSortDirection(for key: String, _ value: ComparisonResult) {
if value == .orderedAscending {
setInt(for: key, ComparisonResult.orderedAscending.rawValue)
}
else {
setInt(for: key, ComparisonResult.orderedDescending.rawValue)
}
}
}

View File

@ -1,28 +1,106 @@
//
// AppDelegate.swift
// NetNewsWire-iOS
// NetNewsWire
//
// Created by Brent Simmons on 2/5/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
import Account
var appDelegate: AppDelegate!
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate, UnreadCountProvider {
var window: UIWindow?
var faviconDownloader: FaviconDownloader!
var imageDownloader: ImageDownloader!
var authorAvatarDownloader: AuthorAvatarDownloader!
var feedIconDownloader: FeedIconDownloader!
private let log = Log()
var unreadCount = 0 {
didSet {
if unreadCount != oldValue {
postUnreadCountDidChangeNotification()
}
}
}
override init() {
super.init()
appDelegate = self
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
// Initialize the AccountManager as soon as possible or it will cause problems
// if the application is restoring preserved state.
_ = AccountManager.shared
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Set up the split view
let splitViewController = window!.rootViewController as! UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
splitViewController.delegate = self
AppDefaults.registerDefaults()
let isFirstRun = AppDefaults.isFirstRun
if isFirstRun {
logDebugMessage("Is first run.")
}
let localAccount = AccountManager.shared.localAccount
DefaultFeedsImporter.importIfNeeded(isFirstRun, account: localAccount)
let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil)
faviconDownloader = FaviconDownloader(folder: faviconsFolderURL.absoluteString)
let imagesFolderURL = tempDir.appendingPathComponent("Images")
try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil)
imageDownloader = ImageDownloader(folder: imagesFolderURL.absoluteString)
authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader)
feedIconDownloader = FeedIconDownloader(imageDownloader: imageDownloader)
DispatchQueue.main.async {
self.unreadCount = AccountManager.shared.unreadCount
}
return true
}
// func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
//
// let versionNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
// coder.encode(versionNumber, forKey: "VersionNumber")
//
// return true
//
// }
//
// func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
// if let storedVersionNumber = coder.decodeObject(forKey: "VersionNumber") as? String {
// let versionNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
// if versionNumber == storedVersionNumber {
// return true
// }
// }
// return false
// }
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
@ -50,12 +128,39 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
if topAsDetailController.detailItem == nil {
if topAsDetailController.article == nil {
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
return true
}
return false
}
// MARK: Notifications
@objc func unreadCountDidChange(_ note: Notification) {
if note.object is AccountManager {
unreadCount = AccountManager.shared.unreadCount
}
}
// MARK: - API
func logMessage(_ message: String, type: LogItem.ItemType) {
#if DEBUG
if type == .debug {
print("logMessage: \(message) - \(type)")
}
#endif
let logItem = LogItem(type: type, message: message)
log.add(logItem)
}
func logDebugMessage(_ message: String) {
logMessage(message, type: .debug)
}
}

View File

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13163" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="H1p-Uh-vWS">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="H1p-Uh-vWS">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13143"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -9,8 +13,9 @@
<!--Master-->
<scene sceneID="pY4-Hu-kfo">
<objects>
<navigationController title="Master" id="RMx-3f-FxP" sceneMemberID="viewController">
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="Pmd-2v-anx">
<navigationController storyboardIdentifier="MasterPrimaryNavigationViewController" title="Master" useStoryboardIdentifierAsRestorationIdentifier="YES" id="RMx-3f-FxP" sceneMemberID="viewController">
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" largeTitles="YES" id="Pmd-2v-anx">
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
@ -24,44 +29,128 @@
<!--Detail-->
<scene sceneID="yUG-lL-AsK">
<objects>
<viewController title="Detail" id="JEX-9P-axG" customClass="DetailViewController" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="DetailViewController" title="Detail" useStoryboardIdentifierAsRestorationIdentifier="YES" id="JEX-9P-axG" customClass="DetailViewController" customModule="Solstone" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="svH-Pt-448">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" text="Detail view content goes here" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="0XM-y9-sOw">
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<wkWebView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="t8d-md-Yhc">
<rect key="frame" x="0.0" y="88" width="414" height="774"/>
<color key="backgroundColor" red="0.36078431370000003" green="0.38823529410000002" blue="0.4039215686" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
</wkWebView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="0XM-y9-sOw" firstAttribute="leading" secondItem="svH-Pt-448" secondAttribute="leading" constant="20" symbolic="YES" id="Tsc-yG-G1q"/>
<constraint firstItem="0XM-y9-sOw" firstAttribute="centerY" secondItem="svH-Pt-448" secondAttribute="centerY" id="jWN-iV-94e"/>
<constraint firstAttribute="trailing" secondItem="0XM-y9-sOw" secondAttribute="trailing" constant="20" symbolic="YES" id="tHV-ZD-HQj"/>
<constraint firstItem="t8d-md-Yhc" firstAttribute="top" secondItem="VUw-jc-0yf" secondAttribute="top" id="0aK-ew-1HG"/>
<constraint firstItem="VUw-jc-0yf" firstAttribute="trailing" secondItem="t8d-md-Yhc" secondAttribute="trailing" id="31v-r8-kzh"/>
<constraint firstItem="VUw-jc-0yf" firstAttribute="bottom" secondItem="t8d-md-Yhc" secondAttribute="bottom" id="kK6-eC-XwD"/>
<constraint firstItem="t8d-md-Yhc" firstAttribute="leading" secondItem="VUw-jc-0yf" secondAttribute="leading" id="nDG-aV-vqc"/>
</constraints>
<viewLayoutGuide key="safeArea" id="VUw-jc-0yf"/>
</view>
<toolbarItems/>
<navigationItem key="navigationItem" title="Detail" id="mOI-FS-AaM"/>
<navigationItem key="navigationItem" id="mOI-FS-AaM">
<rightBarButtonItems>
<barButtonItem systemItem="action" id="9Ut-5B-JKP">
<connections>
<action selector="showActivityDialog:" destination="JEX-9P-axG" id="t7U-uT-fs5"/>
</connections>
</barButtonItem>
<barButtonItem image="browserImage" id="DMh-3X-ebd">
<connections>
<action selector="openBrowser:" destination="JEX-9P-axG" id="R0r-fI-NI7"/>
</connections>
</barButtonItem>
<barButtonItem image="starOpenImage" id="wU4-eH-wC9">
<connections>
<action selector="toggleStar:" destination="JEX-9P-axG" id="4Mp-Ir-N5v"/>
</connections>
</barButtonItem>
<barButtonItem image="circleOpenImage" id="hy0-LS-MzE">
<connections>
<action selector="toggleRead:" destination="JEX-9P-axG" id="A8V-r1-o9u"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="detailDescriptionLabel" destination="0XM-y9-sOw" id="deQ-Na-JPF"/>
<outlet property="actionBarButtonItem" destination="9Ut-5B-JKP" id="9bO-kz-cTz"/>
<outlet property="browserBarButtonItem" destination="DMh-3X-ebd" id="PkT-Tn-8kG"/>
<outlet property="readBarButtonItem" destination="hy0-LS-MzE" id="BzM-x9-tuj"/>
<outlet property="starBarButtonItem" destination="wU4-eH-wC9" id="Z8Q-Lt-dKk"/>
<outlet property="webView" destination="t8d-md-Yhc" id="Iqg-bg-wds"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="FJe-Yq-33r" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="709" y="129"/>
<point key="canvasLocation" x="709" y="741"/>
</scene>
<!--Timeline-->
<scene sceneID="fag-XH-avP">
<objects>
<tableViewController storyboardIdentifier="MasterTimelineViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Kyk-vK-QRX" customClass="MasterTimelineViewController" customModule="Solstone" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="mtv-Ik-FoJ">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" rowHeight="208" id="T5d-L4-OKG" customClass="MasterTimelineTableViewCell" customModule="Solstone" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="414" height="208"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="T5d-L4-OKG" id="QKC-jN-XDy">
<rect key="frame" x="0.0" y="0.0" width="414" height="207.5"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
<connections>
<segue destination="vC3-pB-5Vb" kind="showDetail" identifier="showDetail" id="RT3-gH-cyN"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="Kyk-vK-QRX" id="qMR-hi-7SO"/>
<outlet property="delegate" destination="Kyk-vK-QRX" id="rH3-5N-a6z"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Timeline" id="wcC-1L-ug4">
<barButtonItem key="rightBarButtonItem" image="threeCirclesImage" id="qD2-64-fyX">
<connections>
<action selector="markAllAsRead:" destination="Kyk-vK-QRX" id="Irh-YU-8GS"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nzm-Gf-Xce" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="709" y="45"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="uQv-y4-hI4">
<objects>
<navigationController storyboardIdentifier="MasterTimelineNavigationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="vHh-eJ-Wug" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="Df0-za-Olh">
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="Kyk-vK-QRX" kind="relationship" relationship="rootViewController" id="g8t-I4-iBe"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Kif-ju-wKR" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-39" y="45"/>
</scene>
<!--Split View Controller-->
<scene sceneID="Nki-YV-4Qg">
<objects>
<splitViewController id="H1p-Uh-vWS" sceneMemberID="viewController">
<splitViewController storyboardIdentifier="SplitViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="H1p-Uh-vWS" sceneMemberID="viewController">
<toolbarItems/>
<connections>
<segue destination="RMx-3f-FxP" kind="relationship" relationship="masterViewController" id="BlO-5A-QYV"/>
<segue destination="vC3-pB-5Vb" kind="relationship" relationship="detailViewController" id="Tll-UG-LXB"/>
<segue destination="vC3-pB-5Vb" kind="relationship" relationship="detailViewController" id="FRG-EO-hQw"/>
</connections>
</splitViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cZU-Oi-B1e" sceneMemberID="firstResponder"/>
@ -71,32 +160,19 @@
<!--Master-->
<scene sceneID="smW-Zh-WAh">
<objects>
<tableViewController title="Master" clearsSelectionOnViewWillAppear="NO" id="7bK-jq-Zjz" customClass="MasterViewController" customModuleProvider="target" sceneMemberID="viewController">
<tableViewController storyboardIdentifier="MasterPrimaryViewController" title="Master" useStoryboardIdentifierAsRestorationIdentifier="YES" clearsSelectionOnViewWillAppear="NO" id="7bK-jq-Zjz" customClass="MasterPrimaryViewController" customModule="Solstone" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="r7i-6Z-zg0">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes>
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="Cell" textLabel="Arm-wq-HPj" style="IBUITableViewCellStyleDefault" id="WCw-Qf-5nD">
<rect key="frame" x="0.0" y="86" width="375" height="44"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="zNG-5C-pQm" customClass="MasterTableViewCell" customModule="Solstone" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="WCw-Qf-5nD" id="37f-cq-3Eg">
<rect key="frame" x="0.0" y="0.0" width="375" height="43"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="zNG-5C-pQm" id="5gB-Jr-qIo">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Arm-wq-HPj">
<rect key="frame" x="15" y="0.0" width="345" height="43"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<color key="highlightedColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<segue destination="vC3-pB-5Vb" kind="showDetail" identifier="showDetail" id="6S0-TO-JiA"/>
</connections>
</tableViewCell>
</prototypes>
<sections/>
@ -105,17 +181,81 @@
<outlet property="delegate" destination="7bK-jq-Zjz" id="RA6-mI-bju"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Master" id="Zdf-7t-Un8"/>
<navigationItem key="navigationItem" title="Home" id="Zdf-7t-Un8">
<barButtonItem key="leftBarButtonItem" title="Tools" image="toolsImage" id="2lM-3y-hKs">
<connections>
<action selector="showOPMLImportExport:" destination="7bK-jq-Zjz" id="qIa-yP-E6a"/>
</connections>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem image="addFolderImage" id="KGY-I9-bqM">
<connections>
<action selector="addFolder:" destination="7bK-jq-Zjz" id="EvU-df-PgO"/>
</connections>
</barButtonItem>
<barButtonItem systemItem="add" id="yo6-w4-SfI">
<connections>
<action selector="addFeed:" destination="7bK-jq-Zjz" id="05K-Hg-zNJ"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="709" y="-630"/>
</scene>
<!--Secondary-->
<scene sceneID="Nfg-Jx-7ig">
<objects>
<tableViewController storyboardIdentifier="MasterSecondaryViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aKa-71-ZA9" customClass="MasterSecondaryViewController" customModule="Solstone" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="6Kx-w7-mE7">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="27q-ZF-kC8" customClass="MasterTableViewCell" customModule="Solstone" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="27q-ZF-kC8" id="lUQ-Im-VJe">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="aKa-71-ZA9" id="hzg-mr-du3"/>
<outlet property="delegate" destination="aKa-71-ZA9" id="kS8-Gn-bnZ"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Secondary" id="3Ja-fK-wIr"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ILK-0w-PGv" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2159" y="-630"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="4zT-Jk-55W">
<objects>
<navigationController storyboardIdentifier="MasterSecondaryNavigationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="5et-QD-nMk" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="kf3-JC-lZU">
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="aKa-71-ZA9" kind="relationship" relationship="rootViewController" id="KU7-08-42q"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="qBw-2N-kNV" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1422" y="-629"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="r7l-gg-dq7">
<objects>
<navigationController id="vC3-pB-5Vb" sceneMemberID="viewController">
<navigationController storyboardIdentifier="DetailNavigationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="vC3-pB-5Vb" sceneMemberID="viewController">
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="DjV-YW-jjY">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
@ -124,10 +264,18 @@
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="SLD-UC-DBI" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-45" y="129"/>
<point key="canvasLocation" x="-39" y="741"/>
</scene>
</scenes>
<resources>
<image name="addFolderImage" width="20" height="16"/>
<image name="browserImage" width="20" height="20"/>
<image name="circleOpenImage" width="20" height="20"/>
<image name="starOpenImage" width="18" height="18"/>
<image name="threeCirclesImage" width="20" height="20"/>
<image name="toolsImage" width="20" height="18"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="6S0-TO-JiA"/>
<segue reference="FRG-EO-hQw"/>
</inferredMetricsTieBreakers>
</document>

View File

@ -0,0 +1,187 @@
//
// DetailViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import WebKit
import Account
import Articles
class DetailViewController: UIViewController {
@IBOutlet weak var readBarButtonItem: UIBarButtonItem!
@IBOutlet weak var starBarButtonItem: UIBarButtonItem!
@IBOutlet weak var actionBarButtonItem: UIBarButtonItem!
@IBOutlet weak var browserBarButtonItem: UIBarButtonItem!
@IBOutlet weak var webView: WKWebView!
var article: Article? {
didSet {
reloadUI()
reloadHTML()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationItem.largeTitleDisplayMode = .never
webView.navigationDelegate = self
reloadUI()
reloadHTML()
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
}
func reloadUI() {
guard let article = article else {
readBarButtonItem.isEnabled = false
starBarButtonItem.isEnabled = false
browserBarButtonItem.isEnabled = false
actionBarButtonItem.isEnabled = false
return
}
readBarButtonItem.isEnabled = true
starBarButtonItem.isEnabled = true
browserBarButtonItem.isEnabled = true
actionBarButtonItem.isEnabled = true
let readImage = article.status.read ? AppAssets.circleOpenImage : AppAssets.circleClosedImage
readBarButtonItem.image = readImage
let starImage = article.status.starred ? AppAssets.starClosedImage : AppAssets.starOpenImage
starBarButtonItem.image = starImage
}
func reloadHTML() {
guard let article = article, let webView = webView else {
return
}
let style = ArticleStylesManager.shared.currentStyle
let html = ArticleRenderer.articleHTML(article: article, style: style)
webView.loadHTMLString(html, baseURL: article.baseURL)
}
@objc func statusesDidChange(_ note: Notification) {
guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
return
}
if articles.count == 1 && articles.first?.articleID == article?.articleID {
reloadUI()
}
}
@IBAction func toggleRead(_ sender: Any) {
if let article = article {
markArticles(Set([article]), statusKey: .read, flag: !article.status.read)
}
}
@IBAction func toggleStar(_ sender: Any) {
if let article = article {
markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred)
}
}
@IBAction func openBrowser(_ sender: Any) {
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
return
}
UIApplication.shared.open(url, options: [:])
}
@IBAction func showActivityDialog(_ sender: Any) {
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
return
}
let itemSource = ArticleActivityItemSource(url: url, subject: article?.title)
let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil)
present(activityViewController, animated: true)
}
}
class ArticleActivityItemSource: NSObject, UIActivityItemSource {
private let url: URL
private let subject: String?
init(url: URL, subject: String?) {
self.url = url
self.subject = subject
}
func activityViewControllerPlaceholderItem(_ : UIActivityViewController) -> Any {
return url
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return url
}
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
return subject ?? ""
}
}
extension DetailViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if components?.scheme == "http" || components?.scheme == "https" {
UIApplication.shared.open(url)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
} else {
decisionHandler(.allow)
}
}
}
private extension Article {
var baseURL: URL? {
var s = url
if s == nil {
s = feed?.homePageURL
}
if s == nil {
s = feed?.url
}
guard let urlString = s else {
return nil
}
var urlComponents = URLComponents(string: urlString)
if urlComponents == nil {
return nil
}
// Cant use url-with-fragment as base URL. The webview wont load. See scripting.com/rss.xml for example.
urlComponents!.fragment = nil
guard let url = urlComponents!.url, url.scheme == "http" || url.scheme == "https" else {
return nil
}
return url
}
}

View File

@ -1,44 +0,0 @@
//
// DetailViewController.swift
// NetNewsWire-iOS
//
// Created by Brent Simmons on 2/5/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import UIKit
class DetailViewController: UIViewController {
@IBOutlet weak var detailDescriptionLabel: UILabel!
func configureView() {
// Update the user interface for the detail item.
if let detail = detailItem {
if let label = detailDescriptionLabel {
label.text = detail.description
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
configureView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
var detailItem: NSDate? {
didSet {
// Update the view.
configureView()
}
}
}

View File

@ -0,0 +1,25 @@
//
// String+.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.width)
}
}

View File

@ -0,0 +1,32 @@
//Copyright © 2019 Vincode, Inc. All rights reserved.
import UIKit
extension UIImage {
func maskWithColor(color: UIColor) -> UIImage? {
let maskImage = cgImage!
let width = size.width
let height = size.height
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let context = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)!
context.clip(to: bounds, mask: maskImage)
context.setFillColor(color.cgColor)
context.fill(bounds)
if let cgImage = context.makeImage() {
let coloredImage = UIImage(cgImage: cgImage)
return coloredImage
} else {
return nil
}
}
}

View File

@ -0,0 +1,29 @@
//
// UIStoryboard+.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
extension UIStoryboard {
static var main: UIStoryboard {
return UIStoryboard(name: "Main", bundle: nil)
}
func instantiateController<T>(ofType type: T.Type = T.self) -> T where T: UIViewController {
let storyboardId = String(describing: type)
guard let viewController = instantiateViewController(withIdentifier: storyboardId) as? T else {
print("Unable to load view with Scene Identifier: \(storyboardId)")
fatalError()
}
return viewController
}
}

View File

@ -0,0 +1,128 @@
//
// MasterTableViewCell.swift
// NetNewsWire
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import RSCore
import Account
import RSTree
class MasterTableViewCell : UITableViewCell {
override var accessibilityLabel: String? {
set {}
get {
if unreadCount > 0 {
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessiblity")
return "\(name) \(unreadCount) \(unreadLabel)"
} else {
return name
}
}
}
var faviconImage: UIImage? {
didSet {
if let image = faviconImage {
faviconImageView.image = shouldShowImage ? image : nil
}
else {
faviconImageView.image = nil
}
}
}
var shouldShowImage = false {
didSet {
if shouldShowImage != oldValue {
setNeedsLayout()
}
faviconImageView.image = shouldShowImage ? faviconImage : nil
}
}
var unreadCount: Int {
get {
return unreadCountView.unreadCount
}
set {
if unreadCountView.unreadCount != newValue {
unreadCountView.unreadCount = newValue
unreadCountView.isHidden = (newValue < 1)
setNeedsLayout()
}
}
}
var name: String {
get {
return titleView.text ?? ""
}
set {
if titleView.text != newValue {
titleView.text = newValue
setNeedsDisplay()
setNeedsLayout()
}
}
}
private let titleView: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.lineBreakMode = .byTruncatingTail
label.allowsDefaultTighteningForTruncation = false
return label
}()
private let faviconImageView: UIImageView = {
return UIImageView(image: AppAssets.feedImage)
}()
private let unreadCountView = MasterUnreadCountView(frame: CGRect.zero)
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func layoutSubviews() {
super.layoutSubviews()
let layout = MasterTableViewCellLayout(cellSize: bounds.size, shouldShowImage: shouldShowImage, label: titleView, unreadCountView: unreadCountView)
layoutWith(layout)
}
}
private extension MasterTableViewCell {
func commonInit() {
addSubviewAtInit(unreadCountView)
addSubviewAtInit(faviconImageView)
addSubviewAtInit(titleView)
}
func addSubviewAtInit(_ view: UIView) {
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
}
func layoutWith(_ layout: MasterTableViewCellLayout) {
faviconImageView.rs_setFrameIfNotEqual(layout.faviconRect)
titleView.rs_setFrameIfNotEqual(layout.titleRect)
unreadCountView.rs_setFrameIfNotEqual(layout.unreadCountRect)
}
}
extension UIView {
func rs_setFrameIfNotEqual(_ rect: CGRect) {
if !self.frame.equalTo(rect) {
self.frame = rect
}
}
}

View File

@ -0,0 +1,74 @@
//
// MasterTableViewCellLayout.swift
// NetNewsWire
//
// Created by Brent Simmons on 11/24/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
struct MasterTableViewCellLayout {
private static let imageSize = CGSize(width: 16, height: 16)
private static let imageMarginLeft = CGFloat(integerLiteral: 8)
private static let imageMarginRight = CGFloat(integerLiteral: 8)
private static let unreadCountMarginLeft = CGFloat(integerLiteral: 8)
private static let unreadCountMarginRight = CGFloat(integerLiteral: 8)
let faviconRect: CGRect
let titleRect: CGRect
let unreadCountRect: CGRect
init(cellSize: CGSize, shouldShowImage: Bool, label: UILabel, unreadCountView: MasterUnreadCountView) {
let bounds = CGRect(x: 0.0, y: 0.0, width: floor(cellSize.width), height: floor(cellSize.height))
var rFavicon = CGRect.zero
if shouldShowImage {
rFavicon = CGRect(x: MasterTableViewCellLayout.imageMarginLeft, y: 0.0, width: MasterTableViewCellLayout.imageSize.width, height: MasterTableViewCellLayout.imageSize.height)
rFavicon = MasterTableViewCellLayout.centerVertically(rFavicon, bounds)
}
self.faviconRect = rFavicon
let labelSize = SingleLineUILabelSizer.size(for: label.text ?? "", font: label.font!)
var rLabel = CGRect(x: 0.0, y: 0.0, width: labelSize.width, height: labelSize.height)
if shouldShowImage {
rLabel.origin.x = rFavicon.maxX + MasterTableViewCellLayout.imageMarginRight
}
rLabel = MasterTableViewCellLayout.centerVertically(rLabel, bounds)
let unreadCountSize = unreadCountView.intrinsicContentSize
let unreadCountIsHidden = unreadCountView.unreadCount < 1
var rUnread = CGRect.zero
if !unreadCountIsHidden {
rUnread.size = unreadCountSize
rUnread.origin.x = (bounds.maxX - unreadCountSize.width) - MasterTableViewCellLayout.unreadCountMarginRight
rUnread = MasterTableViewCellLayout.centerVertically(rUnread, bounds)
let labelMaxX = rUnread.minX - MasterTableViewCellLayout.unreadCountMarginLeft
if rLabel.maxX > labelMaxX {
rLabel.size.width = labelMaxX - rLabel.minX
}
}
self.unreadCountRect = rUnread
if rLabel.maxX > bounds.maxX {
rLabel.size.width = bounds.maxX - rLabel.maxX
}
self.titleRect = rLabel
}
// Ideally this will be implemented in RSCore (see RSGeometry)
static func centerVertically(_ originalRect: CGRect, _ containerRect: CGRect) -> CGRect {
var result = originalRect
result.origin.y = containerRect.midY - (result.height / 2.0)
result = result.integral
result.size = originalRect.size
return result
}
}

View File

@ -0,0 +1,107 @@
//
// MasterUnreadCountView.swift
// NetNewsWire
//
// Created by Brent Simmons on 11/22/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import UIKit
private let padding = UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)
private let cornerRadius = 8.0
private let bgColor = UIColor.darkGray
private let textColor = UIColor.white
private let textFont = UIFont.systemFont(ofSize: 11.0, weight: UIFont.Weight.semibold)
private var textAttributes: [NSAttributedString.Key: AnyObject] = [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()]
private var textSizeCache = [Int: CGSize]()
class MasterUnreadCountView : UIView {
var unreadCount = 0 {
didSet {
invalidateIntrinsicContentSize()
setNeedsDisplay()
}
}
var unreadCountString: String {
return unreadCount < 1 ? "" : "\(unreadCount)"
}
private var intrinsicContentSizeIsValid = false
private var _intrinsicContentSize = CGSize.zero
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
}
override var intrinsicContentSize: CGSize {
if !intrinsicContentSizeIsValid {
var size = CGSize.zero
if unreadCount > 0 {
size = textSize()
size.width += (padding.left + padding.right)
size.height += (padding.top + padding.bottom)
}
_intrinsicContentSize = size
intrinsicContentSizeIsValid = true
}
return _intrinsicContentSize
}
override func invalidateIntrinsicContentSize() {
intrinsicContentSizeIsValid = false
}
private func textSize() -> CGSize {
if unreadCount < 1 {
return CGSize.zero
}
if let cachedSize = textSizeCache[unreadCount] {
return cachedSize
}
var size = unreadCountString.size(withAttributes: textAttributes)
size.height = ceil(size.height)
size.width = ceil(size.width)
textSizeCache[unreadCount] = size
return size
}
private func textRect() -> CGRect {
let size = textSize()
var r = CGRect.zero
r.size = size
r.origin.x = (bounds.maxX - padding.right) - r.size.width
r.origin.y = padding.top
return r
}
override func draw(_ dirtyRect: CGRect) {
let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius)
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: cornerRadii)
bgColor.setFill()
path.fill()
if unreadCount > 0 {
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
}
}
}

View File

@ -0,0 +1,121 @@
//
// MasterPrimaryViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import RSCore
import RSTree
class MasterPrimaryViewController: MasterViewController {
// MARK: Actions
@IBAction func showOPMLImportExport(_ sender: UIBarButtonItem) {
let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let importOPML = UIAlertAction(title: "Import OPML", style: .default) { [unowned self] alertAction in
let docPicker = UIDocumentPickerViewController(documentTypes: ["public.xml", "org.opml.opml"], in: .import)
docPicker.delegate = self
docPicker.modalPresentationStyle = .formSheet
self.present(docPicker, animated: true)
}
optionMenu.addAction(importOPML)
let exportOPML = UIAlertAction(title: "Export OPML", style: .default) { [unowned self] alertAction in
let filename = "MySubscriptions.opml"
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
let opmlString = OPMLExporter.OPMLString(with: AccountManager.shared.localAccount, 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)
}
optionMenu.addAction(exportOPML)
optionMenu.addAction(UIAlertAction(title: "Cancel", style: .cancel))
if let popoverController = optionMenu.popoverPresentationController {
popoverController.barButtonItem = sender
}
self.present(optionMenu, animated: true)
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return treeController.rootNode.numberOfChildNodes
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return treeController.rootNode.childAtIndex(section)?.numberOfChildNodes ?? 0
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let nameProvider = treeController.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
return nil
}
return nameProvider.nameForDisplay
}
// MARK: API
override func delete(indexPath: IndexPath) {
guard let containerNode = treeController.rootNode.childAtIndex(indexPath.section),
let deleteNode = containerNode.childAtIndex(indexPath.row),
let container = containerNode.representedObject as? Container else {
return
}
animatingChanges = true
if let feed = deleteNode.representedObject as? Feed {
container.deleteFeed(feed)
}
if let folder = deleteNode.representedObject as? Folder {
container.deleteFolder(folder)
}
treeController.rebuild()
tableView.deleteRows(at: [indexPath], with: .automatic)
animatingChanges = false
}
override func nodeFor(indexPath: IndexPath) -> Node? {
return treeController.rootNode.childAtIndex(indexPath.section)?.childAtIndex(indexPath.row)
}
}
extension MasterPrimaryViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
for url in urls {
do {
try OPMLImporter.parseAndImport(fileURL: url, account: AccountManager.shared.localAccount)
} catch {
presentError(title: "OPML Import Error", message: error.localizedDescription)
}
}
}
}

View File

@ -0,0 +1,74 @@
//
// MasterSecondaryViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import RSCore
import RSTree
class MasterSecondaryViewController: MasterViewController {
var viewRootNode: Node?
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewRootNode?.numberOfChildNodes ?? 0
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
guard let containerNode = viewRootNode,
let deleteNode = containerNode.childAtIndex(indexPath.row),
let container = containerNode.representedObject as? Container,
let feed = deleteNode.representedObject as? Feed else {
return
}
animatingChanges = true
container.deleteFeed(feed)
treeController.rebuild()
tableView.deleteRows(at: [indexPath], with: .fade)
animatingChanges = false
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
// MARK: API
override func delete(indexPath: IndexPath) {
guard let containerNode = viewRootNode,
let deleteNode = containerNode.childAtIndex(indexPath.row),
let container = containerNode.representedObject as? Container,
let feed = deleteNode.representedObject as? Feed else {
return
}
animatingChanges = true
container.deleteFeed(feed)
treeController.rebuild()
tableView.deleteRows(at: [indexPath], with: .fade)
animatingChanges = false
}
override func nodeFor(indexPath: IndexPath) -> Node? {
return viewRootNode?.childAtIndex(indexPath.row)
}
}

View File

@ -0,0 +1,135 @@
//
// MasterTreeControllerDelegate.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import RSTree
import Articles
import Account
final class MasterTreeControllerDelegate: TreeControllerDelegate {
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
if node.isRoot {
return childNodesForRootNode(node)
}
if node.representedObject is Container {
return childNodesForContainerNode(node)
}
if node.representedObject is SmartFeedsController {
return childNodesForSmartFeeds(node)
}
return nil
}
}
private extension MasterTreeControllerDelegate {
func childNodesForRootNode(_ rootNode: Node) -> [Node]? {
// The top-level nodes are Smart Feeds and accounts.
let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared)
smartFeedsNode.canHaveChildNodes = true
smartFeedsNode.isGroupItem = true
return [smartFeedsNode] + sortedAccountNodes(rootNode)
}
func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] {
return SmartFeedsController.shared.smartFeeds.map { parentNode.existingOrNewChildNode(with: $0) }
}
func childNodesForContainerNode(_ containerNode: Node) -> [Node]? {
let container = containerNode.representedObject as! Container
var children = [AnyObject]()
children.append(contentsOf: Array(container.topLevelFeeds))
if let folders = container.folders {
children.append(contentsOf: Array(folders))
}
var updatedChildNodes = [Node]()
children.forEach { (representedObject) in
if let existingNode = containerNode.childNodeRepresentingObject(representedObject) {
if !updatedChildNodes.contains(existingNode) {
updatedChildNodes += [existingNode]
return
}
}
if let newNode = self.createNode(representedObject: representedObject, parent: containerNode) {
updatedChildNodes += [newNode]
}
}
return updatedChildNodes.sortedAlphabeticallyWithFoldersAtEnd()
}
func createNode(representedObject: Any, parent: Node) -> Node? {
if let feed = representedObject as? Feed {
return createNode(feed: feed, parent: parent)
}
if let folder = representedObject as? Folder {
return createNode(folder: folder, parent: parent)
}
if let account = representedObject as? Account {
return createNode(account: account, parent: parent)
}
return nil
}
func createNode(feed: Feed, parent: Node) -> Node {
return parent.createChildNode(feed)
}
func createNode(folder: Folder, parent: Node) -> Node {
let node = parent.createChildNode(folder)
node.canHaveChildNodes = true
return node
}
func createNode(account: Account, parent: Node) -> Node {
let node = parent.createChildNode(account)
node.canHaveChildNodes = true
node.isGroupItem = true
return node
}
func sortedAccountNodes(_ parent: Node) -> [Node] {
let nodes = AccountManager.shared.accounts.map { (account) -> Node in
let accountNode = parent.existingOrNewChildNode(with: account)
accountNode.canHaveChildNodes = true
accountNode.isGroupItem = true
return accountNode
}
return nodes.sortedAlphabetically()
}
func nodeInArrayRepresentingObject(_ nodes: [Node], _ representedObject: AnyObject) -> Node? {
for oneNode in nodes {
if oneNode.representedObject === representedObject {
return oneNode
}
}
return nil
}
}

View File

@ -0,0 +1,320 @@
//
// MasterViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import RSCore
import RSTree
class MasterViewController: UITableViewController {
var animatingChanges = false
let treeControllerDelegate = MasterTreeControllerDelegate()
lazy var treeController: TreeController = {
return TreeController(delegate: treeControllerDelegate)
}()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
@objc private func refreshAccounts(_ sender: Any) {
AccountManager.shared.refreshAll()
}
@objc dynamic func progressDidChange(_ notification: Notification) {
if AccountManager.shared.combinedRefreshProgress.isComplete {
refreshControl?.endRefreshing()
} else {
refreshControl?.beginRefreshing()
}
}
@objc func containerChildrenDidChange(_ note: Notification) {
rebuildTreeAndReloadDataIfNeeded()
}
@objc func batchUpdateDidPerform(_ notification: Notification) {
rebuildTreeAndReloadDataIfNeeded()
}
@objc func unreadCountDidChange(_ note: Notification) {
guard let representedObject = note.object else {
return
}
configureUnreadCountForCellsForRepresentedObject(representedObject as AnyObject)
}
@objc func faviconDidBecomeAvailable(_ note: Notification) {
applyToAvailableCells(configureFavicon)
}
@objc func feedSettingDidChange(_ note: Notification) {
guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
return
}
if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL {
configureCellsForRepresentedObject(feed)
}
}
@objc func displayNameDidChange(_ note: Notification) {
guard let object = note.object else {
return
}
rebuildTreeAndReloadDataIfNeeded()
configureCellsForRepresentedObject(object as AnyObject)
}
// MARK: Table View
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTableViewCell
guard let node = nodeFor(indexPath: indexPath) else {
return cell
}
configure(cell, node)
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
guard let node = nodeFor(indexPath: indexPath), !(node.representedObject is PseudoFeed) else {
return false
}
return true
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
// Set up the delete action
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completionHandler) in
self?.delete(indexPath: indexPath)
completionHandler(true)
}
deleteAction.backgroundColor = UIColor.red
// Set up the rename action
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
let renameAction = UIContextualAction(style: .normal, title: renameTitle) { [weak self] (action, view, completionHandler) in
self?.rename(indexPath: indexPath)
completionHandler(true)
}
renameAction.backgroundColor = UIColor.gray
return UISwipeActionsConfiguration(actions: [deleteAction, renameAction])
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let node = nodeFor(indexPath: indexPath) else {
assertionFailure()
return
}
if let pseudoFeed = node.representedObject as? PseudoFeed {
let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
timeline.title = pseudoFeed.nameForDisplay
timeline.representedObjects = [pseudoFeed]
self.navigationController?.pushViewController(timeline, animated: true)
}
if let folder = node.representedObject as? Folder {
let secondary = UIStoryboard.main.instantiateController(ofType: MasterSecondaryViewController.self)
secondary.title = folder.nameForDisplay
secondary.viewRootNode = node
self.navigationController?.pushViewController(secondary, animated: true)
}
if let feed = node.representedObject as? Feed {
let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
timeline.title = feed.nameForDisplay
timeline.representedObjects = [feed]
self.navigationController?.pushViewController(timeline, animated: true)
}
}
// MARK: Actions
@IBAction func addFeed(_ sender: UIBarButtonItem) {
let feedViewController = UIStoryboard(name: "AddFeed", bundle: nil).instantiateViewController(withIdentifier: "AddFeedNavigationController")
feedViewController.modalPresentationStyle = .popover
feedViewController.popoverPresentationController?.barButtonItem = sender
self.present(feedViewController, animated: true)
}
@IBAction func addFolder(_ sender: UIBarButtonItem) {
let feedViewController = UIStoryboard(name: "AddFolder", bundle: nil).instantiateViewController(withIdentifier: "AddFolderNavigationController")
feedViewController.modalPresentationStyle = .popover
feedViewController.popoverPresentationController?.barButtonItem = sender
self.present(feedViewController, animated: true)
}
// MARK: API
func configure(_ cell: MasterTableViewCell, _ node: Node) {
cell.name = nameFor(node)
configureUnreadCount(cell, node)
configureFavicon(cell, node)
cell.shouldShowImage = node.representedObject is SmallIconProvider
}
func configureUnreadCount(_ cell: MasterTableViewCell, _ node: Node) {
cell.unreadCount = unreadCountFor(node)
}
func configureFavicon(_ cell: MasterTableViewCell, _ node: Node) {
cell.faviconImage = imageFor(node)
}
func imageFor(_ node: Node) -> UIImage? {
if let smallIconProvider = node.representedObject as? SmallIconProvider {
return smallIconProvider.smallIcon
}
return nil
}
func nameFor(_ node: Node) -> String {
if let displayNameProvider = node.representedObject as? DisplayNameProvider {
return displayNameProvider.nameForDisplay
}
return ""
}
func unreadCountFor(_ node: Node) -> Int {
if let unreadCountProvider = node.representedObject as? UnreadCountProvider {
return unreadCountProvider.unreadCount
}
return 0
}
func delete(indexPath: IndexPath) {
assertionFailure()
}
func rename(indexPath: IndexPath) {
let title = NSLocalizedString("Rename", comment: "Rename")
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
// TODO: Add the title of what is being renamed...
// let formatString = NSLocalizedString("Rename %@", comment: "Feed finder")
// let message = NSString.localizedStringWithFormat(formatString as NSString, )
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in
guard let node = self?.nodeFor(indexPath: indexPath),
let name = alertController.textFields?[0].text,
!name.isEmpty else {
return
}
if let feed = node.representedObject as? Feed {
feed.editedName = name
} else if let folder = node.representedObject as? Folder {
folder.name = name
}
}
alertController.addAction(renameAction)
alertController.addTextField() { textField in
textField.placeholder = NSLocalizedString("Name", comment: "Name")
}
self.present(alertController, animated: true) {
}
}
func nodeFor(indexPath: IndexPath) -> Node? {
assertionFailure()
return nil
}
}
// MARK: Private
private extension MasterViewController {
func rebuildTreeAndReloadDataIfNeeded() {
if !animatingChanges && !BatchUpdate.shared.isPerforming {
treeController.rebuild()
tableView.reloadData()
}
}
func configureCellsForRepresentedObject(_ representedObject: AnyObject) {
applyToCellsForRepresentedObject(representedObject, configure)
}
func configureUnreadCountForCellsForRepresentedObject(_ representedObject: AnyObject) {
applyToCellsForRepresentedObject(representedObject, configureUnreadCount)
}
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ callback: (MasterTableViewCell, Node) -> Void) {
applyToAvailableCells { (cell, node) in
if node.representedObject === representedObject {
callback(cell, node)
}
}
}
func applyToAvailableCells(_ callback: (MasterTableViewCell, Node) -> Void) {
tableView.visibleCells.forEach { cell in
guard let indexPath = tableView.indexPath(for: cell), let node = nodeFor(indexPath: indexPath) else {
return
}
callback(cell as! MasterTableViewCell, node)
}
}
}

View File

@ -1,95 +0,0 @@
//
// MasterViewController.swift
// NetNewsWire-iOS
//
// Created by Brent Simmons on 2/5/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import UIKit
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
var objects = [Any]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationItem.leftBarButtonItem = editButtonItem
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
if let split = splitViewController {
let controllers = split.viewControllers
detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@objc
func insertNewObject(_ sender: Any?) {
objects.insert(NSDate(), at: 0)
let indexPath = IndexPath(row: 0, section: 0)
tableView.insertRows(at: [indexPath], with: .automatic)
}
// MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let object = objects[indexPath.row] as! NSDate
cell.textLabel!.text = object.description
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
}

View File

@ -24,6 +24,11 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>

View File

@ -0,0 +1,163 @@
body {
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
margin-right: 20px;
font-family: -apple-system;
font-size: 18px;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.feedlink {
font-weight: bold;
}
.headerTable {
width: 100%;
height: 68px;
}
.systemMessage {
position: absolute;
top: 45%;
left: 50%;
transform: translateX(-55%) translateY(-50%);
}
:root {
--body-color: #444;
--body-background-color: -apple-system-text-background;
--link-color: hsla(215, 99%, 43%, 1);
--header-table-border-color: rgba(0, 0, 0, 0.1);
--header-color: rgba(0, 0, 0, 0.3);
--header-link-color: rgba(0, 0, 0, 0.3);
--body-code-color: #666;
--system-message-color: #cbcbcb;
--feedlink-color: rgba(0, 0, 0, 0.6);
}
@media(prefers-color-scheme: dark) {
:root {
--body-color: #d2d2d2;
--body-background-color: #2d2d2d;
--link-color: #4490e2;
--header-table-border-color: rgba(255, 255, 255, 0.1);
--header-color: #d2d2d2;
--header-link-color: #4490e2;
--body-code-color: #b2b2b2;
--system-message-color: #5f5f5f
}
}
body {
color: var(--body-color);
background-color: var(--body-background-color);
}
body a, body a:link, body a:visited {
color: var(--link-color);
}
body .headerTable {
border-bottom: 1px solid var(--header-table-border-color);
}
body .header {
color: var(--header-color);
}
body .header a:link, body .header a:visited {
color: var(--header-link-color);
}
body .articleDateline, body .articleDateLine.a:link, body .articleDateline a:visited {
color: var(--header-color);
}
body code, body pre {
color: var(--body-code-color);
}
body > .systemMessage {
color: var(--system-message-color);
}
.feedlink a:link, .feedlink a:visited {
color: var(--feed-link-color);
}
.avatar img {
border-radius: 4px;
}
.feedIcon {
border-radius: 4px;
}
.rightAlign {
text-align: right;
}
.leftAlign {
text-align: left;
}
.articleTitle {
margin-top: 26px;
}
.articleDateline {
margin-bottom: 25px;
font-weight: bold;
}
.articleBody {
line-height: 1.6em;
}
h1 {
line-height: 1.15em;
font-weight: bold;
}
code, pre {
font-family: "SF Mono", Menlo, "Courier New", Courier, monospace;
font-size: 14px;
}
pre {
white-space: pre-wrap;
}
img, video {
max-width: 100%;
height: auto;
}
/*Block ads and junk*/
iframe[src*="feedads"],
iframe[src*="doubleclick"],
iframe[src*="plusone.google"] {
display: none !important;
}
a[href*=".ads."],
a[href*="feedads"],
a[href*="feedburner"],
a[href*="doubleclick"],
a[href*="//ads."],
a[href*="api.tweetmeme"],
a[href*="delicious.com/post?"],
a[href*="digg.com/submit?"],
a[href*="google.com/bookmarks/mark?"],
a[href*="posterous.com/share?"],
a[href*="tumblr.com/share?"],
a[href*="linkedin.com/shareArticle?"],
a[href*="facebook.com/share.php?"],
a[href*="http://twitter.com/home?"],
a[href*="addtoany.com/share_save"] {
display: none !important;
}
img[src*=".ads."],
img[src*="//ads."],
img[src*="doubleclick"],
img[src*="feedads"],
img[src*="feedburner"],
img[src*="feedblitz"],
img[src*="share-buttons"] {
display: none !important;
}

View File

@ -0,0 +1,62 @@
//
// MasterTimelineCellData.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import Articles
struct MasterTimelineCellData {
let title: String
let text: String
let dateString: String
let feedName: String
let showFeedName: Bool
let avatar: UIImage? // feed icon, user avatar, or favicon
let showAvatar: Bool // Make space even when avatar is nil
let featuredImage: UIImage? // image from within the article
let read: Bool
let starred: Bool
init(article: Article, showFeedName: Bool, feedName: String?, avatar: UIImage?, showAvatar: Bool, featuredImage: UIImage?) {
self.title = TimelineStringFormatter.truncatedTitle(article)
self.text = TimelineStringFormatter.truncatedSummary(article)
self.dateString = TimelineStringFormatter.dateString(article.logicalDatePublished)
if let feedName = feedName {
self.feedName = TimelineStringFormatter.truncatedFeedName(feedName)
}
else {
self.feedName = ""
}
self.showFeedName = showFeedName
self.showAvatar = showAvatar
self.avatar = avatar
self.featuredImage = featuredImage
self.read = article.status.read
self.starred = article.status.starred
}
init() { //Empty
self.title = ""
self.text = ""
self.dateString = ""
self.feedName = ""
self.showFeedName = false
self.showAvatar = false
self.avatar = nil
self.featuredImage = nil
self.read = true
self.starred = false
}
}

View File

@ -0,0 +1,252 @@
//
// MasterTimelineCellLayout.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import RSCore
struct MasterTimelineCellLayout {
static let cellPadding = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
static let feedColor = AppAssets.timelineTextSecondaryColor
static let feedNameFont = UIFont.systemFont(ofSize: UIFont.systemFontSize)
static let dateColor = AppAssets.timelineTextSecondaryColor
static let dateFont = UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold)
static let dateMarginBottom = CGFloat(integerLiteral: 1)
static let titleColor = AppAssets.timelineTextPrimaryColor
static let titleFont = UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: .semibold)
static let titleBottomMargin = CGFloat(integerLiteral: 1)
static let titleNumberOfLines = 2
static let textColor = AppAssets.timelineTextPrimaryColor
static let textFont = UIFont.systemFont(ofSize: UIFont.systemFontSize)
static let textOnlyFont = UIFont.systemFont(ofSize: UIFont.systemFontSize)
static let unreadCircleDimension = CGFloat(integerLiteral: 8)
static let unreadCircleMarginRight = CGFloat(integerLiteral: 8)
static let starDimension = CGFloat(integerLiteral: 13)
static let avatarSize = CGSize(width: 48.0, height: 48.0)
static let avatarMarginLeft = CGFloat(integerLiteral: 8)
static let avatarCornerRadius = CGFloat(integerLiteral: 4)
let width: CGFloat
let height: CGFloat
let feedNameRect: CGRect
let dateRect: CGRect
let titleRect: CGRect
let numberOfLinesForTitle: Int
let summaryRect: CGRect
let textRect: CGRect
let unreadIndicatorRect: CGRect
let starRect: CGRect
let avatarImageRect: CGRect
let paddingBottom: CGFloat
init(width: CGFloat, height: CGFloat, feedNameRect: CGRect, dateRect: CGRect, titleRect: CGRect, numberOfLinesForTitle: Int, summaryRect: CGRect, textRect: CGRect, unreadIndicatorRect: CGRect, starRect: CGRect, avatarImageRect: CGRect, paddingBottom: CGFloat) {
self.width = width
self.feedNameRect = feedNameRect
self.dateRect = dateRect
self.titleRect = titleRect
self.numberOfLinesForTitle = numberOfLinesForTitle
self.summaryRect = summaryRect
self.textRect = textRect
self.unreadIndicatorRect = unreadIndicatorRect
self.starRect = starRect
self.avatarImageRect = avatarImageRect
self.paddingBottom = paddingBottom
if height > 0.1 {
self.height = height
} else {
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom
}
}
init(width: CGFloat, height: CGFloat, cellData: MasterTimelineCellData, hasAvatar: Bool) {
// If height == 0.0, then height is calculated.
let showAvatar = hasAvatar && cellData.showAvatar
var textBoxRect = MasterTimelineCellLayout.rectForTextBox(cellData, showAvatar, width)
let (titleRect, numberOfLinesForTitle) = MasterTimelineCellLayout.rectForTitle(textBoxRect, cellData)
let summaryRect = numberOfLinesForTitle > 0 ? MasterTimelineCellLayout.rectForSummary(textBoxRect, titleRect, numberOfLinesForTitle, cellData) : CGRect.zero
let textRect = numberOfLinesForTitle > 0 ? CGRect.zero : MasterTimelineCellLayout.rectForText(textBoxRect, cellData)
var lastTextRect = titleRect
if numberOfLinesForTitle == 0 {
lastTextRect = textRect
} else if numberOfLinesForTitle == 1 {
if summaryRect.height > 0.1 {
lastTextRect = summaryRect
}
}
let dateRect = MasterTimelineCellLayout.rectForDate(textBoxRect, lastTextRect, cellData)
let feedNameRect = MasterTimelineCellLayout.rectForFeedName(textBoxRect, dateRect, cellData)
textBoxRect.size.height = ceil([titleRect, summaryRect, textRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
let avatarImageRect = MasterTimelineCellLayout.rectForAvatar(cellData, showAvatar, textBoxRect, width, height)
let unreadIndicatorRect = MasterTimelineCellLayout.rectForUnreadIndicator(textBoxRect)
let starRect = MasterTimelineCellLayout.rectForStar(unreadIndicatorRect)
let paddingBottom = MasterTimelineCellLayout.cellPadding.bottom
self.init(width: width, height: height, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, numberOfLinesForTitle: numberOfLinesForTitle, summaryRect: summaryRect, textRect: textRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: paddingBottom)
}
static func height(for width: CGFloat, cellData: MasterTimelineCellData) -> CGFloat {
let layout = MasterTimelineCellLayout(width: width, height: 0.0, cellData: cellData, hasAvatar: true)
return layout.height
}
}
// MARK: - Calculate Rects
private extension MasterTimelineCellLayout {
static func rectForTextBox(_ cellData: MasterTimelineCellData, _ showAvatar: Bool, _ width: CGFloat) -> CGRect {
// Returned height is a placeholder. Not needed when this is calculated.
let textBoxOriginX = MasterTimelineCellLayout.cellPadding.left + MasterTimelineCellLayout.unreadCircleDimension + MasterTimelineCellLayout.unreadCircleMarginRight
let textBoxMaxX = floor((width - MasterTimelineCellLayout.cellPadding.right) - (showAvatar ? MasterTimelineCellLayout.avatarSize.width + MasterTimelineCellLayout.avatarMarginLeft : 0.0))
let textBoxWidth = floor(textBoxMaxX - textBoxOriginX)
let textBoxRect = CGRect(x: textBoxOriginX, y: MasterTimelineCellLayout.cellPadding.top, width: textBoxWidth, height: 1000000)
return textBoxRect
}
static func rectForTitle(_ textBoxRect: CGRect, _ cellData: MasterTimelineCellData) -> (CGRect, Int) {
var r = textBoxRect
if cellData.title.isEmpty {
r.size.height = 0
return (r, 0)
}
let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MasterTimelineCellLayout.titleFont, numberOfLines: MasterTimelineCellLayout.titleNumberOfLines, width: Int(textBoxRect.width))
r.size.height = sizeInfo.size.height
if sizeInfo.numberOfLinesUsed < 1 {
r.size.height = 0
}
return (r, sizeInfo.numberOfLinesUsed)
}
static func rectForSummary(_ textBoxRect: CGRect, _ titleRect: CGRect, _ titleNumberOfLines: Int, _ cellData: MasterTimelineCellData) -> CGRect {
if titleNumberOfLines >= MasterTimelineCellLayout.titleNumberOfLines || cellData.text.isEmpty {
return CGRect.zero
}
return rectOfLineBelow(titleRect, titleRect, 0, cellData.text, MasterTimelineCellLayout.textFont)
}
static func rectForText(_ textBoxRect: CGRect, _ cellData: MasterTimelineCellData) -> CGRect {
var r = textBoxRect
if cellData.text.isEmpty {
r.size.height = 0
return r
}
let sizeInfo = MultilineUILabelSizer.size(for: cellData.text, font: MasterTimelineCellLayout.textOnlyFont, numberOfLines: MasterTimelineCellLayout.titleNumberOfLines, width: Int(textBoxRect.width))
r.size.height = sizeInfo.size.height
if sizeInfo.numberOfLinesUsed < 1 {
r.size.height = 0
}
return r
}
static func rectForDate(_ textBoxRect: CGRect, _ rectAbove: CGRect, _ cellData: MasterTimelineCellData) -> CGRect {
return rectOfLineBelow(textBoxRect, rectAbove, MasterTimelineCellLayout.titleBottomMargin, cellData.dateString, MasterTimelineCellLayout.dateFont)
}
static func rectForFeedName(_ textBoxRect: CGRect, _ dateRect: CGRect, _ cellData: MasterTimelineCellData) -> CGRect {
if !cellData.showFeedName {
return CGRect.zero
}
return rectOfLineBelow(textBoxRect, dateRect, MasterTimelineCellLayout.dateMarginBottom, cellData.feedName, MasterTimelineCellLayout.feedNameFont)
}
static func rectOfLineBelow(_ textBoxRect: CGRect, _ rectAbove: CGRect, _ topMargin: CGFloat, _ value: String, _ font: UIFont) -> CGRect {
let textFieldSize = SingleLineUILabelSizer.size(for: value, font: font)
var r = CGRect.zero
r.size = textFieldSize
r.origin.y = rectAbove.maxY + topMargin
r.origin.x = textBoxRect.origin.x
var width = textFieldSize.width
width = min(width, textBoxRect.size.width)
width = max(width, 0.0)
r.size.width = width
return r
}
static func rectForUnreadIndicator(_ titleRect: CGRect) -> CGRect {
var r = CGRect.zero
r.size = CGSize(width: MasterTimelineCellLayout.unreadCircleDimension, height: MasterTimelineCellLayout.unreadCircleDimension)
r.origin.x = MasterTimelineCellLayout.cellPadding.left
r.origin.y = titleRect.minY + 6
return r
}
static func rectForStar(_ unreadIndicatorRect: CGRect) -> CGRect {
var r = CGRect.zero
r.size.width = MasterTimelineCellLayout.starDimension
r.size.height = MasterTimelineCellLayout.starDimension
r.origin.x = floor(unreadIndicatorRect.origin.x - ((MasterTimelineCellLayout.starDimension - MasterTimelineCellLayout.unreadCircleDimension) / 2.0))
r.origin.y = unreadIndicatorRect.origin.y - 4.0
return r
}
static func rectForAvatar(_ cellData: MasterTimelineCellData, _ showAvatar: Bool, _ textBoxRect: CGRect, _ width: CGFloat, _ height: CGFloat) -> CGRect {
var r = CGRect.zero
if !showAvatar {
return r
}
r.size = MasterTimelineCellLayout.avatarSize
r.origin.x = (width - MasterTimelineCellLayout.cellPadding.right) - r.size.width
r.origin.y = textBoxRect.origin.y + 4.0
return r
}
}
private extension Array where Element == CGRect {
func maxY() -> CGFloat {
var y: CGFloat = 0.0
self.forEach { y = Swift.max(y, $0.maxY) }
return y
}
}

View File

@ -0,0 +1,230 @@
//
// MasterTimelineTableViewCell.swift
// NetNewsWire
//
// Created by Brent Simmons on 8/31/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import RSCore
class MasterTimelineTableViewCell: UITableViewCell {
private let titleView = MasterTimelineTableViewCell.multiLineUILabel()
private let summaryView = MasterTimelineTableViewCell.singleLineUILabel()
private let textView = MasterTimelineTableViewCell.multiLineUILabel()
private let unreadIndicatorView = MasterUnreadIndicatorView(frame: CGRect.zero)
private let dateView = MasterTimelineTableViewCell.singleLineUILabel()
private let feedNameView = MasterTimelineTableViewCell.singleLineUILabel()
private lazy var avatarImageView = {
return UIImageView(image: AppAssets.feedImage)
}()
private lazy var starView = {
return UIImageView(image: AppAssets.timelineStarImage)
}()
private lazy var textFields = {
return [self.dateView, self.feedNameView, self.titleView, self.summaryView, self.textView]
}()
var cellData: MasterTimelineCellData! {
didSet {
updateSubviews()
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override var frame: CGRect {
didSet {
setNeedsLayout()
}
}
override func layoutSubviews() {
super.layoutSubviews()
let layoutRects = updatedLayoutRects()
setFrame(for: titleView, rect: layoutRects.titleRect)
setFrame(for: summaryView, rect: layoutRects.summaryRect)
setFrame(for: textView, rect: layoutRects.textRect)
dateView.rs_setFrameIfNotEqual(layoutRects.dateRect)
unreadIndicatorView.rs_setFrameIfNotEqual(layoutRects.unreadIndicatorRect)
feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect)
avatarImageView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect)
starView.rs_setFrameIfNotEqual(layoutRects.starRect)
}
}
// MARK: - Private
private extension MasterTimelineTableViewCell {
static func singleLineUILabel() -> UILabel {
let label = UILabel()
label.lineBreakMode = .byTruncatingTail
label.allowsDefaultTighteningForTruncation = false
return label
}
static func multiLineUILabel() -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.allowsDefaultTighteningForTruncation = false
return label
}
func setFrame(for label: UILabel, rect: CGRect) {
if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 {
hideView(label)
} else {
showView(label)
label.rs_setFrameIfNotEqual(rect)
}
}
func addSubviewAtInit(_ view: UIView, hidden: Bool) {
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = hidden
}
func commonInit() {
addSubviewAtInit(titleView, hidden: false)
addSubviewAtInit(summaryView, hidden: true)
addSubviewAtInit(textView, hidden: true)
addSubviewAtInit(unreadIndicatorView, hidden: true)
addSubviewAtInit(dateView, hidden: false)
addSubviewAtInit(feedNameView, hidden: true)
addSubviewAtInit(avatarImageView, hidden: true)
addSubviewAtInit(starView, hidden: true)
}
func updatedLayoutRects() -> MasterTimelineCellLayout {
return MasterTimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, hasAvatar: avatarImageView.image != nil)
}
func updateTitleView() {
titleView.font = MasterTimelineCellLayout.titleFont
titleView.textColor = MasterTimelineCellLayout.titleColor
updateTextFieldText(titleView, cellData?.title)
}
func updateSummaryView() {
summaryView.font = MasterTimelineCellLayout.textFont
summaryView.textColor = MasterTimelineCellLayout.textColor
updateTextFieldText(summaryView, cellData?.text)
}
func updateTextView() {
textView.font = MasterTimelineCellLayout.textFont
textView.textColor = MasterTimelineCellLayout.textColor
updateTextFieldText(textView, cellData?.text)
}
func updateDateView() {
dateView.font = MasterTimelineCellLayout.dateFont
dateView.textColor = MasterTimelineCellLayout.dateColor
updateTextFieldText(dateView, cellData.dateString)
}
func updateTextFieldText(_ label: UILabel, _ text: String?) {
let s = text ?? ""
if label.text != s {
label.text = s
setNeedsLayout()
}
}
func updateFeedNameView() {
if cellData.showFeedName {
showView(feedNameView)
feedNameView.font = MasterTimelineCellLayout.feedNameFont
feedNameView.textColor = MasterTimelineCellLayout.feedColor
updateTextFieldText(feedNameView, cellData.feedName)
} else {
hideView(feedNameView)
}
}
func updateUnreadIndicator() {
showOrHideView(unreadIndicatorView, cellData.read || cellData.starred)
}
func updateStarView() {
showOrHideView(starView, !cellData.starred)
}
func updateAvatar() {
// The avatar should be bigger than a favicon. Theyre too small; they look weird.
guard let image = cellData.avatar, cellData.showAvatar, image.size.height >= 22.0, image.size.width >= 22.0 else {
makeAvatarEmpty()
return
}
showView(avatarImageView)
avatarImageView.layer.cornerRadius = MasterTimelineCellLayout.avatarCornerRadius
avatarImageView.clipsToBounds = true
if avatarImageView.image !== image {
avatarImageView.image = image
setNeedsLayout()
}
}
func makeAvatarEmpty() {
if avatarImageView.image != nil {
avatarImageView.image = nil
setNeedsLayout()
}
hideView(avatarImageView)
}
func hideView(_ view: UIView) {
if !view.isHidden {
view.isHidden = true
}
}
func showView(_ view: UIView) {
if view.isHidden {
view.isHidden = false
}
}
func showOrHideView(_ view: UIView, _ shouldHide: Bool) {
shouldHide ? hideView(view) : showView(view)
}
func updateSubviews() {
updateTitleView()
updateSummaryView()
updateTextView()
updateDateView()
updateFeedNameView()
updateUnreadIndicator()
updateStarView()
updateAvatar()
}
}

View File

@ -0,0 +1,33 @@
//
// MasterUnreadIndicatorView.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/16/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import UIKit
class MasterUnreadIndicatorView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
}
static let bezierPath: UIBezierPath = {
let r = CGRect(x: 0.0, y: 0.0, width: MasterTimelineCellLayout.unreadCircleDimension, height: MasterTimelineCellLayout.unreadCircleDimension)
return UIBezierPath(ovalIn: r)
}()
override func draw(_ dirtyRect: CGRect) {
AppAssets.timelineUnreadCircleColor.setFill()
MasterUnreadIndicatorView.bezierPath.fill()
}
}

View File

@ -0,0 +1,179 @@
//
// UILabelSizerSpecifier.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/19/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import UIKit
// Get the height of an NSTextField given a string, font, and width.
// Uses a cache. Avoids actually measuring text as much as possible.
// Main thread only.
typealias WidthHeightCache = [Int: Int] // width: height
private struct UILabelSizerSpecifier: Hashable {
let numberOfLines: Int
let font: UIFont
}
struct TextFieldSizeInfo {
let size: CGSize // Integral size (ceiled)
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
}
final class MultilineUILabelSizer {
private let numberOfLines: Int
private let font: UIFont
private let singleLineHeightEstimate: Int
private let doubleLineHeightEstimate: Int
private var cache = [String: WidthHeightCache]() // Each string has a cache.
private static var sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]()
private init(numberOfLines: Int, font: UIFont) {
self.numberOfLines = numberOfLines
self.font = font
self.singleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y", 200, font)
self.doubleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, font)
}
static func size(for string: String, font: UIFont, numberOfLines: Int, width: Int) -> TextFieldSizeInfo {
return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width)
}
static func emptyCache() {
sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]()
}
}
// MARK: - Private
private extension MultilineUILabelSizer {
static func sizer(numberOfLines: Int, font: UIFont) -> MultilineUILabelSizer {
let specifier = UILabelSizerSpecifier(numberOfLines: numberOfLines, font: font)
if let cachedSizer = sizers[specifier] {
return cachedSizer
}
let newSizer = MultilineUILabelSizer(numberOfLines: numberOfLines, font: font)
sizers[specifier] = newSizer
return newSizer
}
func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo {
let textFieldHeight = height(for: string, width: width)
let numberOfLinesUsed = numberOfLines(for: textFieldHeight)
let size = CGSize(width: width, height: textFieldHeight)
let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed)
return sizeInfo
}
func height(for string: String, width: Int) -> Int {
if cache[string] == nil {
cache[string] = WidthHeightCache()
}
if let height = cache[string]![width] {
return height
}
if let height = heightConsideringNeighbors(cache[string]!, width) {
return height
}
var height = MultilineUILabelSizer.calculateHeight(string, width, font)
let maxHeight = singleLineHeightEstimate * numberOfLines
if height > maxHeight {
height = maxHeight
}
cache[string]![width] = height
return height
}
static func calculateHeight(_ string: String, _ width: Int, _ font: UIFont) -> Int {
let height = string.height(withConstrainedWidth: CGFloat(width), font: font)
return Int(ceil(height))
}
func numberOfLines(for height: Int) -> Int {
// Well have to see if this really works reliably.
let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0
let lines = Int(round(CGFloat(height) / averageHeight))
return lines
}
func heightIsProbablySingleLineHeight(_ height: Int) -> Bool {
return heightIsProbablyEqualToEstimate(height, singleLineHeightEstimate)
}
func heightIsProbablyDoubleLineHeight(_ height: Int) -> Bool {
return heightIsProbablyEqualToEstimate(height, doubleLineHeightEstimate)
}
func heightIsProbablyEqualToEstimate(_ height: Int, _ estimate: Int) -> Bool {
let slop = 4
let minimum = estimate - slop
let maximum = estimate + slop
return height >= minimum && height <= maximum
}
func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? {
// Given width, if the height at width - something and width + something is equal,
// then that height must be correct for the given width.
// Also:
// If a narrower neighbors height is single line height, then this wider width must also be single-line height.
// If a wider neighbors height is double line height, and numberOfLines == 2, then this narrower width must able be double-line height.
var smallNeighbor = (width: 0, height: 0)
var largeNeighbor = (width: 0, height: 0)
for (oneWidth, oneHeight) in heightCache {
if oneWidth < width && heightIsProbablySingleLineHeight(oneHeight) {
return oneHeight
}
if numberOfLines == 2 && oneWidth > width && heightIsProbablyDoubleLineHeight(oneHeight) {
return oneHeight
}
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
smallNeighbor = (oneWidth, oneHeight)
}
else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
largeNeighbor = (oneWidth, oneHeight)
}
if smallNeighbor.width != 0 && smallNeighbor.height == largeNeighbor.height {
return smallNeighbor.height
}
}
return nil
}
}

View File

@ -0,0 +1,64 @@
//
// SingleLineUILabelSizer.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/19/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import UIKit
// Get the size of an UILabel configured with a specific font with a specific size.
// Uses a cache.
// Main thready only.
final class SingleLineUILabelSizer {
let font: UIFont
private var cache = [String: CGSize]()
init(font: UIFont) {
self.font = font
}
func size(for text: String) -> CGSize {
if let cachedSize = cache[text] {
return cachedSize
}
let height = text.height(withConstrainedWidth: .greatestFiniteMagnitude, font: font)
let width = text.width(withConstrainedHeight: .greatestFiniteMagnitude, font: font)
let calculatedSize = CGSize(width: ceil(width), height: ceil(height))
cache[text] = calculatedSize
return calculatedSize
}
static private var sizers = [UIFont: SingleLineUILabelSizer]()
static func sizer(for font: UIFont) -> SingleLineUILabelSizer {
if let cachedSizer = sizers[font] {
return cachedSizer
}
let newSizer = SingleLineUILabelSizer(font: font)
sizers[font] = newSizer
return newSizer
}
// Use this call. Its easiest.
static func size(for text: String, font: UIFont) -> CGSize {
return sizer(for: font).size(for: text)
}
static func emptyCache() {
sizers = [UIFont: SingleLineUILabelSizer]()
}
}

View File

@ -0,0 +1,602 @@
//
// MasterTimelineViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
import Account
import Articles
class MasterTimelineViewController: UITableViewController {
private var showAvatars = false
private var rowHeightWithFeedName: CGFloat = 0.0
private var rowHeightWithoutFeedName: CGFloat = 0.0
private var currentRowHeight: CGFloat {
return showFeedNames ? rowHeightWithFeedName : rowHeightWithoutFeedName
}
static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
var detailViewController: DetailViewController? {
if let split = splitViewController {
let controllers = split.viewControllers
return (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
return nil
}
var representedObjects: [AnyObject]? {
didSet {
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
if let representedObjects = representedObjects {
if representedObjects.count == 1 && representedObjects.first is Feed {
showFeedNames = false
}
else {
showFeedNames = true
}
}
else {
showFeedNames = false
}
fetchArticles()
if articles.count > 0 {
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
}
}
}
}
var articles = ArticleArray() {
didSet {
if articles == oldValue {
return
}
if articles.representSameArticlesInSameOrder(as: oldValue) {
// When the array is the same  same articles, same order
// but some data in some of the articles may have changed.
// Just reload visible cells in this case: dont call reloadData.
articleRowMap = [String: Int]()
reloadAllVisibleCells()
return
}
updateShowAvatars()
articleRowMap = [String: Int]()
tableView.reloadData()
}
}
private var articleRowMap = [String: Int]() // articleID: rowIndex
private var showFeedNames = false {
didSet {
if showFeedNames != oldValue {
updateShowAvatars()
updateTableViewRowHeight()
}
}
}
private var sortDirection = AppDefaults.timelineSortDirection {
didSet {
if sortDirection != oldValue {
sortDirectionDidChange()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
updateRowHeights()
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
let article = articles[indexPath.row]
controller.article = article
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
// MARK Actions
@IBAction func markAllAsRead(_ sender: Any) {
let title = NSLocalizedString("Mark All Read", comment: "Mark All Read")
let message = NSLocalizedString("Mark all articles in this timeline as read?", comment: "Mark all articles")
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("Mark All Read", comment: "Mark All Read")
let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in
guard let articles = self?.articles else { return }
markArticles(Set(articles), statusKey: .read, flag: true)
}
alertController.addAction(markAction)
present(alertController, animated: true)
}
// MARK: - Table view
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return articles.count
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let article = articles[indexPath.row]
// Set up the star action
let starTitle = article.status.starred ?
NSLocalizedString("Unstar", comment: "Unstar") :
NSLocalizedString("Star", comment: "Star")
let starAction = UIContextualAction(style: .normal, title: starTitle) { (action, view, completionHandler) in
markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred)
completionHandler(true)
}
starAction.image = AppAssets.starClosedImage
starAction.backgroundColor = AppAssets.starColor
// Set up the read action
let readTitle = article.status.read ?
NSLocalizedString("Unread", comment: "Unread") :
NSLocalizedString("Read", comment: "Read")
let readAction = UIContextualAction(style: .normal, title: readTitle) { (action, view, completionHandler) in
markArticles(Set([article]), statusKey: .read, flag: !article.status.read)
completionHandler(true)
}
readAction.image = AppAssets.circleClosedImage
readAction.backgroundColor = AppAssets.timelineUnreadCircleColor
let configuration = UISwipeActionsConfiguration(actions: [starAction, readAction])
return configuration
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell
let article = articles[indexPath.row]
configureTimelineCell(cell, article: article)
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let article = articles[indexPath.row]
if !article.status.read {
markArticles(Set([article]), statusKey: .read, flag: true)
}
}
// MARK: Notifications
@objc func statusesDidChange(_ note: Notification) {
guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
return
}
reloadVisibleCells(for: articles)
}
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
return
}
performBlockAndRestoreSelection {
tableView.indexPathsForVisibleRows?.forEach { indexPath in
guard let article = articles.articleAtRow(indexPath.row) else {
return
}
if feed == article.feed {
tableView.reloadRows(at: [indexPath], with: .none)
return
}
}
}
}
@objc func avatarDidBecomeAvailable(_ note: Notification) {
guard showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
return
}
performBlockAndRestoreSelection {
tableView.indexPathsForVisibleRows?.forEach { indexPath in
guard let article = articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
return
}
for author in authors {
if author.avatarURL == avatarURL {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
}
}
}
@objc func imageDidBecomeAvailable(_ note: Notification) {
if showAvatars {
queueReloadVisableCells()
}
}
@objc func accountDidDownloadArticles(_ note: Notification) {
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
return
}
let shouldFetchAndMergeArticles = representedObjectsContainsAnyFeed(feeds) || representedObjectsContainsAnyPseudoFeed()
if shouldFetchAndMergeArticles {
queueFetchAndMergeArticles()
}
}
@objc func userDefaultsDidChange(_ note: Notification) {
self.sortDirection = AppDefaults.timelineSortDirection
}
// MARK: Reloading
@objc func reloadAllVisibleCells() {
tableView.beginUpdates()
performBlockAndRestoreSelection {
tableView.reloadRows(at: tableView.indexPathsForVisibleRows!, with: .none)
}
tableView.endUpdates()
}
private func reloadVisibleCells(for articles: [Article]) {
reloadVisibleCells(for: Set(articles.articleIDs()))
}
private func reloadVisibleCells(for articles: Set<Article>) {
reloadVisibleCells(for: articles.articleIDs())
}
private func reloadVisibleCells(for articleIDs: Set<String>) {
if articleIDs.isEmpty {
return
}
let indexes = indexesForArticleIDs(articleIDs)
reloadVisibleCells(for: indexes)
}
private func reloadVisibleCells(for indexes: IndexSet) {
performBlockAndRestoreSelection {
tableView.indexPathsForVisibleRows?.forEach { indexPath in
if indexes.contains(indexPath.row) {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
}
}
// MARK: Cell Configuring
private func calculateRowHeight(showingFeedNames: Bool) -> CGFloat {
let longTitle = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
let prototypeID = "prototype"
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, userDeleted: false, dateArrived: Date())
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: nil, dateModified: nil, authors: nil, attachments: nil, status: status)
let prototypeCellData = MasterTimelineCellData(article: prototypeArticle, showFeedName: showingFeedNames, feedName: "Prototype Feed Name", avatar: nil, showAvatar: false, featuredImage: nil)
let height = MasterTimelineCellLayout.height(for: 100, cellData: prototypeCellData)
return height
}
private func updateRowHeights() {
rowHeightWithFeedName = calculateRowHeight(showingFeedNames: true)
rowHeightWithoutFeedName = calculateRowHeight(showingFeedNames: false)
updateTableViewRowHeight()
}
@objc func fetchAndMergeArticles() {
guard let representedObjects = representedObjects else {
return
}
var unsortedArticles = fetchUnsortedArticles(for: representedObjects)
// Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles.
let unsortedArticleIDs = unsortedArticles.articleIDs()
for article in articles {
if !unsortedArticleIDs.contains(article.articleID) {
unsortedArticles.insert(article)
}
}
updateArticles(with: unsortedArticles)
}
}
// MARK: Private
private extension MasterTimelineViewController {
func configureTimelineCell(_ cell: MasterTimelineTableViewCell, article: Article) {
var avatar = avatarFor(article)
if avatar == nil, let feed = article.feed {
avatar = appDelegate.faviconDownloader.favicon(for: feed)
}
let featuredImage = featuredImageFor(article)
cell.cellData = MasterTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatars, featuredImage: featuredImage)
}
func avatarFor(_ article: Article) -> UIImage? {
if !showAvatars {
return nil
}
if let authors = article.authors {
for author in authors {
if let image = avatarForAuthor(author) {
return image
}
}
}
guard let feed = article.feed else {
return nil
}
return appDelegate.feedIconDownloader.icon(for: feed)
}
func avatarForAuthor(_ author: Author) -> UIImage? {
return appDelegate.authorAvatarDownloader.image(for: author)
}
func featuredImageFor(_ article: Article) -> UIImage? {
if let url = article.imageURL, let data = appDelegate.imageDownloader.image(for: url) {
return RSImage(data: data)
}
return nil
}
func queueReloadVisableCells() {
CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells))
}
func updateTableViewRowHeight() {
tableView.rowHeight = currentRowHeight
tableView.estimatedRowHeight = currentRowHeight
}
func updateShowAvatars() {
if showFeedNames {
self.showAvatars = true
return
}
for article in articles {
if let authors = article.authors {
for author in authors {
if author.avatarURL != nil {
self.showAvatars = true
return
}
}
}
}
self.showAvatars = false
}
func representedObjectArraysAreEqual(_ objects1: [AnyObject]?, _ objects2: [AnyObject]?) -> Bool {
if objects1 == nil && objects2 == nil {
return true
}
guard let objects1 = objects1, let objects2 = objects2 else {
return false
}
if objects1.count != objects2.count {
return false
}
var ix = 0
for oneObject in objects1 {
if oneObject !== objects2[ix] {
return false
}
ix += 1
}
return true
}
// MARK: Fetching Articles
func fetchArticles() {
guard let representedObjects = representedObjects else {
emptyTheTimeline()
return
}
let fetchedArticles = fetchUnsortedArticles(for: representedObjects)
updateArticles(with: fetchedArticles)
}
func emptyTheTimeline() {
if !articles.isEmpty {
articles = [Article]()
}
}
func sortDirectionDidChange() {
updateArticles(with: Set(articles))
}
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
let indexPaths = tableView.indexPathsForSelectedRows
block()
indexPaths?.forEach { [weak self] indexPath in
self?.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
}
func updateArticles(with unsortedArticles: Set<Article>) {
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection)
if articles != sortedArticles {
articles = sortedArticles
}
}
func fetchUnsortedArticles(for representedObjects: [Any]) -> Set<Article> {
var fetchedArticles = Set<Article>()
for object in representedObjects {
if let articleFetcher = object as? ArticleFetcher {
fetchedArticles.formUnion(articleFetcher.fetchArticles())
}
}
return fetchedArticles
}
func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
var indexes = IndexSet()
articleIDs.forEach { (articleID) in
guard let oneIndex = row(for: articleID) else {
return
}
if oneIndex != NSNotFound {
indexes.insert(oneIndex)
}
}
return indexes
}
func row(for articleID: String) -> Int? {
updateArticleRowMapIfNeeded()
return articleRowMap[articleID]
}
func updateArticleRowMap() {
var rowMap = [String: Int]()
var index = 0
articles.forEach { (article) in
rowMap[article.articleID] = index
index += 1
}
articleRowMap = rowMap
}
func updateArticleRowMapIfNeeded() {
if articleRowMap.isEmpty {
updateArticleRowMap()
}
}
func queueFetchAndMergeArticles() {
MasterTimelineViewController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
}
func representedObjectsContainsAnyPseudoFeed() -> Bool {
guard let representedObjects = representedObjects else {
return false
}
for representedObject in representedObjects {
if representedObject is PseudoFeed {
return true
}
}
return false
}
func representedObjectsContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
// Return true if theres a match or if a folder contains (recursively) one of feeds
guard let representedObjects = representedObjects else {
return false
}
for representedObject in representedObjects {
if let feed = representedObject as? Feed {
for oneFeed in feeds {
if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url {
return true
}
}
}
else if let folder = representedObject as? Folder {
for oneFeed in feeds {
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
return true
}
}
}
}
return false
}
}