├── .gitignore ├── CHANGELOG.md ├── HNReader.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── mattrighetti.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── mattrighetti.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── HNReader ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ └── Contents.json ├── HNClient │ ├── HackerNews.swift │ ├── HackerNewsClient.swift │ ├── ItemCache.swift │ └── ItemDownloader.swift ├── HNReader.entitlements ├── HNReader.xcdatamodeld │ ├── .xccurrentversion │ └── HNReader.xcdatamodel │ │ └── contents ├── HNReaderApp.swift ├── Info.plist ├── Model │ ├── Item.swift │ └── User.swift ├── Persistence.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Utils │ ├── +Date.swift │ └── +View.swift ├── View │ ├── Components │ │ ├── BgButton.swift │ │ └── HTMLText.swift │ ├── ConditionalRedactedModifier.swift │ ├── HNReader │ │ └── HNReader.app │ │ │ └── Contents │ │ │ ├── Info.plist │ │ │ ├── MacOS │ │ │ └── HNReader │ │ │ ├── PkgInfo │ │ │ ├── Resources │ │ │ ├── AppIcon.icns │ │ │ ├── Assets.car │ │ │ └── HNReader.momd │ │ │ │ ├── HNReader.mom │ │ │ │ ├── HNReader.omo │ │ │ │ └── VersionInfo.plist │ │ │ └── _CodeSignature │ │ │ └── CodeResources │ ├── HomeView.swift │ ├── InfoView.swift │ ├── ItemCell.swift │ └── ItemList.swift ├── ViewModel │ ├── AppState.swift │ └── ItemListViewModel.swift └── fonts │ ├── IBMPlexSerif-Bold.ttf │ ├── IBMPlexSerif-Light.ttf │ ├── IBMPlexSerif-Regular.ttf │ └── IBMPlexSerif-SemiBold.ttf ├── HNReaderTests ├── HNClientTests │ ├── HackerNewsClientTests.swift │ └── HackerNewsTests.swift ├── Info.plist ├── ModelTests │ └── ItemTests.swift └── UtilsTests │ └── +DateTests.swift ├── LICENSE ├── Logo ├── HNReader Logo.sketch └── Logo.png ├── README.md └── resources ├── appstore.png └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | GoogleService-Info.plist 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. 3 | 4 | - - - 5 | ## 1.3.1 - 2023-11-05 6 | #### Bug Fixes 7 | - fix bundle version - (805ebaf) - Mattia Righetti 8 | - fix settings view - (54e1250) - Mattia Righetti 9 | #### Features 10 | - flattened ui and add url/comments link - (0007c9c) - Mattia Righetti 11 | #### Miscellaneous Chores 12 | - **(version)** bump to v1.3 - (f472012) - Mattia Righetti 13 | - xcode stuff - (c1fe436) - Mattia Righetti 14 | #### Refactoring 15 | - misc infoview refactoring - (9bbfb60) - Mattia Righetti 16 | - infoview + bgbutton - (db8b862) - Mattia Righetti 17 | - switch to #Preview - (b4f470e) - Mattia Righetti 18 | 19 | - - - 20 | 21 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). 22 | -------------------------------------------------------------------------------- /HNReader.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3307109CEEE15ACDD7B88CFE /* HackerNewsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071E538EC434DF1A245518 /* HackerNewsClientTests.swift */; }; 11 | 330710FDACC9DEEBEC56011F /* ItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071EEBE46634E658582AE3 /* ItemTests.swift */; }; 12 | 330711A9216E762026AF98A0 /* +Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3307159309D438EFAA1259C7 /* +Date.swift */; }; 13 | 330713D3016ED410AFD53FDF /* ItemListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3307147A30B9051D95FFFB20 /* ItemListViewModel.swift */; }; 14 | 3307147AB95F03650FC40B97 /* ItemCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071CD8B451FE700B64F387 /* ItemCache.swift */; }; 15 | 330718D415D21296AA14E7CA /* HackerNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071291447141A6D31E671B /* HackerNewsTests.swift */; }; 16 | 330719203034BDB177F28C41 /* +DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071B0E5439D8D207CB68F4 /* +DateTests.swift */; }; 17 | 33071F1C64D4742E1F947FAA /* ItemDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071D0E5913DB91DDDBDADB /* ItemDownloader.swift */; }; 18 | 5F109D592AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */; }; 19 | 5F109D5F2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5F109D5B2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf */; }; 20 | 5F109D602AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5F109D5C2AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf */; }; 21 | 5F109D612AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5F109D5D2AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf */; }; 22 | 5F109D622AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5F109D5E2AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf */; }; 23 | 5FCE20A72AF7B25A00BF4097 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCE20A62AF7B25A00BF4097 /* InfoView.swift */; }; 24 | 5FCE20A92AF7B47B00BF4097 /* BgButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCE20A82AF7B47B00BF4097 /* BgButton.swift */; }; 25 | 5FCE20AB2AF7B8EE00BF4097 /* +View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCE20AA2AF7B8EE00BF4097 /* +View.swift */; }; 26 | C93F99B6267554F00046F870 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B5267554F00046F870 /* ItemCell.swift */; }; 27 | C93F99B8267557FC0046F870 /* ItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B7267557FC0046F870 /* ItemList.swift */; }; 28 | C93F99BA267580CE0046F870 /* HTMLText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B9267580CE0046F870 /* HTMLText.swift */; }; 29 | C9D0937726741BBE002CC786 /* HNReaderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0937626741BBE002CC786 /* HNReaderApp.swift */; }; 30 | C9D0937926741BBE002CC786 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0937826741BBE002CC786 /* HomeView.swift */; }; 31 | C9D0937B26741BBF002CC786 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9D0937A26741BBF002CC786 /* Assets.xcassets */; }; 32 | C9D0937E26741BBF002CC786 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9D0937D26741BBF002CC786 /* Preview Assets.xcassets */; }; 33 | C9D0938026741BBF002CC786 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0937F26741BBF002CC786 /* Persistence.swift */; }; 34 | C9D093AC26741C25002CC786 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D093AB26741C25002CC786 /* Item.swift */; }; 35 | C9E9BCFD2674C80E001B4E19 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E9BCFC2674C80E001B4E19 /* AppState.swift */; }; 36 | C9E9BCFF2674CB6C001B4E19 /* HackerNewsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E9BCFE2674CB6C001B4E19 /* HackerNewsClient.swift */; }; 37 | C9E9BD012674D007001B4E19 /* HackerNews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E9BD002674D007001B4E19 /* HackerNews.swift */; }; 38 | C9E9BD032674D095001B4E19 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E9BD022674D095001B4E19 /* User.swift */; }; 39 | /* End PBXBuildFile section */ 40 | 41 | /* Begin PBXContainerItemProxy section */ 42 | C9D0938B26741BC0002CC786 /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = C9D0936B26741BBE002CC786 /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = C9D0937226741BBE002CC786; 47 | remoteInfo = HNReader; 48 | }; 49 | /* End PBXContainerItemProxy section */ 50 | 51 | /* Begin PBXFileReference section */ 52 | 33071291447141A6D31E671B /* HackerNewsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HackerNewsTests.swift; sourceTree = ""; }; 53 | 3307147A30B9051D95FFFB20 /* ItemListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListViewModel.swift; sourceTree = ""; }; 54 | 3307159309D438EFAA1259C7 /* +Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "+Date.swift"; sourceTree = ""; }; 55 | 33071B0E5439D8D207CB68F4 /* +DateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "+DateTests.swift"; sourceTree = ""; }; 56 | 33071CD8B451FE700B64F387 /* ItemCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCache.swift; sourceTree = ""; }; 57 | 33071D0E5913DB91DDDBDADB /* ItemDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemDownloader.swift; sourceTree = ""; }; 58 | 33071E538EC434DF1A245518 /* HackerNewsClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HackerNewsClientTests.swift; sourceTree = ""; }; 59 | 33071EEBE46634E658582AE3 /* ItemTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemTests.swift; sourceTree = ""; }; 60 | 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalRedactedModifier.swift; sourceTree = ""; }; 61 | 5F109D5B2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSerif-Light.ttf"; sourceTree = ""; }; 62 | 5F109D5C2AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSerif-Bold.ttf"; sourceTree = ""; }; 63 | 5F109D5D2AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSerif-Regular.ttf"; sourceTree = ""; }; 64 | 5F109D5E2AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSerif-SemiBold.ttf"; sourceTree = ""; }; 65 | 5FCE20A62AF7B25A00BF4097 /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; 66 | 5FCE20A82AF7B47B00BF4097 /* BgButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BgButton.swift; sourceTree = ""; }; 67 | 5FCE20AA2AF7B8EE00BF4097 /* +View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "+View.swift"; sourceTree = ""; }; 68 | C93F99B5267554F00046F870 /* ItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCell.swift; sourceTree = ""; }; 69 | C93F99B7267557FC0046F870 /* ItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemList.swift; sourceTree = ""; }; 70 | C93F99B9267580CE0046F870 /* HTMLText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLText.swift; sourceTree = ""; }; 71 | C9D0937326741BBE002CC786 /* HNReader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HNReader.app; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | C9D0937626741BBE002CC786 /* HNReaderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HNReaderApp.swift; sourceTree = ""; }; 73 | C9D0937826741BBE002CC786 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 74 | C9D0937A26741BBF002CC786 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 75 | C9D0937D26741BBF002CC786 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 76 | C9D0937F26741BBF002CC786 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; 77 | C9D0938426741BBF002CC786 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 78 | C9D0938526741BBF002CC786 /* HNReader.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HNReader.entitlements; sourceTree = ""; }; 79 | C9D0938A26741BC0002CC786 /* HNReaderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HNReaderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | C9D0939026741BC0002CC786 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 81 | C9D093AB26741C25002CC786 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 82 | C9E9BCFC2674C80E001B4E19 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; 83 | C9E9BCFE2674CB6C001B4E19 /* HackerNewsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackerNewsClient.swift; sourceTree = ""; }; 84 | C9E9BD002674D007001B4E19 /* HackerNews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackerNews.swift; sourceTree = ""; }; 85 | C9E9BD022674D095001B4E19 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFrameworksBuildPhase section */ 89 | C9D0937026741BBE002CC786 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | C9D0938726741BC0002CC786 /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | /* End PBXFrameworksBuildPhase section */ 104 | 105 | /* Begin PBXGroup section */ 106 | 3307153CA4CBEF847DCF967F /* Utils */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 3307159309D438EFAA1259C7 /* +Date.swift */, 110 | 5FCE20AA2AF7B8EE00BF4097 /* +View.swift */, 111 | ); 112 | path = Utils; 113 | sourceTree = ""; 114 | }; 115 | 330716F7289F75F50DB30273 /* UtilsTests */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 33071B0E5439D8D207CB68F4 /* +DateTests.swift */, 119 | ); 120 | path = UtilsTests; 121 | sourceTree = ""; 122 | }; 123 | 33071AE1286F3A139C23E0A9 /* HNClientTests */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 33071E538EC434DF1A245518 /* HackerNewsClientTests.swift */, 127 | 33071291447141A6D31E671B /* HackerNewsTests.swift */, 128 | ); 129 | path = HNClientTests; 130 | sourceTree = ""; 131 | }; 132 | 33071C911DD94AB17E409FD7 /* ModelTests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 33071EEBE46634E658582AE3 /* ItemTests.swift */, 136 | ); 137 | path = ModelTests; 138 | sourceTree = ""; 139 | }; 140 | 5F96E4AB2AF70C2C00593AD6 /* fonts */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 5F109D5C2AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf */, 144 | 5F109D5B2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf */, 145 | 5F109D5D2AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf */, 146 | 5F109D5E2AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf */, 147 | ); 148 | path = fonts; 149 | sourceTree = ""; 150 | }; 151 | C9926691267588B80035A88F /* Components */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | C93F99B9267580CE0046F870 /* HTMLText.swift */, 155 | 5FCE20A82AF7B47B00BF4097 /* BgButton.swift */, 156 | ); 157 | path = Components; 158 | sourceTree = ""; 159 | }; 160 | C9D0936A26741BBE002CC786 = { 161 | isa = PBXGroup; 162 | children = ( 163 | C9D0937526741BBE002CC786 /* HNReader */, 164 | C9D0938D26741BC0002CC786 /* HNReaderTests */, 165 | C9D0937426741BBE002CC786 /* Products */, 166 | ); 167 | sourceTree = ""; 168 | }; 169 | C9D0937426741BBE002CC786 /* Products */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | C9D0937326741BBE002CC786 /* HNReader.app */, 173 | C9D0938A26741BC0002CC786 /* HNReaderTests.xctest */, 174 | ); 175 | name = Products; 176 | sourceTree = ""; 177 | }; 178 | C9D0937526741BBE002CC786 /* HNReader */ = { 179 | isa = PBXGroup; 180 | children = ( 181 | 5F96E4AB2AF70C2C00593AD6 /* fonts */, 182 | C9D093AA26741BFD002CC786 /* HNClient */, 183 | C9D093A926741BF6002CC786 /* ViewModel */, 184 | C9D093A826741BF0002CC786 /* Model */, 185 | C9D093A726741BE1002CC786 /* View */, 186 | C9D0937626741BBE002CC786 /* HNReaderApp.swift */, 187 | C9D0937A26741BBF002CC786 /* Assets.xcassets */, 188 | C9D0937F26741BBF002CC786 /* Persistence.swift */, 189 | C9D0938426741BBF002CC786 /* Info.plist */, 190 | C9D0938526741BBF002CC786 /* HNReader.entitlements */, 191 | C9D0937C26741BBF002CC786 /* Preview Content */, 192 | 3307153CA4CBEF847DCF967F /* Utils */, 193 | ); 194 | path = HNReader; 195 | sourceTree = ""; 196 | }; 197 | C9D0937C26741BBF002CC786 /* Preview Content */ = { 198 | isa = PBXGroup; 199 | children = ( 200 | C9D0937D26741BBF002CC786 /* Preview Assets.xcassets */, 201 | ); 202 | path = "Preview Content"; 203 | sourceTree = ""; 204 | }; 205 | C9D0938D26741BC0002CC786 /* HNReaderTests */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | C9D0939026741BC0002CC786 /* Info.plist */, 209 | 33071AE1286F3A139C23E0A9 /* HNClientTests */, 210 | 330716F7289F75F50DB30273 /* UtilsTests */, 211 | 33071C911DD94AB17E409FD7 /* ModelTests */, 212 | ); 213 | path = HNReaderTests; 214 | sourceTree = ""; 215 | }; 216 | C9D093A726741BE1002CC786 /* View */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | C9D0937826741BBE002CC786 /* HomeView.swift */, 220 | C93F99B5267554F00046F870 /* ItemCell.swift */, 221 | C93F99B7267557FC0046F870 /* ItemList.swift */, 222 | C9926691267588B80035A88F /* Components */, 223 | 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */, 224 | 5FCE20A62AF7B25A00BF4097 /* InfoView.swift */, 225 | ); 226 | path = View; 227 | sourceTree = ""; 228 | }; 229 | C9D093A826741BF0002CC786 /* Model */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | C9D093AB26741C25002CC786 /* Item.swift */, 233 | C9E9BD022674D095001B4E19 /* User.swift */, 234 | ); 235 | path = Model; 236 | sourceTree = ""; 237 | }; 238 | C9D093A926741BF6002CC786 /* ViewModel */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | C9E9BCFC2674C80E001B4E19 /* AppState.swift */, 242 | 3307147A30B9051D95FFFB20 /* ItemListViewModel.swift */, 243 | ); 244 | path = ViewModel; 245 | sourceTree = ""; 246 | }; 247 | C9D093AA26741BFD002CC786 /* HNClient */ = { 248 | isa = PBXGroup; 249 | children = ( 250 | C9E9BCFE2674CB6C001B4E19 /* HackerNewsClient.swift */, 251 | C9E9BD002674D007001B4E19 /* HackerNews.swift */, 252 | 33071D0E5913DB91DDDBDADB /* ItemDownloader.swift */, 253 | 33071CD8B451FE700B64F387 /* ItemCache.swift */, 254 | ); 255 | path = HNClient; 256 | sourceTree = ""; 257 | }; 258 | /* End PBXGroup section */ 259 | 260 | /* Begin PBXNativeTarget section */ 261 | C9D0937226741BBE002CC786 /* HNReader */ = { 262 | isa = PBXNativeTarget; 263 | buildConfigurationList = C9D0939E26741BC0002CC786 /* Build configuration list for PBXNativeTarget "HNReader" */; 264 | buildPhases = ( 265 | C9D0936F26741BBE002CC786 /* Sources */, 266 | C9D0937026741BBE002CC786 /* Frameworks */, 267 | C9D0937126741BBE002CC786 /* Resources */, 268 | ); 269 | buildRules = ( 270 | ); 271 | dependencies = ( 272 | ); 273 | name = HNReader; 274 | packageProductDependencies = ( 275 | ); 276 | productName = HNReader; 277 | productReference = C9D0937326741BBE002CC786 /* HNReader.app */; 278 | productType = "com.apple.product-type.application"; 279 | }; 280 | C9D0938926741BC0002CC786 /* HNReaderTests */ = { 281 | isa = PBXNativeTarget; 282 | buildConfigurationList = C9D093A126741BC0002CC786 /* Build configuration list for PBXNativeTarget "HNReaderTests" */; 283 | buildPhases = ( 284 | C9D0938626741BC0002CC786 /* Sources */, 285 | C9D0938726741BC0002CC786 /* Frameworks */, 286 | C9D0938826741BC0002CC786 /* Resources */, 287 | ); 288 | buildRules = ( 289 | ); 290 | dependencies = ( 291 | C9D0938C26741BC0002CC786 /* PBXTargetDependency */, 292 | ); 293 | name = HNReaderTests; 294 | productName = HNReaderTests; 295 | productReference = C9D0938A26741BC0002CC786 /* HNReaderTests.xctest */; 296 | productType = "com.apple.product-type.bundle.unit-test"; 297 | }; 298 | /* End PBXNativeTarget section */ 299 | 300 | /* Begin PBXProject section */ 301 | C9D0936B26741BBE002CC786 /* Project object */ = { 302 | isa = PBXProject; 303 | attributes = { 304 | BuildIndependentTargetsInParallel = YES; 305 | LastSwiftUpdateCheck = 1250; 306 | LastUpgradeCheck = 1510; 307 | TargetAttributes = { 308 | C9D0937226741BBE002CC786 = { 309 | CreatedOnToolsVersion = 12.5; 310 | }; 311 | C9D0938926741BC0002CC786 = { 312 | CreatedOnToolsVersion = 12.5; 313 | TestTargetID = C9D0937226741BBE002CC786; 314 | }; 315 | }; 316 | }; 317 | buildConfigurationList = C9D0936E26741BBE002CC786 /* Build configuration list for PBXProject "HNReader" */; 318 | compatibilityVersion = "Xcode 9.3"; 319 | developmentRegion = en; 320 | hasScannedForEncodings = 0; 321 | knownRegions = ( 322 | en, 323 | Base, 324 | ); 325 | mainGroup = C9D0936A26741BBE002CC786; 326 | packageReferences = ( 327 | ); 328 | productRefGroup = C9D0937426741BBE002CC786 /* Products */; 329 | projectDirPath = ""; 330 | projectRoot = ""; 331 | targets = ( 332 | C9D0937226741BBE002CC786 /* HNReader */, 333 | C9D0938926741BC0002CC786 /* HNReaderTests */, 334 | ); 335 | }; 336 | /* End PBXProject section */ 337 | 338 | /* Begin PBXResourcesBuildPhase section */ 339 | C9D0937126741BBE002CC786 /* Resources */ = { 340 | isa = PBXResourcesBuildPhase; 341 | buildActionMask = 2147483647; 342 | files = ( 343 | C9D0937E26741BBF002CC786 /* Preview Assets.xcassets in Resources */, 344 | 5F109D612AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf in Resources */, 345 | 5F109D602AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf in Resources */, 346 | 5F109D622AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf in Resources */, 347 | C9D0937B26741BBF002CC786 /* Assets.xcassets in Resources */, 348 | 5F109D5F2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf in Resources */, 349 | ); 350 | runOnlyForDeploymentPostprocessing = 0; 351 | }; 352 | C9D0938826741BC0002CC786 /* Resources */ = { 353 | isa = PBXResourcesBuildPhase; 354 | buildActionMask = 2147483647; 355 | files = ( 356 | ); 357 | runOnlyForDeploymentPostprocessing = 0; 358 | }; 359 | /* End PBXResourcesBuildPhase section */ 360 | 361 | /* Begin PBXSourcesBuildPhase section */ 362 | C9D0936F26741BBE002CC786 /* Sources */ = { 363 | isa = PBXSourcesBuildPhase; 364 | buildActionMask = 2147483647; 365 | files = ( 366 | C9D093AC26741C25002CC786 /* Item.swift in Sources */, 367 | C9D0938026741BBF002CC786 /* Persistence.swift in Sources */, 368 | C9D0937926741BBE002CC786 /* HomeView.swift in Sources */, 369 | C93F99B6267554F00046F870 /* ItemCell.swift in Sources */, 370 | 5FCE20A92AF7B47B00BF4097 /* BgButton.swift in Sources */, 371 | 5FCE20A72AF7B25A00BF4097 /* InfoView.swift in Sources */, 372 | 5F109D592AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift in Sources */, 373 | C93F99B8267557FC0046F870 /* ItemList.swift in Sources */, 374 | C9E9BCFD2674C80E001B4E19 /* AppState.swift in Sources */, 375 | C9E9BD032674D095001B4E19 /* User.swift in Sources */, 376 | C93F99BA267580CE0046F870 /* HTMLText.swift in Sources */, 377 | C9E9BCFF2674CB6C001B4E19 /* HackerNewsClient.swift in Sources */, 378 | C9E9BD012674D007001B4E19 /* HackerNews.swift in Sources */, 379 | C9D0937726741BBE002CC786 /* HNReaderApp.swift in Sources */, 380 | 330713D3016ED410AFD53FDF /* ItemListViewModel.swift in Sources */, 381 | 330711A9216E762026AF98A0 /* +Date.swift in Sources */, 382 | 33071F1C64D4742E1F947FAA /* ItemDownloader.swift in Sources */, 383 | 5FCE20AB2AF7B8EE00BF4097 /* +View.swift in Sources */, 384 | 3307147AB95F03650FC40B97 /* ItemCache.swift in Sources */, 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | }; 388 | C9D0938626741BC0002CC786 /* Sources */ = { 389 | isa = PBXSourcesBuildPhase; 390 | buildActionMask = 2147483647; 391 | files = ( 392 | 3307109CEEE15ACDD7B88CFE /* HackerNewsClientTests.swift in Sources */, 393 | 330718D415D21296AA14E7CA /* HackerNewsTests.swift in Sources */, 394 | 330719203034BDB177F28C41 /* +DateTests.swift in Sources */, 395 | 330710FDACC9DEEBEC56011F /* ItemTests.swift in Sources */, 396 | ); 397 | runOnlyForDeploymentPostprocessing = 0; 398 | }; 399 | /* End PBXSourcesBuildPhase section */ 400 | 401 | /* Begin PBXTargetDependency section */ 402 | C9D0938C26741BC0002CC786 /* PBXTargetDependency */ = { 403 | isa = PBXTargetDependency; 404 | target = C9D0937226741BBE002CC786 /* HNReader */; 405 | targetProxy = C9D0938B26741BC0002CC786 /* PBXContainerItemProxy */; 406 | }; 407 | /* End PBXTargetDependency section */ 408 | 409 | /* Begin XCBuildConfiguration section */ 410 | C9D0939C26741BC0002CC786 /* Debug */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | ALWAYS_SEARCH_USER_PATHS = NO; 414 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 415 | CLANG_ANALYZER_NONNULL = YES; 416 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 417 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 418 | CLANG_CXX_LIBRARY = "libc++"; 419 | CLANG_ENABLE_MODULES = YES; 420 | CLANG_ENABLE_OBJC_ARC = YES; 421 | CLANG_ENABLE_OBJC_WEAK = YES; 422 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 423 | CLANG_WARN_BOOL_CONVERSION = YES; 424 | CLANG_WARN_COMMA = YES; 425 | CLANG_WARN_CONSTANT_CONVERSION = YES; 426 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 427 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 428 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 429 | CLANG_WARN_EMPTY_BODY = YES; 430 | CLANG_WARN_ENUM_CONVERSION = YES; 431 | CLANG_WARN_INFINITE_RECURSION = YES; 432 | CLANG_WARN_INT_CONVERSION = YES; 433 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 434 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 435 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 436 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 437 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 438 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 439 | CLANG_WARN_STRICT_PROTOTYPES = YES; 440 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 441 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 442 | CLANG_WARN_UNREACHABLE_CODE = YES; 443 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 444 | COPY_PHASE_STRIP = NO; 445 | DEAD_CODE_STRIPPING = YES; 446 | DEBUG_INFORMATION_FORMAT = dwarf; 447 | ENABLE_STRICT_OBJC_MSGSEND = YES; 448 | ENABLE_TESTABILITY = YES; 449 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 450 | GCC_C_LANGUAGE_STANDARD = gnu11; 451 | GCC_DYNAMIC_NO_PIC = NO; 452 | GCC_NO_COMMON_BLOCKS = YES; 453 | GCC_OPTIMIZATION_LEVEL = 0; 454 | GCC_PREPROCESSOR_DEFINITIONS = ( 455 | "DEBUG=1", 456 | "$(inherited)", 457 | ); 458 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 459 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 460 | GCC_WARN_UNDECLARED_SELECTOR = YES; 461 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 462 | GCC_WARN_UNUSED_FUNCTION = YES; 463 | GCC_WARN_UNUSED_VARIABLE = YES; 464 | MACOSX_DEPLOYMENT_TARGET = 14.0; 465 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 466 | MTL_FAST_MATH = YES; 467 | ONLY_ACTIVE_ARCH = YES; 468 | SDKROOT = macosx; 469 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 470 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 471 | }; 472 | name = Debug; 473 | }; 474 | C9D0939D26741BC0002CC786 /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ALWAYS_SEARCH_USER_PATHS = NO; 478 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 479 | CLANG_ANALYZER_NONNULL = YES; 480 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 481 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 482 | CLANG_CXX_LIBRARY = "libc++"; 483 | CLANG_ENABLE_MODULES = YES; 484 | CLANG_ENABLE_OBJC_ARC = YES; 485 | CLANG_ENABLE_OBJC_WEAK = YES; 486 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 487 | CLANG_WARN_BOOL_CONVERSION = YES; 488 | CLANG_WARN_COMMA = YES; 489 | CLANG_WARN_CONSTANT_CONVERSION = YES; 490 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 491 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 492 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 493 | CLANG_WARN_EMPTY_BODY = YES; 494 | CLANG_WARN_ENUM_CONVERSION = YES; 495 | CLANG_WARN_INFINITE_RECURSION = YES; 496 | CLANG_WARN_INT_CONVERSION = YES; 497 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 498 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 499 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 500 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 501 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 502 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 503 | CLANG_WARN_STRICT_PROTOTYPES = YES; 504 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 505 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 506 | CLANG_WARN_UNREACHABLE_CODE = YES; 507 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 508 | COPY_PHASE_STRIP = NO; 509 | DEAD_CODE_STRIPPING = YES; 510 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 511 | ENABLE_NS_ASSERTIONS = NO; 512 | ENABLE_STRICT_OBJC_MSGSEND = YES; 513 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 514 | GCC_C_LANGUAGE_STANDARD = gnu11; 515 | GCC_NO_COMMON_BLOCKS = YES; 516 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 517 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 518 | GCC_WARN_UNDECLARED_SELECTOR = YES; 519 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 520 | GCC_WARN_UNUSED_FUNCTION = YES; 521 | GCC_WARN_UNUSED_VARIABLE = YES; 522 | MACOSX_DEPLOYMENT_TARGET = 14.0; 523 | MTL_ENABLE_DEBUG_INFO = NO; 524 | MTL_FAST_MATH = YES; 525 | SDKROOT = macosx; 526 | SWIFT_COMPILATION_MODE = wholemodule; 527 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 528 | }; 529 | name = Release; 530 | }; 531 | C9D0939F26741BC0002CC786 /* Debug */ = { 532 | isa = XCBuildConfiguration; 533 | buildSettings = { 534 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 535 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 536 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 537 | CODE_SIGN_ENTITLEMENTS = HNReader/HNReader.entitlements; 538 | CODE_SIGN_IDENTITY = "Apple Development"; 539 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 540 | CODE_SIGN_STYLE = Automatic; 541 | COMBINE_HIDPI_IMAGES = YES; 542 | CURRENT_PROJECT_VERSION = 1.3; 543 | DEAD_CODE_STRIPPING = YES; 544 | DEVELOPMENT_ASSET_PATHS = "\"HNReader/Preview Content\""; 545 | DEVELOPMENT_TEAM = H89RFW5UZ6; 546 | ENABLE_HARDENED_RUNTIME = YES; 547 | ENABLE_PREVIEWS = YES; 548 | INFOPLIST_FILE = HNReader/Info.plist; 549 | INFOPLIST_KEY_CFBundleDisplayName = HNReader; 550 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news"; 551 | LD_RUNPATH_SEARCH_PATHS = ( 552 | "$(inherited)", 553 | "@executable_path/../Frameworks", 554 | ); 555 | MACOSX_DEPLOYMENT_TARGET = 14.0; 556 | MARKETING_VERSION = 1.3; 557 | PRODUCT_BUNDLE_IDENTIFIER = com.mattrighetti.HNReader; 558 | PRODUCT_NAME = "$(TARGET_NAME)"; 559 | PROVISIONING_PROFILE_SPECIFIER = ""; 560 | SWIFT_VERSION = 5.0; 561 | }; 562 | name = Debug; 563 | }; 564 | C9D093A026741BC0002CC786 /* Release */ = { 565 | isa = XCBuildConfiguration; 566 | buildSettings = { 567 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 568 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 569 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 570 | CODE_SIGN_ENTITLEMENTS = HNReader/HNReader.entitlements; 571 | CODE_SIGN_IDENTITY = "Apple Development"; 572 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 573 | CODE_SIGN_STYLE = Automatic; 574 | COMBINE_HIDPI_IMAGES = YES; 575 | CURRENT_PROJECT_VERSION = 1.3; 576 | DEAD_CODE_STRIPPING = YES; 577 | DEVELOPMENT_ASSET_PATHS = "\"HNReader/Preview Content\""; 578 | DEVELOPMENT_TEAM = H89RFW5UZ6; 579 | ENABLE_HARDENED_RUNTIME = YES; 580 | ENABLE_PREVIEWS = YES; 581 | INFOPLIST_FILE = HNReader/Info.plist; 582 | INFOPLIST_KEY_CFBundleDisplayName = HNReader; 583 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news"; 584 | LD_RUNPATH_SEARCH_PATHS = ( 585 | "$(inherited)", 586 | "@executable_path/../Frameworks", 587 | ); 588 | MACOSX_DEPLOYMENT_TARGET = 14.0; 589 | MARKETING_VERSION = 1.3; 590 | PRODUCT_BUNDLE_IDENTIFIER = com.mattrighetti.HNReader; 591 | PRODUCT_NAME = "$(TARGET_NAME)"; 592 | PROVISIONING_PROFILE_SPECIFIER = ""; 593 | SWIFT_VERSION = 5.0; 594 | }; 595 | name = Release; 596 | }; 597 | C9D093A226741BC0002CC786 /* Debug */ = { 598 | isa = XCBuildConfiguration; 599 | buildSettings = { 600 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 601 | BUNDLE_LOADER = "$(TEST_HOST)"; 602 | CODE_SIGN_STYLE = Automatic; 603 | COMBINE_HIDPI_IMAGES = YES; 604 | DEAD_CODE_STRIPPING = YES; 605 | DEVELOPMENT_TEAM = H89RFW5UZ6; 606 | INFOPLIST_FILE = HNReaderTests/Info.plist; 607 | LD_RUNPATH_SEARCH_PATHS = ( 608 | "$(inherited)", 609 | "@executable_path/../Frameworks", 610 | "@loader_path/../Frameworks", 611 | ); 612 | MACOSX_DEPLOYMENT_TARGET = 14.0; 613 | PRODUCT_BUNDLE_IDENTIFIER = com.mattrighetti.HNReaderTests; 614 | PRODUCT_NAME = "$(TARGET_NAME)"; 615 | SWIFT_VERSION = 5.0; 616 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HNReader.app/Contents/MacOS/HNReader"; 617 | }; 618 | name = Debug; 619 | }; 620 | C9D093A326741BC0002CC786 /* Release */ = { 621 | isa = XCBuildConfiguration; 622 | buildSettings = { 623 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 624 | BUNDLE_LOADER = "$(TEST_HOST)"; 625 | CODE_SIGN_STYLE = Automatic; 626 | COMBINE_HIDPI_IMAGES = YES; 627 | DEAD_CODE_STRIPPING = YES; 628 | DEVELOPMENT_TEAM = H89RFW5UZ6; 629 | INFOPLIST_FILE = HNReaderTests/Info.plist; 630 | LD_RUNPATH_SEARCH_PATHS = ( 631 | "$(inherited)", 632 | "@executable_path/../Frameworks", 633 | "@loader_path/../Frameworks", 634 | ); 635 | MACOSX_DEPLOYMENT_TARGET = 14.0; 636 | PRODUCT_BUNDLE_IDENTIFIER = com.mattrighetti.HNReaderTests; 637 | PRODUCT_NAME = "$(TARGET_NAME)"; 638 | SWIFT_VERSION = 5.0; 639 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HNReader.app/Contents/MacOS/HNReader"; 640 | }; 641 | name = Release; 642 | }; 643 | /* End XCBuildConfiguration section */ 644 | 645 | /* Begin XCConfigurationList section */ 646 | C9D0936E26741BBE002CC786 /* Build configuration list for PBXProject "HNReader" */ = { 647 | isa = XCConfigurationList; 648 | buildConfigurations = ( 649 | C9D0939C26741BC0002CC786 /* Debug */, 650 | C9D0939D26741BC0002CC786 /* Release */, 651 | ); 652 | defaultConfigurationIsVisible = 0; 653 | defaultConfigurationName = Release; 654 | }; 655 | C9D0939E26741BC0002CC786 /* Build configuration list for PBXNativeTarget "HNReader" */ = { 656 | isa = XCConfigurationList; 657 | buildConfigurations = ( 658 | C9D0939F26741BC0002CC786 /* Debug */, 659 | C9D093A026741BC0002CC786 /* Release */, 660 | ); 661 | defaultConfigurationIsVisible = 0; 662 | defaultConfigurationName = Release; 663 | }; 664 | C9D093A126741BC0002CC786 /* Build configuration list for PBXNativeTarget "HNReaderTests" */ = { 665 | isa = XCConfigurationList; 666 | buildConfigurations = ( 667 | C9D093A226741BC0002CC786 /* Debug */, 668 | C9D093A326741BC0002CC786 /* Release */, 669 | ); 670 | defaultConfigurationIsVisible = 0; 671 | defaultConfigurationName = Release; 672 | }; 673 | /* End XCConfigurationList section */ 674 | }; 675 | rootObject = C9D0936B26741BBE002CC786 /* Project object */; 676 | } 677 | -------------------------------------------------------------------------------- /HNReader.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HNReader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /HNReader.xcodeproj/project.xcworkspace/xcuserdata/mattrighetti.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader.xcodeproj/project.xcworkspace/xcuserdata/mattrighetti.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /HNReader.xcodeproj/xcuserdata/mattrighetti.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | HNReader.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | }, 6 | { 7 | "appearances" : [ 8 | { 9 | "appearance" : "luminosity", 10 | "value" : "dark" 11 | } 12 | ], 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /HNReader/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HNReader/HNClient/HackerNews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HackerNes.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// HackerNews endpoint data structure 11 | struct HackerNews { 12 | public static let endpoint = "https://hacker-news.firebaseio.com/v0" 13 | 14 | /// HackerNews REST API methods 15 | enum API { 16 | /// HackerNews User REST API methods 17 | enum User { 18 | case id(String) 19 | 20 | public var urlString: String { 21 | switch self { 22 | case .id(let userId): 23 | return "\(HackerNews.endpoint)/user/\(userId).json" 24 | } 25 | } 26 | } 27 | 28 | /// HackerNews Stories REST API methods 29 | enum Stories: String { 30 | case top 31 | case new 32 | case best 33 | case ask 34 | case job 35 | case show 36 | 37 | public var urlString: String { 38 | "\(HackerNews.endpoint)/\(self.rawValue)stories.json" 39 | } 40 | } 41 | 42 | /// HackerNews Item REST API methods 43 | enum Item { 44 | case id(Int) 45 | 46 | public var urlString: String { 47 | switch self { 48 | case .id(let storyId): 49 | return "\(HackerNews.endpoint)/item/\(storyId).json" 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /HNReader/HNClient/HackerNewsClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HackerNewsClient.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | /// HackerNews Client that exposes methods to get users, items and stories from https://news.ycombinator.com 12 | class HackerNewsClient { 13 | public static let shared: HackerNewsClient = HackerNewsClient() 14 | private let session: URLSession = URLSession.shared 15 | private let decoder: JSONDecoder = JSONDecoder() 16 | private var subscriptions = Set() 17 | 18 | /// Retrieves user from HackerNews 19 | public func getUser(withId id: String) -> AnyPublisher { 20 | let url = URL(string: HackerNews.API.User.id(id).urlString)! 21 | return session 22 | .dataTaskPublisher(for: url) 23 | .retry(3) 24 | .map(\.data) 25 | .decode(type: User.self, decoder: decoder) 26 | .receive(on: DispatchQueue.main) 27 | .eraseToAnyPublisher() 28 | } 29 | 30 | /// Retrieves item from HackerNews 31 | public func getItem(withId id: Int) -> AnyPublisher { 32 | let url = URL(string: HackerNews.API.Item.id(id).urlString)! 33 | return session 34 | .dataTaskPublisher(for: url) 35 | .retry(3) 36 | .map(\.data) 37 | .decode(type: Item.self, decoder: decoder) 38 | .eraseToAnyPublisher() 39 | } 40 | 41 | /// Retrieves array of items from HackerNews 42 | /// 43 | /// You can specify which kind of stories you would like to retrieve: 44 | /// - `top` 45 | /// - `best` 46 | /// - `new` 47 | public func getStoriesId(by api: HackerNews.API.Stories) -> AnyPublisher<[Int], Error> { 48 | let url = URL(string: api.urlString)! 49 | return session 50 | .dataTaskPublisher(for: url) 51 | .map(\.data) 52 | .decode(type: [Int].self, decoder: decoder) 53 | .eraseToAnyPublisher() 54 | } 55 | 56 | public func getStories(withIds ids: [Int]) -> AnyPublisher<[Item], Error> { 57 | ids.publisher 58 | .flatMap(getItem) 59 | .collect() 60 | .eraseToAnyPublisher() 61 | } 62 | 63 | public func getStories(by category: HackerNews.API.Stories, limit: Int? = 50) -> AnyPublisher<[Item], Error> { 64 | getStoriesId(by: category) 65 | .flatMap(getStories) 66 | .eraseToAnyPublisher() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /HNReader/HNClient/ItemCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Mattia Righetti on 13/06/21. 3 | // 4 | 5 | import Foundation 6 | 7 | class StructWrapper: NSObject { 8 | let value: T 9 | 10 | init(_ _struct: T) { 11 | value = _struct 12 | } 13 | } 14 | 15 | 16 | class ItemCache: NSCache> { 17 | static let shared = ItemCache() 18 | 19 | func cache(_ item: Item, for key: Int) { 20 | let keyString = NSString(format: "%d", key) 21 | let itemWrapper = StructWrapper(item) 22 | self.setObject(itemWrapper, forKey: keyString) 23 | } 24 | 25 | func getItem(for key: Int) -> Item? { 26 | let keyString = NSString(format: "%d", key) 27 | let itemWrapper = self.object(forKey: keyString) 28 | return itemWrapper?.value 29 | } 30 | } -------------------------------------------------------------------------------- /HNReader/HNClient/ItemDownloader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Mattia Righetti on 13/06/21. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol ItemDownloader { 8 | var cacheKey: Int { get } 9 | func downloadItem(completion: @escaping (Item?) -> Void) 10 | } 11 | 12 | class DefaultItemDownloader: ItemDownloader { 13 | let itemId: Int 14 | var cacheKey: Int { 15 | itemId 16 | } 17 | 18 | init(itemId: Int) { 19 | self.itemId = itemId 20 | } 21 | 22 | func downloadItem(completion: @escaping (Item?) -> ()) { 23 | guard let url = URL(string: HackerNews.API.Item.id(itemId).urlString) else { return } 24 | let task = URLSession.shared.dataTask(with: url) { (data, response, error) in 25 | if let data = data { 26 | var item: Item 27 | do { 28 | item = try JSONDecoder().decode(Item.self, from: data) 29 | completion(item) 30 | } catch { 31 | print("encountered error while downloading item") 32 | completion(nil) 33 | } 34 | } 35 | } 36 | task.resume() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /HNReader/HNReader.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /HNReader/HNReader.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /HNReader/HNReader.xcdatamodeld/HNReader.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /HNReader/HNReaderApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HNReaderApp.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct HNReaderApp: App { 12 | let persistenceController = PersistenceController.shared 13 | let appState = AppState() 14 | 15 | private var displayModeBind: Binding { 16 | Binding( 17 | get: { appState.getColorScheme() }, 18 | set: { 19 | appState.setColorScheme($0) 20 | displayMode = $0 21 | } 22 | ) 23 | } 24 | @State var displayMode: ColorScheme? 25 | 26 | var body: some Scene { 27 | WindowGroup { 28 | HomeView() 29 | .frame(minWidth: 800, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity) 30 | .onAppear { 31 | displayMode = appState.getColorScheme() 32 | } 33 | .preferredColorScheme(displayMode) 34 | .environmentObject(appState) 35 | 36 | } 37 | 38 | Settings { 39 | VStack { 40 | Form { 41 | Picker(selection: displayModeBind, label: Text("Theme")) { 42 | Text("Dark").tag(ColorScheme.dark) 43 | Text("Light").tag(ColorScheme.light) 44 | } 45 | .pickerStyle(SegmentedPickerStyle()) 46 | .frame(maxWidth: 200) 47 | } 48 | } 49 | .frame(minHeight: 100) 50 | .frame(minWidth: 300) 51 | .preferredColorScheme(displayMode) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /HNReader/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSApplicationCategoryType 22 | public.app-category.news 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | UIAppFonts 26 | 27 | IBMPlexSerif-Bold.ttf 28 | IBMPlexSerif-SemiBold.ttf 29 | IBMPlexSerif-Regular.ttf 30 | IBMPlexSerif-Light.ttf 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /HNReader/Model/Item.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Item.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Stories, comments, jobs, Ask HNs and even polls are items. They're identified by their ids, which are unique integers. 12 | 13 | For example, a story: https://hacker-news.firebaseio.com/v0/item/8863.json?print=pretty 14 | ```json 15 | { 16 | "by" : "dhouston", 17 | "descendants" : 71, 18 | "id" : 8863, 19 | "kids" : [ 8952, 9224, 8917, 8884, 8887, 8943, 8869, 8958, 9005, 9671, 8940, 9067, 8908, 9055, 8865, 8881, 8872, 8873, 8955, 10403, 8903, 8928, 9125, 8998, 8901, 8902, 8907, 8894, 8878, 8870, 8980, 8934, 8876 ], 20 | "score" : 111, 21 | "time" : 1175714200, 22 | "title" : "My YC app: Dropbox - Throw away your USB drive", 23 | "type" : "story", 24 | "url" : "http://www.getdropbox.com/u/2/screencast.html" 25 | } 26 | ``` 27 | */ 28 | public struct Item: Decodable { 29 | public let id: Int 30 | public let deleted: Bool? 31 | public let type: ItemType? 32 | public let by: String? 33 | public let time: Int? 34 | public let text: String? 35 | public let dead: Bool? 36 | public let parent: Int? 37 | public let poll: Bool? 38 | public let kids: [Int]? 39 | public let url: String? 40 | public let score: Int? 41 | public let title: String? 42 | public let parts: Int? 43 | public let descendants: Int? 44 | 45 | public var urlHost: String? { 46 | if let url = url { 47 | var hostString = URL(string: url)!.host! 48 | if hostString.contains("www.") { 49 | hostString.removeFirst(4) 50 | } 51 | return hostString 52 | } else { 53 | return nil 54 | } 55 | } 56 | 57 | public var scoreString: String? { 58 | guard let score = score else { return nil } 59 | return "\(score)" 60 | } 61 | 62 | public var timeStringRepresentation: String? { 63 | Date().timeElapsedStringRepresentation(since: Date(timeIntervalSince1970: TimeInterval(time!))) 64 | } 65 | } 66 | 67 | public enum ItemType: String, Decodable { 68 | case poll 69 | case job 70 | case story 71 | case comment 72 | case pollopt 73 | } 74 | -------------------------------------------------------------------------------- /HNReader/Model/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct User: Decodable { 11 | public let id: String 12 | public let created: Int 13 | public let karma: Int 14 | public let about: String? 15 | public let submitted: [Int]? 16 | } 17 | -------------------------------------------------------------------------------- /HNReader/Persistence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistence.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import CoreData 9 | 10 | struct PersistenceController { 11 | static let shared = PersistenceController() 12 | 13 | static var preview: PersistenceController = { 14 | let result = PersistenceController(inMemory: true) 15 | let viewContext = result.container.viewContext 16 | do { 17 | try viewContext.save() 18 | } catch { 19 | // Replace this implementation with code to handle the error appropriately. 20 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 21 | let nsError = error as NSError 22 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 23 | } 24 | return result 25 | }() 26 | 27 | let container: NSPersistentContainer 28 | 29 | init(inMemory: Bool = false) { 30 | container = NSPersistentContainer(name: "HNReader") 31 | if inMemory { 32 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") 33 | } 34 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 35 | if let error = error as NSError? { 36 | // Replace this implementation with code to handle the error appropriately. 37 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 38 | 39 | /* 40 | Typical reasons for an error here include: 41 | * The parent directory does not exist, cannot be created, or disallows writing. 42 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 43 | * The device is out of space. 44 | * The store could not be migrated to the current model version. 45 | Check the error message to determine what the actual problem was. 46 | */ 47 | fatalError("Unresolved error \(error), \(error.userInfo)") 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /HNReader/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HNReader/Utils/+Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Mattia Righetti on 13/06/21. 3 | // 4 | 5 | import Foundation 6 | 7 | extension Date { 8 | public func timeElapsedStringRepresentation(since: Date) -> String { 9 | let elapsedTime = timeIntervalSince(since) 10 | let years = Int(floor(elapsedTime / 365 / 24 / 60 / 60)) 11 | let days = Int(floor(elapsedTime / 24 / 60 / 60)) 12 | let hours = Int(floor(elapsedTime / 60 / 60)) 13 | let minutes = Int(floor(elapsedTime / 60)) 14 | 15 | if years >= 1 { 16 | return "\(years)y" 17 | } else if days >= 1 { 18 | return "\(days)d" 19 | } else if hours >= 1 { 20 | return "\(hours)h" 21 | } else { 22 | return "\(minutes)m" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /HNReader/Utils/+View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +View.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 05/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | @discardableResult 12 | func openInWindow(title: String, sender: Any?) -> NSWindow { 13 | let controller = NSHostingController(rootView: self) 14 | let win = NSWindow(contentViewController: controller) 15 | win.contentViewController = controller 16 | win.title = title 17 | win.makeKeyAndOrderFront(sender) 18 | return win 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /HNReader/View/Components/BgButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BgButton.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 05/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BgButton: View { 11 | var text: String? = nil 12 | var icon: String 13 | var disabled: Bool = false 14 | var minSize: CGSize 15 | var onHover: ((Bool) -> Void)? = nil 16 | var action: (() -> ())? = nil 17 | 18 | var body: some View { 19 | ZStack { 20 | RoundedRectangle(cornerRadius: 15) 21 | .foregroundStyle(Color.gray.opacity(0.1)) 22 | .frame(width: minSize.width, height: minSize.height) 23 | 24 | Label(title: { 25 | if text != nil { 26 | Text(text!) 27 | .font(.custom("IBMPlexSerif-Regular", size: 13)) 28 | } 29 | }, icon: { 30 | Image(systemName: icon) 31 | }) 32 | .foregroundStyle(disabled ? .tertiary : .primary) 33 | .padding() 34 | } 35 | .frame(width: minSize.width, height: minSize.height) 36 | .onHover { 37 | onHover?($0) 38 | } 39 | .onTapGesture { 40 | action?() 41 | } 42 | } 43 | } 44 | 45 | #Preview { 46 | BgButton(text: "Rate app", icon: "star", minSize: CGSize(width: 150, height: 50)).padding() 47 | } 48 | -------------------------------------------------------------------------------- /HNReader/View/Components/HTMLText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLText.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 13/06/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HTMLText: NSViewRepresentable { 11 | var text: String 12 | 13 | func makeNSView(context: Context) -> NSTextField { 14 | let text = NSTextField() 15 | text.isEditable = false 16 | if let attributedString = try? NSAttributedString( 17 | data: self.text.data(using: .utf8)!, 18 | options: [.documentType: NSAttributedString.DocumentType.html], 19 | documentAttributes: nil 20 | ) { 21 | text.attributedStringValue = attributedString 22 | } 23 | text.textColor = .white 24 | return text 25 | } 26 | 27 | func updateNSView(_ nsView: NSViewType, context: Context) {} 28 | } 29 | 30 | #Preview { 31 | HTMLText(text: """ 32 | string <h1>Krupal testing <span style="font-weight: 33 | bold;">Customer WYWO</span></h1> 34 | """) 35 | } 36 | -------------------------------------------------------------------------------- /HNReader/View/ConditionalRedactedModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConditionalRedacted.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 04/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ConditionalRedactedModifier: ViewModifier { 11 | var isRedacted: Bool 12 | 13 | func body(content: Content) -> some View { 14 | if isRedacted { 15 | content.redacted(reason: .placeholder) 16 | } else { 17 | content 18 | } 19 | } 20 | } 21 | 22 | extension View { 23 | func redactIfNull(_ obj: Optional) -> some View { 24 | switch obj { 25 | case .none: 26 | return self.modifier(ConditionalRedactedModifier(isRedacted: true)) 27 | case .some(_): 28 | return self.modifier(ConditionalRedactedModifier(isRedacted: false)) 29 | } 30 | } 31 | } 32 | 33 | #Preview { 34 | VStack { 35 | Text("Some Text") 36 | .redactIfNull(Optional.none) 37 | } 38 | .frame(width: 200, height: 100) 39 | } 40 | -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 20F71 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | HNReader 11 | CFBundleIconFile 12 | AppIcon 13 | CFBundleIconName 14 | AppIcon 15 | CFBundleIdentifier 16 | com.mattrighetti.HNReader 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | HNReader 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | 1.0 25 | CFBundleSupportedPlatforms 26 | 27 | MacOSX 28 | 29 | CFBundleVersion 30 | 1 31 | DTCompiler 32 | com.apple.compilers.llvm.clang.1_0 33 | DTPlatformBuild 34 | 12E262 35 | DTPlatformName 36 | macosx 37 | DTPlatformVersion 38 | 11.3 39 | DTSDKBuild 40 | 20E214 41 | DTSDKName 42 | macosx11.3 43 | DTXcode 44 | 1250 45 | DTXcodeBuild 46 | 12E262 47 | LSApplicationCategoryType 48 | public.app-category.news 49 | LSMinimumSystemVersion 50 | 11.0 51 | 52 | 53 | -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/MacOS/HNReader: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/View/HNReader/HNReader.app/Contents/MacOS/HNReader -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/Resources/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/View/HNReader/HNReader.app/Contents/Resources/AppIcon.icns -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/Resources/Assets.car: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/View/HNReader/HNReader.app/Contents/Resources/Assets.car -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/HNReader.mom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/HNReader.mom -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/HNReader.omo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/HNReader.omo -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/VersionInfo.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/VersionInfo.plist -------------------------------------------------------------------------------- /HNReader/View/HNReader/HNReader.app/Contents/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Resources/AppIcon.icns 8 | 9 | XuqNLC4a6bCVfG02ZjyF5oNNFks= 10 | 11 | Resources/Assets.car 12 | 13 | AOjNs8bov+COFYNU1MoTdNBymKY= 14 | 15 | Resources/HNReader.momd/HNReader.mom 16 | 17 | A5i6Glwsluin1g2ltov3tpUjbLM= 18 | 19 | Resources/HNReader.momd/HNReader.omo 20 | 21 | acxzJXINprFYwcy2DBxx01kTGZc= 22 | 23 | Resources/HNReader.momd/VersionInfo.plist 24 | 25 | caVpC1d/jd/uNi/pQzM4WAsxE60= 26 | 27 | 28 | files2 29 | 30 | Resources/AppIcon.icns 31 | 32 | hash2 33 | 34 | KlNaPoIIGS5Drw9lDCdN2L1VcRHQYkG472WCHnauBQg= 35 | 36 | 37 | Resources/Assets.car 38 | 39 | hash2 40 | 41 | ne33KSa9/FQA9oL3k4QVN+nBWIVtij3HzTNmLFnfZSw= 42 | 43 | 44 | Resources/HNReader.momd/HNReader.mom 45 | 46 | hash2 47 | 48 | J2b313nYGtCcc2LpxMYzwmp5/uYjYA2FeaKKTx36wfA= 49 | 50 | 51 | Resources/HNReader.momd/HNReader.omo 52 | 53 | hash2 54 | 55 | F3Dl+udfH8Ihjo0aD1CMms8lVQTUgKuhWDDTIR2HFb4= 56 | 57 | 58 | Resources/HNReader.momd/VersionInfo.plist 59 | 60 | hash2 61 | 62 | Z6nIstUOYh27AEP8TxmKktPZzMhjQfE91eP5zPHPQ8g= 63 | 64 | 65 | 66 | rules 67 | 68 | ^Resources/ 69 | 70 | ^Resources/.*\.lproj/ 71 | 72 | optional 73 | 74 | weight 75 | 1000 76 | 77 | ^Resources/.*\.lproj/locversion.plist$ 78 | 79 | omit 80 | 81 | weight 82 | 1100 83 | 84 | ^Resources/Base\.lproj/ 85 | 86 | weight 87 | 1010 88 | 89 | ^version.plist$ 90 | 91 | 92 | rules2 93 | 94 | .*\.dSYM($|/) 95 | 96 | weight 97 | 11 98 | 99 | ^(.*/)?\.DS_Store$ 100 | 101 | omit 102 | 103 | weight 104 | 2000 105 | 106 | ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ 107 | 108 | nested 109 | 110 | weight 111 | 10 112 | 113 | ^.* 114 | 115 | ^Info\.plist$ 116 | 117 | omit 118 | 119 | weight 120 | 20 121 | 122 | ^PkgInfo$ 123 | 124 | omit 125 | 126 | weight 127 | 20 128 | 129 | ^Resources/ 130 | 131 | weight 132 | 20 133 | 134 | ^Resources/.*\.lproj/ 135 | 136 | optional 137 | 138 | weight 139 | 1000 140 | 141 | ^Resources/.*\.lproj/locversion.plist$ 142 | 143 | omit 144 | 145 | weight 146 | 1100 147 | 148 | ^Resources/Base\.lproj/ 149 | 150 | weight 151 | 1010 152 | 153 | ^[^/]+$ 154 | 155 | nested 156 | 157 | weight 158 | 10 159 | 160 | ^embedded\.provisionprofile$ 161 | 162 | weight 163 | 20 164 | 165 | ^version\.plist$ 166 | 167 | weight 168 | 20 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /HNReader/View/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | 11 | struct HomeView: View { 12 | var body: some View { 13 | NavigationView { 14 | Sidebar() 15 | ItemList() 16 | } 17 | } 18 | } 19 | 20 | struct Sidebar: View { 21 | @EnvironmentObject var appState: AppState 22 | 23 | var body: some View { 24 | List(selection: $appState.sidebarSelection) { 25 | Section(header: Text("Categories")) { 26 | ForEach(AppState.SidebarSelection.allCases, id: \.self) { selectionItem in 27 | Label(selectionItem.rawValue, systemImage: selectionItem.iconName) 28 | .tag(selectionItem) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | #Preview { 36 | HomeView() 37 | } 38 | -------------------------------------------------------------------------------- /HNReader/View/InfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfoView.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 05/11/23. 6 | // 7 | 8 | import AppKit 9 | import StoreKit 10 | import SwiftUI 11 | 12 | struct InfoView: View { 13 | var version: String { 14 | let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String 15 | /// Build version of the app, i.e. `64` 16 | let appBundleVersion: String = Bundle.main.infoDictionary?["CFBundleVersion"] as! String 17 | 18 | return "\(version) (\(appBundleVersion))" 19 | } 20 | 21 | var body: some View { 22 | VStack { 23 | Text("HNReader v\(version)") 24 | .font(.custom("IBMPlexSerif-SemiBold", size: 15)) 25 | 26 | Text("This project is open source, if you like it you can star it on GitHub.") 27 | .lineLimit(4) 28 | .font(.custom("IBMPlexSerif-Regular", size: 13)) 29 | .foregroundStyle(.secondary) 30 | .multilineTextAlignment(.center) 31 | .padding(.vertical) 32 | 33 | BgButton(text: "Rate HNReader", icon: "star", minSize: CGSize(width: 250, height: 50), onHover: { h in 34 | DispatchQueue.main.async { 35 | if (h) { 36 | NSCursor.pointingHand.push() 37 | } else { 38 | NSCursor.pop() 39 | } 40 | } 41 | }, action: { 42 | NSWorkspace.shared.open(URL(string: "https://apps.apple.com/it/app/id1572480416?action=write-review")!) 43 | }) 44 | 45 | BgButton(text: "Open on GitHub", icon: "arrow.up.right", minSize: CGSize(width: 250, height: 50), onHover: { h in 46 | DispatchQueue.main.async { 47 | if (h) { 48 | NSCursor.pointingHand.push() 49 | } else { 50 | NSCursor.pop() 51 | } 52 | } 53 | }, action: { 54 | NSWorkspace.shared.open(URL(string: "https://github.com/mattrighetti/HNReaderApp.git")!) 55 | }) 56 | } 57 | .padding() 58 | .frame(width: 300) 59 | } 60 | } 61 | 62 | #Preview { 63 | InfoView() 64 | } 65 | -------------------------------------------------------------------------------- /HNReader/View/ItemCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemCell.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | func nullableString(_ s: String?) -> String { 11 | guard let s = s else { return "" } 12 | return s 13 | } 14 | 15 | struct ItemCell: View { 16 | var itemId: Int 17 | let itemDownloader: ItemDownloader 18 | 19 | @Environment(\.colorScheme) var colorScheme 20 | @State var item: Item? 21 | 22 | init(itemId: Int) { 23 | self.itemId = itemId 24 | itemDownloader = DefaultItemDownloader(itemId: itemId) 25 | } 26 | 27 | var body: some View { 28 | HStack { 29 | VStack(alignment: .leading, spacing: 5) { 30 | Text(item?.title ?? String(repeating: "-", count: 30)) 31 | .font(.custom("IBMPlexSerif-Bold", size: 17)) 32 | .redactIfNull(item) 33 | 34 | if let url = item?.urlHost { 35 | Text(url) 36 | .font(.custom("IBMPlexSerif-Light", size: 12)) 37 | .redactIfNull(item) 38 | } 39 | 40 | HStack { 41 | Text(nullableString(item?.scoreString)) 42 | .font(.custom("IBMPlexSerif-SemiBold", size: 12)) 43 | .redactIfNull(item) 44 | 45 | Text("Posted by \(nullableString(item?.by))") 46 | .font(.custom("IBMPlexSerif-Regular", size: 12)) 47 | .redactIfNull(item) 48 | 49 | Text("\(nullableString(item?.timeStringRepresentation))") 50 | .font(.custom("IBMPlexSerif-Regular", size: 12)) 51 | .redactIfNull(item) 52 | 53 | Spacer() 54 | } 55 | } 56 | 57 | HStack { 58 | BgButton(icon: "bubble.left", minSize: CGSize(width: 50, height: 50), onHover: { isHovered in 59 | DispatchQueue.main.async { 60 | if (isHovered) { 61 | NSCursor.pointingHand.push() 62 | } else { 63 | NSCursor.pop() 64 | } 65 | } 66 | }, action: { 67 | if let item = item { 68 | guard let url = URL(string: "https://news.ycombinator.com/item?id=\(item.id)") else { return } 69 | NSWorkspace.shared.open(url) 70 | } 71 | }) 72 | 73 | BgButton(icon: "arrow.up.right", disabled: item?.url == nil, minSize: CGSize(width: 50, height: 50), onHover: { isHovered in 74 | DispatchQueue.main.async { 75 | if (isHovered) { 76 | if item?.url == nil { 77 | NSCursor.operationNotAllowed.push() 78 | } else { 79 | NSCursor.pointingHand.push() 80 | } 81 | } else { 82 | NSCursor.pop() 83 | } 84 | } 85 | }, action: { 86 | guard let url = item?.url, let url = URL(string: url) else { return } 87 | NSWorkspace.shared.open(url) 88 | }) 89 | } 90 | .padding(.leading) 91 | } 92 | .padding() 93 | .background(colorScheme == .dark ? Color.black.opacity(0.3) : Color.gray.opacity(0.1)) 94 | .cornerRadius(10) 95 | .onAppear { 96 | if item == nil { 97 | fetchItem() 98 | } 99 | } 100 | } 101 | 102 | private func fetchItem() { 103 | let cacheKey = itemId 104 | if let cachedItem = ItemCache.shared.getItem(for: cacheKey) { 105 | self.item = cachedItem 106 | } else { 107 | itemDownloader.downloadItem(completion: { item in 108 | guard let item = item else { return } 109 | ItemCache.shared.cache(item, for: cacheKey) 110 | DispatchQueue.main.async { 111 | self.item = item 112 | } 113 | }) 114 | } 115 | } 116 | } 117 | 118 | #Preview { 119 | ItemCell(itemId: 27492268) 120 | } 121 | -------------------------------------------------------------------------------- /HNReader/View/ItemList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemList.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import SwiftUI 9 | import OSLog 10 | 11 | struct ItemList: View { 12 | @EnvironmentObject var appState: AppState 13 | @StateObject var viewModel = ItemListViewModel() 14 | @State private var itemLimitSelection: Int = 1 15 | private var itemLimitOptions: [Int] = [25, 50, 100] 16 | 17 | var body: some View { 18 | List { 19 | ForEach(viewModel.storiesIds, id: \.self) { itemId in 20 | ItemCell(itemId: itemId) 21 | .listRowSeparator(.hidden) 22 | } 23 | } 24 | .onAppear { 25 | viewModel.currentNewsSelection = appState.newsSelection 26 | } 27 | .onChange(of: appState.newsSelection, { 28 | fetchItems(by: appState.newsSelection) 29 | }) 30 | .toolbar { 31 | MaxItemPicker(enabled: false) 32 | Button(action: viewModel.refreshStories) { 33 | Label("Refresh news", systemImage: "arrow.counterclockwise.circle") 34 | } 35 | Button(action: { 36 | InfoView().openInWindow(title: "Info", sender: nil) 37 | }, label: { 38 | Label("Info", systemImage: "info.circle") 39 | }) 40 | } 41 | .navigationTitle("Hacker News") 42 | } 43 | 44 | @ViewBuilder 45 | private func MaxItemPicker(enabled: Bool) -> some View { 46 | if enabled { 47 | Picker("Limit", selection: $itemLimitSelection) { 48 | ForEach(itemLimitOptions.indices, id: \.self) { index in 49 | Text("\(itemLimitOptions[index])") 50 | .tag(index) 51 | } 52 | } 53 | .pickerStyle(SegmentedPickerStyle()) 54 | } else { 55 | EmptyView() 56 | } 57 | } 58 | 59 | private func fetchItems(by category: HackerNews.API.Stories) { 60 | if category != viewModel.currentNewsSelection { 61 | NSLog("changing category from \(viewModel.currentNewsSelection) to \(category)") 62 | viewModel.currentNewsSelection = category 63 | } 64 | } 65 | } 66 | 67 | #Preview { 68 | ItemList() 69 | } 70 | -------------------------------------------------------------------------------- /HNReader/ViewModel/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // HNReader 4 | // 5 | // Created by Mattia Righetti on 12/06/21. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | class AppState: ObservableObject { 12 | @AppStorage("displayMode") var displayMode: DisplayMode = .system 13 | @Published var sidebarSelection: SidebarSelection? = SidebarSelection.top { 14 | willSet { 15 | switch newValue { 16 | case .top: 17 | newsSelection = .top 18 | case .ask: 19 | newsSelection = .ask 20 | case .show: 21 | newsSelection = .show 22 | case .best: 23 | newsSelection = .best 24 | case .new: 25 | newsSelection = .new 26 | case .job: 27 | newsSelection = .job 28 | default: 29 | break 30 | } 31 | } 32 | } 33 | @Published var newsSelection: HackerNews.API.Stories = .top 34 | 35 | func getColorScheme() -> ColorScheme { 36 | switch displayMode { 37 | case .dark: return .dark 38 | case .light: return .light 39 | case .system: return .dark 40 | } 41 | } 42 | 43 | func setColorScheme(_ colorScheme: ColorScheme) { 44 | switch colorScheme { 45 | case .dark: 46 | displayMode = .dark 47 | case .light: 48 | displayMode = .light 49 | @unknown default: 50 | break 51 | } 52 | } 53 | 54 | // Sidebar categories abstraction 55 | enum SidebarSelection: String, Equatable, CaseIterable { 56 | case top = "Top" 57 | case ask = "Ask" 58 | case show = "Show" 59 | case job = "Job" 60 | case best = "Best" 61 | case new = "Newest" 62 | 63 | var iconName: String { 64 | switch self { 65 | case .top: return "flame" 66 | case .ask: return "person.fill.questionmark" 67 | case .new: return "paperplane" 68 | case .show: return "eye.circle" 69 | case .best: return "rosette" 70 | case .job: return "briefcase" 71 | } 72 | } 73 | } 74 | 75 | enum DisplayMode: Int { 76 | case system = 0 77 | case dark = 1 78 | case light = 2 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /HNReader/ViewModel/ItemListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Mattia Righetti on 12/06/21. 3 | // 4 | 5 | import Combine 6 | import SwiftUI 7 | import OSLog 8 | 9 | class ItemListViewModel: ObservableObject { 10 | @Published var currentNewsSelection: HackerNews.API.Stories = .top { 11 | willSet { 12 | fetchStories(by: newValue) 13 | } 14 | } 15 | @Published var storiesIds: [Int] = [] 16 | public var subscriptions = Set() 17 | 18 | public func fetchStories(by category: HackerNews.API.Stories) { 19 | HackerNewsClient.shared.getStoriesId(by: category) 20 | .receive(on: DispatchQueue.main) 21 | .sink(receiveCompletion: { completion in 22 | switch completion { 23 | case .failure(let error): 24 | NSLog("encountered error while completing fetch task: \(error)") 25 | case .finished: 26 | break 27 | } 28 | }, receiveValue: { [unowned self] itemIds in 29 | storiesIds = itemIds 30 | }) 31 | .store(in: &subscriptions) 32 | } 33 | 34 | public func refreshStories() { 35 | fetchStories(by: currentNewsSelection) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /HNReader/fonts/IBMPlexSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/fonts/IBMPlexSerif-Bold.ttf -------------------------------------------------------------------------------- /HNReader/fonts/IBMPlexSerif-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/fonts/IBMPlexSerif-Light.ttf -------------------------------------------------------------------------------- /HNReader/fonts/IBMPlexSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/fonts/IBMPlexSerif-Regular.ttf -------------------------------------------------------------------------------- /HNReader/fonts/IBMPlexSerif-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/HNReader/fonts/IBMPlexSerif-SemiBold.ttf -------------------------------------------------------------------------------- /HNReaderTests/HNClientTests/HackerNewsClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Mattia Righetti on 12/06/21. 3 | // 4 | 5 | import XCTest 6 | import Combine 7 | @testable import HNReader 8 | 9 | class HackerNewsClientTests: XCTestCase { 10 | private var cancellables: Set! 11 | 12 | override func setUp() { 13 | super.setUp() 14 | cancellables = [] 15 | } 16 | 17 | func testGetUser() throws { 18 | var user: User? 19 | var error: Error? 20 | let expectation = self.expectation(description: "userGet") 21 | 22 | HackerNewsClient.shared 23 | .getUser(withId: "mattrighetti") 24 | .sink(receiveCompletion: { completion in 25 | switch completion { 26 | case .finished: 27 | break 28 | case .failure(let err): 29 | error = err 30 | } 31 | 32 | expectation.fulfill() 33 | }, receiveValue: { usr in 34 | user = usr 35 | }) 36 | .store(in: &cancellables) 37 | 38 | waitForExpectations(timeout: 5) 39 | 40 | XCTAssertNil(error) 41 | XCTAssertEqual(user!.id, "mattrighetti") 42 | } 43 | 44 | func testGetItem() throws { 45 | var item: Item? 46 | var error: Error? 47 | let expectation = self.expectation(description: "itemGet") 48 | 49 | HackerNewsClient.shared 50 | .getItem(withId: 27348900) 51 | .sink(receiveCompletion: { completion in 52 | switch completion { 53 | case .finished: 54 | break 55 | case .failure(let err): 56 | error = err 57 | } 58 | 59 | expectation.fulfill() 60 | }, receiveValue: { newItem in 61 | item = newItem 62 | }) 63 | .store(in: &cancellables) 64 | 65 | waitForExpectations(timeout: 5) 66 | 67 | XCTAssertNil(error) 68 | XCTAssertNotNil(item) 69 | XCTAssertEqual(item!.id, 27348900) 70 | } 71 | 72 | func testGetTopStories() throws { 73 | var stories: [Item]? 74 | var error: Error? 75 | let expectation = self.expectation(description: "bestStoriesGet") 76 | 77 | HackerNewsClient.shared 78 | .getStories(by: .top, limit: 100) 79 | .sink(receiveCompletion: { completion in 80 | switch completion { 81 | case .finished: 82 | break 83 | case .failure(let err): 84 | error = err 85 | } 86 | 87 | expectation.fulfill() 88 | }, receiveValue: { items in 89 | stories = items 90 | }) 91 | .store(in: &cancellables) 92 | 93 | waitForExpectations(timeout: 15) 94 | 95 | XCTAssertNil(error) 96 | XCTAssertNotNil(stories) 97 | print(stories) 98 | XCTAssertFalse(stories!.isEmpty) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /HNReaderTests/HNClientTests/HackerNewsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Mattia Righetti on 12/06/21. 3 | // 4 | 5 | import XCTest 6 | @testable import HNReader 7 | 8 | class HackerNewsTests: XCTestCase { 9 | func testItemApiUrlStrings() throws { 10 | let itemUrlExpected = "https://hacker-news.firebaseio.com/v0/item/39.json" 11 | let itemUrl = HackerNews.API.Item.id(39) 12 | XCTAssertEqual(itemUrlExpected, itemUrl.urlString) 13 | } 14 | 15 | func testUserApiUrlString() throws { 16 | let userUrlExpected = "https://hacker-news.firebaseio.com/v0/user/randomuser.json" 17 | let userUrl = HackerNews.API.User.id("randomuser") 18 | XCTAssertEqual(userUrlExpected, userUrl.urlString) 19 | } 20 | 21 | func testStoriesApiUrlString() throws { 22 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/topstories.json", HackerNews.API.Stories.top.urlString) 23 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/newstories.json", HackerNews.API.Stories.new.urlString) 24 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/beststories.json", HackerNews.API.Stories.best.urlString) 25 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/askstories.json", HackerNews.API.Stories.ask.urlString) 26 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/showstories.json", HackerNews.API.Stories.show.urlString) 27 | } 28 | } -------------------------------------------------------------------------------- /HNReaderTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /HNReaderTests/ModelTests/ItemTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Mattia Righetti on 13/06/21. 3 | // 4 | 5 | import XCTest 6 | @testable import HNReader 7 | 8 | class ItemTests: XCTestCase { 9 | func testItemDateIntervalFormatter() { 10 | let yesterday = Date(timeIntervalSinceNow: -(60*60*24)) 11 | let item1 = Item(id: 213412, 12 | deleted: false, 13 | type: .story, 14 | by: nil, 15 | time: Int(yesterday.timeIntervalSince1970), 16 | text: nil, 17 | dead: nil, 18 | parent: nil, 19 | poll: nil, 20 | kids: nil, 21 | url: "https://www.reddit.com/r/MechanicalKeyboards", 22 | score: nil, 23 | title: nil, 24 | parts: nil, 25 | descendants: nil) 26 | 27 | XCTAssertEqual("1d", item1.timeStringRepresentation) 28 | XCTAssertEqual("reddit.com", item1.urlHost) 29 | } 30 | } -------------------------------------------------------------------------------- /HNReaderTests/UtilsTests/+DateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Mattia Righetti on 13/06/21. 3 | // 4 | 5 | import XCTest 6 | @testable import HNReader 7 | 8 | class DateTests: XCTestCase { 9 | func testElapsedTimeStringRepresentation() { 10 | let minuteTimeInterval = TimeInterval(60) 11 | let hourTimeInterval = TimeInterval(minuteTimeInterval*60) 12 | let dayTimeInterval = TimeInterval(24*hourTimeInterval) 13 | let yearTimeInterval = TimeInterval(dayTimeInterval*365) 14 | let yesterday = Date(timeIntervalSinceNow: -dayTimeInterval) 15 | let halfHourAgo = Date(timeIntervalSinceNow: -(minuteTimeInterval*30)) 16 | let hourAgo = Date(timeIntervalSinceNow: -hourTimeInterval) 17 | let threeYearsAgo = Date(timeIntervalSinceNow: -(3*yearTimeInterval)) 18 | let s1 = Date().timeElapsedStringRepresentation(since: yesterday) 19 | let s2 = Date().timeElapsedStringRepresentation(since: halfHourAgo) 20 | let s3 = Date().timeElapsedStringRepresentation(since: hourAgo) 21 | let s4 = Date().timeElapsedStringRepresentation(since: threeYearsAgo) 22 | XCTAssertEqual("1d", s1) 23 | XCTAssertEqual("30m", s2) 24 | XCTAssertEqual("1h", s3) 25 | XCTAssertEqual("3y", s4) 26 | } 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Mattia Righetti (mattiarighetti@protonmail.com) 2 | 3 | Licensed under either of 4 | 5 | * Apache License, Version 2.0, (http://www.apache.org/license/LICENSE-2.0) 6 | * MIT License (http://opensource.org/licenses/MIT) 7 | 8 | at your option. -------------------------------------------------------------------------------- /Logo/HNReader Logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/Logo/HNReader Logo.sketch -------------------------------------------------------------------------------- /Logo/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/Logo/Logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # HNReaderApp 6 | This is the public repository for the HNReader macOS application. 7 | 8 | You can report any issue and suggest/request new features in the [issue](https://github.com/mattrighetti/HNReaderApp/issues) section, you can also use the [discussions](https://github.com/mattrighetti/HNReaderApp/discussions) section to chat with others and me about the application. 9 | 10 | The application is still in beta for the moment and needs optimizations. When the time come I will release it to AppStore and on `brew`. 11 | 12 |

13 | 14 | 15 | 16 |

17 | 18 | ## Application preview 19 | ### Dark mode 20 | dark mode 21 | 22 | ### Light mode 23 | light mode 24 | 25 | ## License 26 | [MIT](LICENSE) or [APACHE 2.0](LICENSE) 27 | -------------------------------------------------------------------------------- /resources/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/resources/appstore.png -------------------------------------------------------------------------------- /resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/HNReaderApp/3db05b4dbc0783fd3ae93dd67fdb00deb63e1ea8/resources/screenshot.png --------------------------------------------------------------------------------