mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-01 03:26:54 +01:00
Added initial POC version of NetNewsWire for iOS to use as a starting point for the actual app.
This commit is contained in:
parent
8f1f153e98
commit
8526db8b4c
@ -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)
|
||||
}()
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -6,7 +6,7 @@
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Articles
|
||||
|
||||
extension Notification.Name {
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSWeb
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Articles
|
||||
import Account
|
||||
import RSCore
|
||||
|
@ -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
|
||||
|
@ -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]()
|
||||
|
@ -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() {
|
||||
|
||||
|
190
iOS/Add Feed/AddFeed.storyboard
Normal file
190
iOS/Add Feed/AddFeed.storyboard
Normal 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>
|
40
iOS/Add Feed/AddFeedFolderPickerData.swift
Normal file
40
iOS/Add Feed/AddFeedFolderPickerData.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
248
iOS/Add Feed/AddFeedViewController.swift
Normal file
248
iOS/Add Feed/AddFeedViewController.swift
Normal 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("Can’t add this feed because you’ve 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("Can’t 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("Can’t 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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
43
iOS/Add Feed/FolderTreeControllerDelegate.swift
Normal file
43
iOS/Add Feed/FolderTreeControllerDelegate.swift
Normal 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 can’t 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
|
||||
}
|
||||
}
|
149
iOS/Add Folder/AddFolder.storyboard
Normal file
149
iOS/Add Folder/AddFolder.storyboard
Normal 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>
|
84
iOS/Add Folder/AddFolderViewController.swift
Normal file
84
iOS/Add Folder/AddFolderViewController.swift
Normal 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
71
iOS/AppAssets.swift
Normal 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
136
iOS/AppDefaults.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
187
iOS/Detail/DetailViewController.swift
Normal file
187
iOS/Detail/DetailViewController.swift
Normal 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
|
||||
}
|
||||
|
||||
// Can’t use url-with-fragment as base URL. The webview won’t 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
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
25
iOS/Extensions/String-Extensions.swift
Normal file
25
iOS/Extensions/String-Extensions.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
32
iOS/Extensions/UIImage-Extensions.swift
Normal file
32
iOS/Extensions/UIImage-Extensions.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
29
iOS/Extensions/UIStoryboard-Extensions.swift
Normal file
29
iOS/Extensions/UIStoryboard-Extensions.swift
Normal 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
|
||||
|
||||
}
|
||||
|
||||
}
|
128
iOS/Master/Cell/MasterTableViewCell.swift
Normal file
128
iOS/Master/Cell/MasterTableViewCell.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
74
iOS/Master/Cell/MasterTableViewCellLayout.swift
Normal file
74
iOS/Master/Cell/MasterTableViewCellLayout.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
107
iOS/Master/Cell/MasterUnreadCountView.swift
Normal file
107
iOS/Master/Cell/MasterUnreadCountView.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
121
iOS/Master/MasterPrimaryViewController.swift
Normal file
121
iOS/Master/MasterPrimaryViewController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
74
iOS/Master/MasterSecondaryViewController.swift
Normal file
74
iOS/Master/MasterSecondaryViewController.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
135
iOS/Master/MasterTreeControllerDelegate.swift
Normal file
135
iOS/Master/MasterTreeControllerDelegate.swift
Normal 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
|
||||
}
|
||||
}
|
320
iOS/Master/MasterViewController.swift
Normal file
320
iOS/Master/MasterViewController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
163
iOS/Resources/styleSheet.css
Normal file
163
iOS/Resources/styleSheet.css
Normal 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;
|
||||
}
|
62
iOS/Timeline/Cell/MasterTimelineCellData.swift
Normal file
62
iOS/Timeline/Cell/MasterTimelineCellData.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
252
iOS/Timeline/Cell/MasterTimelineCellLayout.swift
Normal file
252
iOS/Timeline/Cell/MasterTimelineCellLayout.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
230
iOS/Timeline/Cell/MasterTimelineTableViewCell.swift
Normal file
230
iOS/Timeline/Cell/MasterTimelineTableViewCell.swift
Normal 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. They’re 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()
|
||||
}
|
||||
|
||||
}
|
33
iOS/Timeline/Cell/MasterUnreadIndicatorView.swift
Normal file
33
iOS/Timeline/Cell/MasterUnreadIndicatorView.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
179
iOS/Timeline/Cell/MultilineUILabelSizer.swift
Normal file
179
iOS/Timeline/Cell/MultilineUILabelSizer.swift
Normal 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 {
|
||||
|
||||
// We’ll 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 neighbor’s height is single line height, then this wider width must also be single-line height.
|
||||
// If a wider neighbor’s 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
|
||||
|
||||
}
|
||||
|
||||
}
|
64
iOS/Timeline/Cell/SingleLineUILabelSizer.swift
Normal file
64
iOS/Timeline/Cell/SingleLineUILabelSizer.swift
Normal 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. It’s easiest.
|
||||
|
||||
static func size(for text: String, font: UIFont) -> CGSize {
|
||||
return sizer(for: font).size(for: text)
|
||||
}
|
||||
|
||||
static func emptyCache() {
|
||||
sizers = [UIFont: SingleLineUILabelSizer]()
|
||||
}
|
||||
|
||||
}
|
602
iOS/Timeline/MasterTimelineViewController.swift
Normal file
602
iOS/Timeline/MasterTimelineViewController.swift
Normal 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: don’t 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 there’s 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
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user