From 15699b0b85a64d710e5e81587997889697ca44f9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 7 Dec 2021 21:58:02 -0500 Subject: [PATCH] Initial commit --- .gitignore | 78 ++ Fervor/ClientRegistration.swift | 25 + Fervor/Feed.swift | 40 + Fervor/Fervor.h | 18 + Fervor/Fervor.swift | 11 + Fervor/FervorClient.swift | 74 ++ Fervor/Group.swift | 31 + Fervor/Instance.swift | 34 + Fervor/Item.swift | 52 ++ Fervor/Token.swift | 28 + Fervor/URLRequest+Body.swift | 23 + Reader.xcodeproj/project.pbxproj | 861 ++++++++++++++++++ Reader/AppDelegate.swift | 81 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++ Reader/Assets.xcassets/Contents.json | 6 + Reader/Base.lproj/LaunchScreen.storyboard | 25 + Reader/FervorController.swift | 47 + Reader/Info.plist | 23 + Reader/LocalData.swift | 38 + Reader/Reader.xcdatamodeld/.xccurrentversion | 8 + .../Reader.xcdatamodel/contents | 4 + Reader/SceneDelegate.swift | 81 ++ Reader/Screens/Home/HomeViewController.swift | 39 + .../Screens/Login/LoginViewController.swift | 104 +++ ReaderTests/ReaderTests.swift | 33 + ReaderUITests/ReaderUITests.swift | 42 + ReaderUITests/ReaderUITestsLaunchTests.swift | 32 + 28 files changed, 1947 insertions(+) create mode 100644 .gitignore create mode 100644 Fervor/ClientRegistration.swift create mode 100644 Fervor/Feed.swift create mode 100644 Fervor/Fervor.h create mode 100644 Fervor/Fervor.swift create mode 100644 Fervor/FervorClient.swift create mode 100644 Fervor/Group.swift create mode 100644 Fervor/Instance.swift create mode 100644 Fervor/Item.swift create mode 100644 Fervor/Token.swift create mode 100644 Fervor/URLRequest+Body.swift create mode 100644 Reader.xcodeproj/project.pbxproj create mode 100644 Reader/AppDelegate.swift create mode 100644 Reader/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Reader/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Reader/Assets.xcassets/Contents.json create mode 100644 Reader/Base.lproj/LaunchScreen.storyboard create mode 100644 Reader/FervorController.swift create mode 100644 Reader/Info.plist create mode 100644 Reader/LocalData.swift create mode 100644 Reader/Reader.xcdatamodeld/.xccurrentversion create mode 100644 Reader/Reader.xcdatamodeld/Reader.xcdatamodel/contents create mode 100644 Reader/SceneDelegate.swift create mode 100644 Reader/Screens/Home/HomeViewController.swift create mode 100644 Reader/Screens/Login/LoginViewController.swift create mode 100644 ReaderTests/ReaderTests.swift create mode 100644 ReaderUITests/ReaderUITests.swift create mode 100644 ReaderUITests/ReaderUITestsLaunchTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc6431b --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +.DS_Store +MyPlayground.playground/ + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +.build/ + +# CocoaPods - Refactored to standalone file + +# Carthage - Refactored to standalone file + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output + +### Xcode ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated + +## Various settings + +## Other + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno diff --git a/Fervor/ClientRegistration.swift b/Fervor/ClientRegistration.swift new file mode 100644 index 0000000..f26ea15 --- /dev/null +++ b/Fervor/ClientRegistration.swift @@ -0,0 +1,25 @@ +// +// ClientRegistration.swift +// Fervor +// +// Created by Shadowfacts on 11/25/21. +// + +import Foundation + +public struct ClientRegistration: Decodable { + public let clientID: String + public let clientSecret: String + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.clientID = try container.decode(String.self, forKey: .clientID) + self.clientSecret = try container.decode(String.self, forKey: .clientSecret) + } + + private enum CodingKeys: String, CodingKey { + case clientID = "client_id" + case clientSecret = "client_secret" + } +} diff --git a/Fervor/Feed.swift b/Fervor/Feed.swift new file mode 100644 index 0000000..f10294d --- /dev/null +++ b/Fervor/Feed.swift @@ -0,0 +1,40 @@ +// +// Feed.swift +// Fervor +// +// Created by Shadowfacts on 10/29/21. +// + +import Foundation + +public struct Feed: Decodable { + public let id: FervorID + public let title: String + public let url: URL + public let serviceURL: URL? + public let feedURL: URL + public let lastUpdated: Date + public let groupIDs: [FervorID] + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decode(FervorID.self, forKey: .id) + self.title = try container.decode(String.self, forKey: .title) + self.url = try container.decode(URL.self, forKey: .url) + self.serviceURL = try container.decodeIfPresent(URL.self, forKey: .serviceURL) + self.feedURL = try container.decode(URL.self, forKey: .feedURL) + self.lastUpdated = try container.decode(Date.self, forKey: .lastUpdated) + self.groupIDs = try container.decode([FervorID].self, forKey: .groupIDs) + } + + private enum CodingKeys: String, CodingKey { + case id + case title + case url + case serviceURL = "service_url" + case feedURL = "feed_url" + case lastUpdated = "last_updated" + case groupIDs = "group_ids" + } +} diff --git a/Fervor/Fervor.h b/Fervor/Fervor.h new file mode 100644 index 0000000..8ea1a06 --- /dev/null +++ b/Fervor/Fervor.h @@ -0,0 +1,18 @@ +// +// Fervor.h +// Fervor +// +// Created by Shadowfacts on 10/29/21. +// + +#import + +//! Project version number for Fervor. +FOUNDATION_EXPORT double FervorVersionNumber; + +//! Project version string for Fervor. +FOUNDATION_EXPORT const unsigned char FervorVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Fervor/Fervor.swift b/Fervor/Fervor.swift new file mode 100644 index 0000000..392db5e --- /dev/null +++ b/Fervor/Fervor.swift @@ -0,0 +1,11 @@ +// +// Fervor.swift +// Fervor +// +// Created by Shadowfacts on 10/29/21. +// + +import Foundation + +// todo: fervor: ids should be strings +public typealias FervorID = Int diff --git a/Fervor/FervorClient.swift b/Fervor/FervorClient.swift new file mode 100644 index 0000000..a18e39c --- /dev/null +++ b/Fervor/FervorClient.swift @@ -0,0 +1,74 @@ +// +// FervorClient.swift +// Fervor +// +// Created by Shadowfacts on 11/25/21. +// + +import Foundation + +public class FervorClient { + + let instanceURL: URL + let session: URLSession + public var accessToken: String? + + private let decoder = JSONDecoder() + + public init(instanceURL: URL, accessToken: String?, session: URLSession = .shared) { + self.instanceURL = instanceURL + self.accessToken = accessToken + self.session = session + } + + private func buildURL(path: String) -> URL { + var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)! + components.path = path + return components.url! + } + + private func performRequest(_ request: URLRequest) async throws -> T { + var request = request + if let accessToken = accessToken { + request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + } + let (data, _) = try await session.data(for: request, delegate: nil) + let decoded = try decoder.decode(T.self, from: data) + return decoded + } + + public func register(clientName: String, website: URL? = nil, redirectURI: URL) async throws -> ClientRegistration { + var request = URLRequest(url: buildURL(path: "/api/v1/register")) + request.httpMethod = "POST" + request.setURLEncodedBody(params: [ + "client_name": clientName, + "website": website?.absoluteString, + "redirect_uri": redirectURI.absoluteString, + ]) + return try await performRequest(request) + } + + public func token(authCode: String, redirectURI: URL, clientID: String, clientSecret: String) async throws -> Token { + var request = URLRequest(url: buildURL(path: "/oauth/token")) + request.httpMethod = "POST" + request.setURLEncodedBody(params: [ + "grant_type": "authorization_code", + "authorization_code": authCode, + "redirect_uri": redirectURI.absoluteString, + "client_id": clientID, + "client_secret": clientSecret, + ]) + return try await performRequest(request) + } + + public struct Auth { + public let accessToken: String + public let refreshToken: String? + } + + public enum Error: Swift.Error { + case urlSession(Swift.Error) + case decode(Swift.Error) + } + +} diff --git a/Fervor/Group.swift b/Fervor/Group.swift new file mode 100644 index 0000000..1fcb419 --- /dev/null +++ b/Fervor/Group.swift @@ -0,0 +1,31 @@ +// +// Group.swift +// Fervor +// +// Created by Shadowfacts on 10/29/21. +// + +import Foundation + +public struct Group: Decodable { + public let id: FervorID + public let title: String + public let feedIDs: [FervorID] + public let serviceURL: URL? + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decode(FervorID.self, forKey: .id) + self.title = try container.decode(String.self, forKey: .title) + self.feedIDs = try container.decode([FervorID].self, forKey: .feedIDs) + self.serviceURL = try container.decodeIfPresent(URL.self, forKey: .serviceURL) + } + + private enum CodingKeys: String, CodingKey { + case id + case title + case feedIDs = "feed_ids" + case serviceURL = "service_url" + } +} diff --git a/Fervor/Instance.swift b/Fervor/Instance.swift new file mode 100644 index 0000000..5185859 --- /dev/null +++ b/Fervor/Instance.swift @@ -0,0 +1,34 @@ +// +// Instance.swift +// Fervor +// +// Created by Shadowfacts on 10/29/21. +// + +import Foundation + +public struct Instance: Decodable { + public let name: String + public let url: URL + public let version: String + public let implementationName: String + public let implementationVersion: String + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.name = try container.decode(String.self, forKey: .name) + self.url = try container.decode(URL.self, forKey: .url) + self.version = try container.decode(String.self, forKey: .version) + self.implementationName = try container.decode(String.self, forKey: .implementationName) + self.implementationVersion = try container.decode(String.self, forKey: .implementationVersion) + } + + private enum CodingKeys: String, CodingKey { + case name + case url + case version + case implementationName = "implementation_name" + case implementationVersion = "implementation_version" + } +} diff --git a/Fervor/Item.swift b/Fervor/Item.swift new file mode 100644 index 0000000..cefbe2c --- /dev/null +++ b/Fervor/Item.swift @@ -0,0 +1,52 @@ +// +// Item.swift +// Fervor +// +// Created by Shadowfacts on 10/29/21. +// + +import Foundation + +public struct Item: Decodable { + let id: FervorID + let feedID: FervorID + let title: String + let author: String + let published: Date? + let createdAt: Date? + let content: String? + let summary: String? + let url: URL + let serviceURL: URL? + let read: Bool? + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decode(FervorID.self, forKey: .id) + self.feedID = try container.decode(FervorID.self, forKey: .feedID) + self.title = try container.decode(String.self, forKey: .title) + self.author = try container.decode(String.self, forKey: .author) + self.published = try container.decodeIfPresent(Date.self, forKey: .published) + self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) + self.content = try container.decodeIfPresent(String.self, forKey: .content) + self.summary = try container.decodeIfPresent(String.self, forKey: .summary) + self.url = try container.decode(URL.self, forKey: .url) + self.serviceURL = try container.decodeIfPresent(URL.self, forKey: .serviceURL) + self.read = try container.decodeIfPresent(Bool.self, forKey: .read) + } + + private enum CodingKeys: String, CodingKey { + case id + case feedID = "feed_id" + case title + case author + case published + case createdAt = "created_at" + case content + case summary + case url + case serviceURL = "service_url" + case read + } +} diff --git a/Fervor/Token.swift b/Fervor/Token.swift new file mode 100644 index 0000000..d50d9ec --- /dev/null +++ b/Fervor/Token.swift @@ -0,0 +1,28 @@ +// +// Token.swift +// Fervor +// +// Created by Shadowfacts on 11/25/21. +// + +import Foundation + +public struct Token: Decodable { + public let accessToken: String + public let expiresIn: Int? + public let refreshToken: String? + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.accessToken = try container.decode(String.self, forKey: .accessToken) + self.expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn) + self.refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken) + } + + private enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case expiresIn = "expires_in" + case refreshToken = "refresh_token" + } +} diff --git a/Fervor/URLRequest+Body.swift b/Fervor/URLRequest+Body.swift new file mode 100644 index 0000000..021ce26 --- /dev/null +++ b/Fervor/URLRequest+Body.swift @@ -0,0 +1,23 @@ +// +// URLRequest+Body.swift +// Fervor +// +// Created by Shadowfacts on 11/25/21. +// + +import Foundation + +extension URLRequest { + mutating func setURLEncodedBody(params: [String: String?]) { + setValue("application/x-www-form-urlencoded; charset=utf8", forHTTPHeaderField: "Content-Type") + var data = Data() + for (k, v) in params where v != nil { + data.append(contentsOf: k.utf8) + data.append(61) + data.append(contentsOf: v!.utf8) + data.append(38) + } + let _ = data.dropLast() // drop last & + httpBody = data + } +} diff --git a/Reader.xcodeproj/project.pbxproj b/Reader.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bdbad89 --- /dev/null +++ b/Reader.xcodeproj/project.pbxproj @@ -0,0 +1,861 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18B12750469D004A9448 /* LoginViewController.swift */; }; + D65B18B4275048D9004A9448 /* ClientRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18B3275048D9004A9448 /* ClientRegistration.swift */; }; + D65B18B627504920004A9448 /* FervorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18B527504920004A9448 /* FervorController.swift */; }; + D65B18B82750495D004A9448 /* FervorClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18B72750495D004A9448 /* FervorClient.swift */; }; + D65B18BA27504A21004A9448 /* URLRequest+Body.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18B927504A21004A9448 /* URLRequest+Body.swift */; }; + D65B18BC27504FE7004A9448 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18BB27504FE7004A9448 /* Token.swift */; }; + D65B18BE275051A1004A9448 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18BD275051A1004A9448 /* LocalData.swift */; }; + D65B18C127505348004A9448 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18C027505348004A9448 /* HomeViewController.swift */; }; + D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687EB272CD27600874C10 /* AppDelegate.swift */; }; + D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687ED272CD27600874C10 /* SceneDelegate.swift */; }; + D6C687F6272CD27600874C10 /* Reader.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6C687F4272CD27600874C10 /* Reader.xcdatamodeld */; }; + D6C687F8272CD27700874C10 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6C687F7272CD27700874C10 /* Assets.xcassets */; }; + D6C687FB272CD27700874C10 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */; }; + D6C68806272CD27700874C10 /* ReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68805272CD27700874C10 /* ReaderTests.swift */; }; + D6C68810272CD27700874C10 /* ReaderUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6880F272CD27700874C10 /* ReaderUITests.swift */; }; + D6C68812272CD27700874C10 /* ReaderUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68811272CD27700874C10 /* ReaderUITestsLaunchTests.swift */; }; + D6C68826272CD2BA00874C10 /* Fervor.h in Headers */ = {isa = PBXBuildFile; fileRef = D6C68825272CD2BA00874C10 /* Fervor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D6C68829272CD2BA00874C10 /* Fervor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6C68823272CD2BA00874C10 /* Fervor.framework */; }; + D6C6882A272CD2BA00874C10 /* Fervor.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6C68823272CD2BA00874C10 /* Fervor.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D6C68830272CD2CF00874C10 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6882F272CD2CF00874C10 /* Instance.swift */; }; + D6C68832272CD40600874C10 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68831272CD40600874C10 /* Feed.swift */; }; + D6C68834272CD44900874C10 /* Fervor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68833272CD44900874C10 /* Fervor.swift */; }; + D6C68856272CD7C600874C10 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68855272CD7C600874C10 /* Item.swift */; }; + D6C68858272CD8CD00874C10 /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68857272CD8CD00874C10 /* Group.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + D6C68802272CD27700874C10 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6C687E0272CD27600874C10 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D6C687E7272CD27600874C10; + remoteInfo = Reader; + }; + D6C6880C272CD27700874C10 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6C687E0272CD27600874C10 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D6C687E7272CD27600874C10; + remoteInfo = Reader; + }; + D6C68827272CD2BA00874C10 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6C687E0272CD27600874C10 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D6C68822272CD2BA00874C10; + remoteInfo = Fervor; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + D6C6882E272CD2BA00874C10 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D6C6882A272CD2BA00874C10 /* Fervor.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + D65B18B12750469D004A9448 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + D65B18B3275048D9004A9448 /* ClientRegistration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientRegistration.swift; sourceTree = ""; }; + D65B18B527504920004A9448 /* FervorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FervorController.swift; sourceTree = ""; }; + D65B18B72750495D004A9448 /* FervorClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FervorClient.swift; sourceTree = ""; }; + D65B18B927504A21004A9448 /* URLRequest+Body.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Body.swift"; sourceTree = ""; }; + D65B18BB27504FE7004A9448 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + D65B18BD275051A1004A9448 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; + D65B18C027505348004A9448 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + D6C687E8272CD27600874C10 /* Reader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reader.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D6C687EB272CD27600874C10 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D6C687ED272CD27600874C10 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + D6C687F5272CD27600874C10 /* Reader.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Reader.xcdatamodel; sourceTree = ""; }; + D6C687F7272CD27700874C10 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D6C687FA272CD27700874C10 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + D6C687FC272CD27700874C10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D6C68801272CD27700874C10 /* ReaderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReaderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D6C68805272CD27700874C10 /* ReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTests.swift; sourceTree = ""; }; + D6C6880B272CD27700874C10 /* ReaderUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReaderUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D6C6880F272CD27700874C10 /* ReaderUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderUITests.swift; sourceTree = ""; }; + D6C68811272CD27700874C10 /* ReaderUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderUITestsLaunchTests.swift; sourceTree = ""; }; + D6C68823272CD2BA00874C10 /* Fervor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Fervor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D6C68825272CD2BA00874C10 /* Fervor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Fervor.h; sourceTree = ""; }; + D6C6882F272CD2CF00874C10 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; + D6C68831272CD40600874C10 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; + D6C68833272CD44900874C10 /* Fervor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fervor.swift; sourceTree = ""; }; + D6C68855272CD7C600874C10 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + D6C68857272CD8CD00874C10 /* Group.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Group.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D6C687E5272CD27600874C10 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D6C68829272CD2BA00874C10 /* Fervor.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C687FE272CD27700874C10 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C68808272CD27700874C10 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C68820272CD2BA00874C10 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D65B18AF2750468B004A9448 /* Screens */ = { + isa = PBXGroup; + children = ( + D65B18BF2750533E004A9448 /* Home */, + D65B18B027504691004A9448 /* Login */, + ); + path = Screens; + sourceTree = ""; + }; + D65B18B027504691004A9448 /* Login */ = { + isa = PBXGroup; + children = ( + D65B18B12750469D004A9448 /* LoginViewController.swift */, + ); + path = Login; + sourceTree = ""; + }; + D65B18BF2750533E004A9448 /* Home */ = { + isa = PBXGroup; + children = ( + D65B18C027505348004A9448 /* HomeViewController.swift */, + ); + path = Home; + sourceTree = ""; + }; + D6C687DF272CD27600874C10 = { + isa = PBXGroup; + children = ( + D6C687EA272CD27600874C10 /* Reader */, + D6C68804272CD27700874C10 /* ReaderTests */, + D6C6880E272CD27700874C10 /* ReaderUITests */, + D6C68824272CD2BA00874C10 /* Fervor */, + D6C687E9272CD27600874C10 /* Products */, + ); + sourceTree = ""; + }; + D6C687E9272CD27600874C10 /* Products */ = { + isa = PBXGroup; + children = ( + D6C687E8272CD27600874C10 /* Reader.app */, + D6C68801272CD27700874C10 /* ReaderTests.xctest */, + D6C6880B272CD27700874C10 /* ReaderUITests.xctest */, + D6C68823272CD2BA00874C10 /* Fervor.framework */, + ); + name = Products; + sourceTree = ""; + }; + D6C687EA272CD27600874C10 /* Reader */ = { + isa = PBXGroup; + children = ( + D6C687EB272CD27600874C10 /* AppDelegate.swift */, + D6C687ED272CD27600874C10 /* SceneDelegate.swift */, + D65B18B527504920004A9448 /* FervorController.swift */, + D65B18BD275051A1004A9448 /* LocalData.swift */, + D65B18AF2750468B004A9448 /* Screens */, + D6C687F7272CD27700874C10 /* Assets.xcassets */, + D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */, + D6C687FC272CD27700874C10 /* Info.plist */, + D6C687F4272CD27600874C10 /* Reader.xcdatamodeld */, + ); + path = Reader; + sourceTree = ""; + }; + D6C68804272CD27700874C10 /* ReaderTests */ = { + isa = PBXGroup; + children = ( + D6C68805272CD27700874C10 /* ReaderTests.swift */, + ); + path = ReaderTests; + sourceTree = ""; + }; + D6C6880E272CD27700874C10 /* ReaderUITests */ = { + isa = PBXGroup; + children = ( + D6C6880F272CD27700874C10 /* ReaderUITests.swift */, + D6C68811272CD27700874C10 /* ReaderUITestsLaunchTests.swift */, + ); + path = ReaderUITests; + sourceTree = ""; + }; + D6C68824272CD2BA00874C10 /* Fervor */ = { + isa = PBXGroup; + children = ( + D6C68825272CD2BA00874C10 /* Fervor.h */, + D6C68833272CD44900874C10 /* Fervor.swift */, + D65B18B72750495D004A9448 /* FervorClient.swift */, + D65B18B927504A21004A9448 /* URLRequest+Body.swift */, + D65B18B3275048D9004A9448 /* ClientRegistration.swift */, + D6C68831272CD40600874C10 /* Feed.swift */, + D6C68857272CD8CD00874C10 /* Group.swift */, + D6C6882F272CD2CF00874C10 /* Instance.swift */, + D6C68855272CD7C600874C10 /* Item.swift */, + D65B18BB27504FE7004A9448 /* Token.swift */, + ); + path = Fervor; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D6C6881E272CD2BA00874C10 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D6C68826272CD2BA00874C10 /* Fervor.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D6C687E7272CD27600874C10 /* Reader */ = { + isa = PBXNativeTarget; + buildConfigurationList = D6C68815272CD27700874C10 /* Build configuration list for PBXNativeTarget "Reader" */; + buildPhases = ( + D6C687E4272CD27600874C10 /* Sources */, + D6C687E5272CD27600874C10 /* Frameworks */, + D6C687E6272CD27600874C10 /* Resources */, + D6C6882E272CD2BA00874C10 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D6C68828272CD2BA00874C10 /* PBXTargetDependency */, + ); + name = Reader; + productName = Reader; + productReference = D6C687E8272CD27600874C10 /* Reader.app */; + productType = "com.apple.product-type.application"; + }; + D6C68800272CD27700874C10 /* ReaderTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D6C68818272CD27700874C10 /* Build configuration list for PBXNativeTarget "ReaderTests" */; + buildPhases = ( + D6C687FD272CD27700874C10 /* Sources */, + D6C687FE272CD27700874C10 /* Frameworks */, + D6C687FF272CD27700874C10 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D6C68803272CD27700874C10 /* PBXTargetDependency */, + ); + name = ReaderTests; + productName = ReaderTests; + productReference = D6C68801272CD27700874C10 /* ReaderTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D6C6880A272CD27700874C10 /* ReaderUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D6C6881B272CD27700874C10 /* Build configuration list for PBXNativeTarget "ReaderUITests" */; + buildPhases = ( + D6C68807272CD27700874C10 /* Sources */, + D6C68808272CD27700874C10 /* Frameworks */, + D6C68809272CD27700874C10 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D6C6880D272CD27700874C10 /* PBXTargetDependency */, + ); + name = ReaderUITests; + productName = ReaderUITests; + productReference = D6C6880B272CD27700874C10 /* ReaderUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + D6C68822272CD2BA00874C10 /* Fervor */ = { + isa = PBXNativeTarget; + buildConfigurationList = D6C6882B272CD2BA00874C10 /* Build configuration list for PBXNativeTarget "Fervor" */; + buildPhases = ( + D6C6881E272CD2BA00874C10 /* Headers */, + D6C6881F272CD2BA00874C10 /* Sources */, + D6C68820272CD2BA00874C10 /* Frameworks */, + D6C68821272CD2BA00874C10 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Fervor; + productName = Fervor; + productReference = D6C68823272CD2BA00874C10 /* Fervor.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D6C687E0272CD27600874C10 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + D6C687E7272CD27600874C10 = { + CreatedOnToolsVersion = 13.2; + }; + D6C68800272CD27700874C10 = { + CreatedOnToolsVersion = 13.2; + TestTargetID = D6C687E7272CD27600874C10; + }; + D6C6880A272CD27700874C10 = { + CreatedOnToolsVersion = 13.2; + TestTargetID = D6C687E7272CD27600874C10; + }; + D6C68822272CD2BA00874C10 = { + CreatedOnToolsVersion = 13.2; + LastSwiftMigration = 1320; + }; + }; + }; + buildConfigurationList = D6C687E3272CD27600874C10 /* Build configuration list for PBXProject "Reader" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D6C687DF272CD27600874C10; + productRefGroup = D6C687E9272CD27600874C10 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D6C687E7272CD27600874C10 /* Reader */, + D6C68800272CD27700874C10 /* ReaderTests */, + D6C6880A272CD27700874C10 /* ReaderUITests */, + D6C68822272CD2BA00874C10 /* Fervor */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D6C687E6272CD27600874C10 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D6C687FB272CD27700874C10 /* LaunchScreen.storyboard in Resources */, + D6C687F8272CD27700874C10 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C687FF272CD27700874C10 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C68809272CD27700874C10 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C68821272CD2BA00874C10 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D6C687E4272CD27600874C10 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D65B18B627504920004A9448 /* FervorController.swift in Sources */, + D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */, + D6C687F6272CD27600874C10 /* Reader.xcdatamodeld in Sources */, + D65B18BE275051A1004A9448 /* LocalData.swift in Sources */, + D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */, + D65B18C127505348004A9448 /* HomeViewController.swift in Sources */, + D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C687FD272CD27700874C10 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D6C68806272CD27700874C10 /* ReaderTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C68807272CD27700874C10 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D6C68810272CD27700874C10 /* ReaderUITests.swift in Sources */, + D6C68812272CD27700874C10 /* ReaderUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6C6881F272CD2BA00874C10 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D65B18BC27504FE7004A9448 /* Token.swift in Sources */, + D6C68856272CD7C600874C10 /* Item.swift in Sources */, + D6C68830272CD2CF00874C10 /* Instance.swift in Sources */, + D6C68832272CD40600874C10 /* Feed.swift in Sources */, + D6C68858272CD8CD00874C10 /* Group.swift in Sources */, + D6C68834272CD44900874C10 /* Fervor.swift in Sources */, + D65B18BA27504A21004A9448 /* URLRequest+Body.swift in Sources */, + D65B18B82750495D004A9448 /* FervorClient.swift in Sources */, + D65B18B4275048D9004A9448 /* ClientRegistration.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + D6C68803272CD27700874C10 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D6C687E7272CD27600874C10 /* Reader */; + targetProxy = D6C68802272CD27700874C10 /* PBXContainerItemProxy */; + }; + D6C6880D272CD27700874C10 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D6C687E7272CD27600874C10 /* Reader */; + targetProxy = D6C6880C272CD27700874C10 /* PBXContainerItemProxy */; + }; + D6C68828272CD2BA00874C10 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D6C68822272CD2BA00874C10 /* Fervor */; + targetProxy = D6C68827272CD2BA00874C10 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D6C687FA272CD27700874C10 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + D6C68813272CD27700874C10 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + D6C68814272CD27700874C10 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + D6C68816272CD27700874C10 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZPBBSK8L8B; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Reader/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Reader; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D6C68817272CD27700874C10 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZPBBSK8L8B; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Reader/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Reader; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D6C68819272CD27700874C10 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZPBBSK8L8B; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ReaderTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Reader.app/Reader"; + }; + name = Debug; + }; + D6C6881A272CD27700874C10 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZPBBSK8L8B; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ReaderTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Reader.app/Reader"; + }; + name = Release; + }; + D6C6881C272CD27700874C10 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZPBBSK8L8B; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ReaderUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Reader; + }; + name = Debug; + }; + D6C6881D272CD27700874C10 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZPBBSK8L8B; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ReaderUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Reader; + }; + name = Release; + }; + D6C6882C272CD2BA00874C10 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ZPBBSK8L8B; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Fervor; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + D6C6882D272CD2BA00874C10 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ZPBBSK8L8B; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Fervor; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D6C687E3272CD27600874C10 /* Build configuration list for PBXProject "Reader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D6C68813272CD27700874C10 /* Debug */, + D6C68814272CD27700874C10 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D6C68815272CD27700874C10 /* Build configuration list for PBXNativeTarget "Reader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D6C68816272CD27700874C10 /* Debug */, + D6C68817272CD27700874C10 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D6C68818272CD27700874C10 /* Build configuration list for PBXNativeTarget "ReaderTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D6C68819272CD27700874C10 /* Debug */, + D6C6881A272CD27700874C10 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D6C6881B272CD27700874C10 /* Build configuration list for PBXNativeTarget "ReaderUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D6C6881C272CD27700874C10 /* Debug */, + D6C6881D272CD27700874C10 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D6C6882B272CD2BA00874C10 /* Build configuration list for PBXNativeTarget "Fervor" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D6C6882C272CD2BA00874C10 /* Debug */, + D6C6882D272CD2BA00874C10 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + D6C687F4272CD27600874C10 /* Reader.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + D6C687F5272CD27600874C10 /* Reader.xcdatamodel */, + ); + currentVersion = D6C687F5272CD27600874C10 /* Reader.xcdatamodel */; + path = Reader.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = D6C687E0272CD27600874C10 /* Project object */; +} diff --git a/Reader/AppDelegate.swift b/Reader/AppDelegate.swift new file mode 100644 index 0000000..cca9a84 --- /dev/null +++ b/Reader/AppDelegate.swift @@ -0,0 +1,81 @@ +// +// AppDelegate.swift +// Reader +// +// Created by Shadowfacts on 10/29/21. +// + +import UIKit +import CoreData + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + // MARK: - Core Data stack + + lazy var persistentContainer: NSPersistentContainer = { + /* + The persistent container for the application. This implementation + creates and returns a container, having loaded the store for the + application to it. This property is optional since there are legitimate + error conditions that could cause the creation of the store to fail. + */ + let container = NSPersistentContainer(name: "Reader") + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // 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. + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container + }() + + // MARK: - Core Data Saving support + + func saveContext () { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // 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. + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } + +} + diff --git a/Reader/Assets.xcassets/AccentColor.colorset/Contents.json b/Reader/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Reader/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Reader/Assets.xcassets/AppIcon.appiconset/Contents.json b/Reader/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Reader/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Reader/Assets.xcassets/Contents.json b/Reader/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Reader/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Reader/Base.lproj/LaunchScreen.storyboard b/Reader/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Reader/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Reader/FervorController.swift b/Reader/FervorController.swift new file mode 100644 index 0000000..48156c8 --- /dev/null +++ b/Reader/FervorController.swift @@ -0,0 +1,47 @@ +// +// FervorController.swift +// Reader +// +// Created by Shadowfacts on 11/25/21. +// + +import Foundation +import Fervor + +class FervorController { + + static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")! + + let instanceURL: URL + + private let client: FervorClient + private(set) var clientID: String? + private(set) var clientSecret: String? + private(set) var accessToken: String? + + init(instanceURL: URL) { + self.instanceURL = instanceURL + self.client = FervorClient(instanceURL: instanceURL, accessToken: nil) + } + + convenience init(account: LocalData.Account) { + self.init(instanceURL: account.instanceURL) + self.clientID = account.clientID + self.clientSecret = account.clientSecret + self.accessToken = account.accessToken + } + + func register() async throws -> ClientRegistration { + let registration = try await client.register(clientName: "Frenzy iOS", website: nil, redirectURI: FervorController.oauthRedirectURI) + clientID = registration.clientID + clientSecret = registration.clientSecret + return registration + } + + func getToken(authCode: String) async throws { + let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!) + client.accessToken = token.accessToken + accessToken = token.accessToken + } + +} diff --git a/Reader/Info.plist b/Reader/Info.plist new file mode 100644 index 0000000..0eb786d --- /dev/null +++ b/Reader/Info.plist @@ -0,0 +1,23 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/Reader/LocalData.swift b/Reader/LocalData.swift new file mode 100644 index 0000000..39960ff --- /dev/null +++ b/Reader/LocalData.swift @@ -0,0 +1,38 @@ +// +// LocalData.swift +// Reader +// +// Created by Shadowfacts on 11/25/21. +// + +import Foundation + +struct LocalData { + + private init() {} + + private static let encoder = JSONEncoder() + private static let decoder = JSONDecoder() + + static var account: Account? { + get { + guard let data = UserDefaults.standard.data(forKey: "account") else { + return nil + } + return try? decoder.decode(Account.self, from: data) + } + set { + let data = try! encoder.encode(newValue) + UserDefaults.standard.set(data, forKey: "account") + } + } + + struct Account: Codable { + let instanceURL: URL + let clientID: String + let clientSecret: String + let accessToken: String + // todo: refresh tokens + } + +} diff --git a/Reader/Reader.xcdatamodeld/.xccurrentversion b/Reader/Reader.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..ac6d697 --- /dev/null +++ b/Reader/Reader.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Reader.xcdatamodel + + diff --git a/Reader/Reader.xcdatamodeld/Reader.xcdatamodel/contents b/Reader/Reader.xcdatamodeld/Reader.xcdatamodel/contents new file mode 100644 index 0000000..50d2514 --- /dev/null +++ b/Reader/Reader.xcdatamodeld/Reader.xcdatamodel/contents @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Reader/SceneDelegate.swift b/Reader/SceneDelegate.swift new file mode 100644 index 0000000..95db367 --- /dev/null +++ b/Reader/SceneDelegate.swift @@ -0,0 +1,81 @@ +// +// SceneDelegate.swift +// Reader +// +// Created by Shadowfacts on 10/29/21. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + private(set) var fervorController: FervorController! + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let windowScene = (scene as? UIWindowScene) else { return } + + window = UIWindow(windowScene: windowScene) + + if let account = LocalData.account { + fervorController = FervorController(account: account) + createAppUI() + } else { + let loginVC = LoginViewController() + loginVC.delegate = self + window!.rootViewController = loginVC + } + + window!.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + + // Save changes in the application's managed object context when the application transitions to the background. + (UIApplication.shared.delegate as? AppDelegate)?.saveContext() + } + + private func createAppUI() { + let home = HomeViewController(fervorController: fervorController) + let nav = UINavigationController(rootViewController: home) + window!.rootViewController = nav + } + +} + +extension SceneDelegate: LoginViewControllerDelegate { + func didLogin(with controller: FervorController) { + LocalData.account = .init(instanceURL: controller.instanceURL, clientID: controller.clientID!, clientSecret: controller.clientSecret!, accessToken: controller.accessToken!) + fervorController = controller + createAppUI() + } +} diff --git a/Reader/Screens/Home/HomeViewController.swift b/Reader/Screens/Home/HomeViewController.swift new file mode 100644 index 0000000..6442df7 --- /dev/null +++ b/Reader/Screens/Home/HomeViewController.swift @@ -0,0 +1,39 @@ +// +// HomeViewController.swift +// Reader +// +// Created by Shadowfacts on 11/25/21. +// + +import UIKit + +class HomeViewController: UIViewController { + + let fervorController: FervorController + + init(fervorController: FervorController) { + self.fervorController = fervorController + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Logged in to \(fervorController.instanceURL.host!)" + view.addSubview(label) + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: view.centerXAnchor), + label.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + +} diff --git a/Reader/Screens/Login/LoginViewController.swift b/Reader/Screens/Login/LoginViewController.swift new file mode 100644 index 0000000..0d612a9 --- /dev/null +++ b/Reader/Screens/Login/LoginViewController.swift @@ -0,0 +1,104 @@ +// +// LoginViewController.swift +// Reader +// +// Created by Shadowfacts on 11/25/21. +// + +import UIKit +import AuthenticationServices +import Fervor + +protocol LoginViewControllerDelegate: AnyObject { + func didLogin(with controller: FervorController) +} + +class LoginViewController: UIViewController { + + weak var delegate: LoginViewControllerDelegate? + + private var textField: UITextField! + + private var authSession: ASWebAuthenticationSession? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.borderStyle = .roundedRect + textField.backgroundColor = .secondarySystemBackground + textField.keyboardType = .URL + textField.returnKeyType = .next + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + textField.placeholder = "example.com" + textField.addTarget(self, action: #selector(doEnteredURL), for: .primaryActionTriggered) + view.addSubview(textField) + NSLayoutConstraint.activate([ + textField.centerYAnchor.constraint(equalTo: view.centerYAnchor), + textField.centerXAnchor.constraint(equalTo: view.centerXAnchor), + textField.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.75), + ]) + } + + @objc private func doEnteredURL() { + Task { + await enteredURL() + } + } + + @MainActor + private func enteredURL() async { + guard let text = textField.text, + let components = URLComponents(string: text) else { + let alert = UIAlertController(title: "Invalid URL", message: nil, preferredStyle: .alert) + self.present(alert, animated: true) + return + } + + let controller = FervorController(instanceURL: components.url!) + + let registration: ClientRegistration + do { + registration = try await controller.register() + } catch { + let alert = UIAlertController(title: "Unable to Register Client", message: error.localizedDescription, preferredStyle: .alert) + present(alert, animated: true) + return + } + + var authorizeComponents = components + authorizeComponents.path = "/oauth/authorize" + authorizeComponents.queryItems = [ + URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "client_id", value: registration.clientID), + URLQueryItem(name: "redirect_uri", value: FervorController.oauthRedirectURI.absoluteString), + ] + authSession = ASWebAuthenticationSession(url: authorizeComponents.url!, callbackURLScheme: "frenzy") { (callbackURL, error) in + let components = URLComponents(url: callbackURL!, resolvingAgainstBaseURL: false) + guard let codeItem = components?.queryItems?.first(where: { $0.name == "code" }), + let codeValue = codeItem.value else { + fatalError() + } + Task { + try! await controller.getToken(authCode: codeValue) + DispatchQueue.main.async { + self.delegate?.didLogin(with: controller) + } + } + } + + self.authSession!.presentationContextProvider = self + self.authSession!.start() + } + +} + +extension LoginViewController: ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return self.view.window! + } +} diff --git a/ReaderTests/ReaderTests.swift b/ReaderTests/ReaderTests.swift new file mode 100644 index 0000000..841902f --- /dev/null +++ b/ReaderTests/ReaderTests.swift @@ -0,0 +1,33 @@ +// +// ReaderTests.swift +// ReaderTests +// +// Created by Shadowfacts on 10/29/21. +// + +import XCTest +@testable import Reader + +class ReaderTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/ReaderUITests/ReaderUITests.swift b/ReaderUITests/ReaderUITests.swift new file mode 100644 index 0000000..bc707ba --- /dev/null +++ b/ReaderUITests/ReaderUITests.swift @@ -0,0 +1,42 @@ +// +// ReaderUITests.swift +// ReaderUITests +// +// Created by Shadowfacts on 10/29/21. +// + +import XCTest + +class ReaderUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/ReaderUITests/ReaderUITestsLaunchTests.swift b/ReaderUITests/ReaderUITestsLaunchTests.swift new file mode 100644 index 0000000..cd2e2cb --- /dev/null +++ b/ReaderUITests/ReaderUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// ReaderUITestsLaunchTests.swift +// ReaderUITests +// +// Created by Shadowfacts on 10/29/21. +// + +import XCTest + +class ReaderUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}