diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 9210c9a..79be371 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -170,11 +170,14 @@ D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */; }; D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */; }; D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */; }; + D53415A5298C782A00FD67B1 /* ContributorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53415A4298C782A00FD67B1 /* ContributorsView.swift */; }; + D53415A7298C78BC00FD67B1 /* Contributors.plist in Resources */ = {isa = PBXBuildFile; fileRef = D53415A6298C78BC00FD67B1 /* Contributors.plist */; }; D5864970297734280081477E /* CheatMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586496F297734280081477E /* CheatMetadata.swift */; }; D586497229774ABD0081477E /* CheatBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586497129774ABD0081477E /* CheatBase.swift */; }; D5864978297756CE0081477E /* CheatBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5864977297756CE0081477E /* CheatBaseView.swift */; }; D5A98CE2284EF14B00E023E5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */; }; D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF27629884F8600F21ACF /* CheatDevice.swift */; }; + D5D797E6298D946200738869 /* Contributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D797E5298D946200738869 /* Contributor.swift */; }; D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */; }; /* End PBXBuildFile section */ @@ -373,11 +376,14 @@ D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+JIT.swift"; sourceTree = ""; }; D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerManager+Delta.swift"; sourceTree = ""; }; D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Filename.swift"; sourceTree = ""; }; + D53415A4298C782A00FD67B1 /* ContributorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorsView.swift; sourceTree = ""; }; + D53415A6298C78BC00FD67B1 /* Contributors.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Contributors.plist; sourceTree = ""; }; D586496F297734280081477E /* CheatMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatMetadata.swift; sourceTree = ""; }; D586497129774ABD0081477E /* CheatBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBase.swift; sourceTree = ""; }; D5864977297756CE0081477E /* CheatBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBaseView.swift; sourceTree = ""; }; D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; D5AAF27629884F8600F21ACF /* CheatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatDevice.swift; sourceTree = ""; }; + D5D797E5298D946200738869 /* Contributor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contributor.swift; sourceTree = ""; }; D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySearchBar.swift; sourceTree = ""; }; DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -744,6 +750,7 @@ BF11734E1DA32CEC00047DF8 /* Controllers */, BF1DAD5B1D9F574900E752A7 /* Controller Skins */, BF48F74C219A16C100BC2FC1 /* Syncing */, + D5D797E4298D944C00738869 /* Contributors */, ); path = Settings; sourceTree = ""; @@ -795,6 +802,7 @@ BF6BB2451BB73FE800CCF94A /* Assets.xcassets */, BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */, D5050A1B2989A84200ABE08D /* cheatbase.sqlite */, + D53415A6298C78BC00FD67B1 /* Contributors.plist */, ); path = Resources; sourceTree = ""; @@ -917,6 +925,15 @@ path = Cheats; sourceTree = ""; }; + D5D797E4298D944C00738869 /* Contributors */ = { + isa = PBXGroup; + children = ( + D5D797E5298D946200738869 /* Contributor.swift */, + D53415A4298C782A00FD67B1 /* ContributorsView.swift */, + ); + path = Contributors; + sourceTree = ""; + }; FD1E8AE87FA2DB8793F7B937 /* Pods */ = { isa = PBXGroup; children = ( @@ -1023,6 +1040,7 @@ BF3540001C5DA3C500C1184C /* PausePresentationControllerContentView.xib in Resources */, BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */, BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */, + D53415A7298C78BC00FD67B1 /* Contributors.plist in Resources */, BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */, BFFC46461D59861000AF2CC6 /* LaunchScreen.storyboard in Resources */, D5050A1C2989A84200ABE08D /* cheatbase.sqlite in Resources */, @@ -1183,6 +1201,7 @@ BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */, BFC3628021ADE2BA00EF2BE6 /* UIAlertController+Error.swift in Sources */, BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */, + D5D797E6298D946200738869 /* Contributor.swift in Sources */, BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */, BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */, D586497229774ABD0081477E /* CheatBase.swift in Sources */, @@ -1193,6 +1212,7 @@ BF59426B1E09BBD00051894B /* GridCollectionViewLayout.swift in Sources */, BF6424851F5CBDC900D6AB44 /* UIView+ParentViewController.swift in Sources */, BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */, + D53415A5298C782A00FD67B1 /* ContributorsView.swift in Sources */, BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */, BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */, D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */, diff --git a/Delta/Base.lproj/Settings.storyboard b/Delta/Base.lproj/Settings.storyboard index 36583b0..ded0fc2 100644 --- a/Delta/Base.lproj/Settings.storyboard +++ b/Delta/Base.lproj/Settings.storyboard @@ -643,6 +643,22 @@ + + + + + + + + + + + diff --git a/Delta/Settings/Contributors/Contributor.swift b/Delta/Settings/Contributors/Contributor.swift new file mode 100644 index 0000000..f94c388 --- /dev/null +++ b/Delta/Settings/Contributors/Contributor.swift @@ -0,0 +1,45 @@ +// +// Contributor.swift +// Delta +// +// Created by Riley Testut on 2/3/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +struct Contributor: Identifiable, Decodable +{ + var name: String + + var id: String { + // Use names as identifiers for now. + return self.name + } + + var url: URL? { + guard let link = self.link, let url = URL(string: link) else { return nil } + return url + } + private var link: String? + + var linkName: String? + + var contributions: [Contribution] +} + +struct Contribution: Identifiable, Decodable +{ + var name: String + + var id: String { + // Use names as identifiers for now. + return self.name + } + + var url: URL? { + guard let link = self.link, let url = URL(string: link) else { return nil } + return url + } + private var link: String? +} diff --git a/Delta/Settings/Contributors/ContributorsView.swift b/Delta/Settings/Contributors/ContributorsView.swift new file mode 100644 index 0000000..13b28d8 --- /dev/null +++ b/Delta/Settings/Contributors/ContributorsView.swift @@ -0,0 +1,200 @@ +// +// ContributionsView.swift +// Delta +// +// Created by Riley Testut on 2/2/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import SwiftUI +import SafariServices + +@available(iOS 14, *) +private extension NavigationLink where Label == EmptyView, Destination == EmptyView +{ + // Copied from https://stackoverflow.com/a/66891173 + static var empty: NavigationLink { + self.init(destination: EmptyView(), label: { EmptyView() }) + } +} + +@available(iOS 14, *) +extension ContributorsView +{ + fileprivate class ViewModel: ObservableObject + { + @Published + var contributors: [Contributor]? + + @Published + var error: Error? + + @Published + var webViewURL: URL? + + weak var hostingController: UIViewController? + + func loadContributors() + { + guard self.contributors == nil else { return } + + do + { + let fileURL = Bundle.main.url(forResource: "Contributors", withExtension: "plist")! + let data = try Data(contentsOf: fileURL) + + let contributors = try PropertyListDecoder().decode([Contributor].self, from: data) + self.contributors = contributors + } + catch + { + self.error = error + } + } + } + + static func makeViewController() -> UIHostingController + { + let viewModel = ViewModel() + let contributorsView = ContributorsView(viewModel: viewModel) + + let hostingController = UIHostingController(rootView: contributorsView) + hostingController.title = NSLocalizedString("Contributors", comment: "") + + viewModel.hostingController = hostingController + + return hostingController + } +} + +@available(iOS 14, *) +struct ContributorsView: View +{ + @StateObject + private var viewModel: ViewModel + + @State + private var showErrorAlert: Bool = false + + var body: some View { + List { + Section(content: {}, footer: { + Text("These individuals have contributed to the open-source Delta project on GitHub.\n\nThank you to all our contributors, your help is much appreciated 💜") + .font(.subheadline) + }) + + ForEach(viewModel.contributors ?? []) { contributor in + Section { + // First row = contributor + ContributionCell(name: Text(contributor.name).bold(), url: contributor.url, linkName: contributor.linkName) { webViewURL in + viewModel.webViewURL = webViewURL + } + + // Remaining rows = contributions + ForEach(contributor.contributions) { contribution in + ContributionCell(name: Text(contribution.name), url: contribution.url) { webViewURL in + viewModel.webViewURL = webViewURL + } + } + } + } + } + .listStyle(.grouped) // TODO: Change to .insetGrouped once we drop iOS 13 support. + .environmentObject(viewModel) + .alert(isPresented: $showErrorAlert) { + Alert(title: Text("Unable to Load Contributors"), message: Text(viewModel.error?.localizedDescription ?? ""), dismissButton: .default(Text("OK")) { + guard let hostingController = viewModel.hostingController else { return } + hostingController.navigationController?.popViewController(animated: true) + }) + } + .onReceive(viewModel.$error) { error in + guard error != nil else { return } + showErrorAlert = true + } + .onReceive(viewModel.$webViewURL) { webViewURL in + guard let webViewURL else { return } + openURL(webViewURL) + } + .onAppear { + viewModel.loadContributors() + } + } + + fileprivate init(contributors: [Contributor]? = nil, viewModel: ViewModel = ViewModel()) + { + if let contributors + { + // Don't overwrite passed-in viewModel.contributors if contributors is nil. + viewModel.contributors = contributors + } + + self._viewModel = StateObject(wrappedValue: viewModel) + } +} + +@available(iOS 14, *) +struct ContributionCell: View +{ + var name: Text + var url: URL? + var linkName: String? + + var action: (URL) -> Void + + var body: some View { + + let body = Button { + guard let url else { return } + + Task { @MainActor in + // Dispatch Task to avoid "Publishing changes from within view updates is not allowed, this will cause undefined behavior." runtime error on iOS 16. + self.action(url) + } + + } label: { + HStack { + self.name + .font(.system(size: 17)) // Match Settings screen + + Spacer() + + if let linkName + { + Text(linkName) + .font(.system(size: 17)) // Match Settings screen + .foregroundColor(.gray) + } + + if url != nil + { + NavigationLink.empty + .fixedSize() + } + } + } + .accentColor(.primary) + + if url != nil + { + body + } + else + { + // No URL to open, so disable cell highlighting. + body.buttonStyle(.plain) + } + } +} + +@available(iOS 14, *) +private extension ContributorsView +{ + func openURL(_ url: URL) + { + guard let hostingController = viewModel.hostingController else { return } + + let safariViewController = SFSafariViewController(url: url) + safariViewController.preferredControlTintColor = .deltaPurple + hostingController.present(safariViewController, animated: true) + } +} diff --git a/Delta/Settings/SettingsViewController.swift b/Delta/Settings/SettingsViewController.swift index 73c43d6..6a34705 100644 --- a/Delta/Settings/SettingsViewController.swift +++ b/Delta/Settings/SettingsViewController.swift @@ -49,6 +49,7 @@ private extension SettingsViewController case caroline case grant case litRitt + case contributors case softwareLicenses } } @@ -279,6 +280,13 @@ private extension SettingsViewController } } } + + @available(iOS 14, *) + func showContributors() + { + let hostingController = ContributorsView.makeViewController() + self.navigationController?.pushViewController(hostingController, animated: true) + } } private extension SettingsViewController @@ -418,6 +426,10 @@ extension SettingsViewController case .caroline: self.openTwitter(username: "1carolinemoore") case .grant: self.openTwitter(username: "grantgliner") case .litRitt: self.openTwitter(username: "litritt_z") + case .contributors: + guard #available(iOS 14, *) else { return } + self.showContributors() + case .softwareLicenses: break } } @@ -425,13 +437,35 @@ extension SettingsViewController override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + primary: switch Section(rawValue: indexPath.section)! { - #if !BETA - case .credits where indexPath.row == CreditsRow.litRitt.rawValue: return 0.0 - #endif - default: return super.tableView(tableView, heightForRowAt: indexPath) + case .credits: + let row = CreditsRow(rawValue: indexPath.row)! + switch row + { + case .grant: + // Hide row on iOS 14 and above + guard #available(iOS 14, *) else { break primary } + return 0.0 + + case .litRitt: + // Hide row on iOS 14 and above + guard #available(iOS 14, *) else { break primary } + return 0.0 + + case .contributors: + // Hide row on iOS 13 and below + guard #unavailable(iOS 14) else { break primary } + return 0.0 + + default: break + } + + default: break } + + return super.tableView(tableView, heightForRowAt: indexPath) } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? diff --git a/Resources/Contributors.plist b/Resources/Contributors.plist new file mode 100644 index 0000000..e35ff2b --- /dev/null +++ b/Resources/Contributors.plist @@ -0,0 +1,70 @@ + + + + + + name + Grant Gliner + link + https://twitter.com/GrantGliner + linkName + @GrantGliner + contributions + + + name + Pause Menu Icons + + + + + name + Eric Lewis + link + https://github.com/ericlewis + linkName + github.com/ericlewis + contributions + + + name + Dark Mode + link + https://github.com/rileytestut/Delta/pull/82 + + + + + name + Chris Rittenhouse + link + https://github.com/LitRitt + linkName + github.com/LitRitt + contributions + + + name + Genesis Skin + + + + + name + Noah Keck + link + https://github.com/noah978 + linkName + github.com/noah978 + contributions + + + name + CheatBase + link + https://github.com/CheatBase/CheatBase + + + + +