diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index e5c8a3f..f891927 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; }; 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; }; + 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; }; 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; }; 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; @@ -21,11 +22,14 @@ 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; + 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; }; + 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; }; 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; }; 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7376EF28A97D1400D60918 /* SwiftUIX */; }; 0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */; }; 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */; }; 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; }; + 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78041C28BFB3EA001E8CA3 /* String.swift */; }; 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; }; 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */; }; 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */; }; @@ -76,6 +80,7 @@ /* Begin PBXFileReference section */ 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = ""; }; 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = ""; }; + 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = ""; }; 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = ""; }; 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; @@ -85,9 +90,12 @@ 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchProgressView.swift; sourceTree = ""; }; + 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; + 0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = ""; }; 0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = ""; }; 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = ""; }; 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = ""; }; + 0C78041C28BFB3EA001E8CA3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = ""; }; 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceView.swift; sourceTree = ""; }; 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogView.swift; sourceTree = ""; }; @@ -180,6 +188,7 @@ 0CA148C4288903F000DE2211 /* RealDebridModels.swift */, 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */, + 0C68135128BC1A7C00FAD890 /* GithubModels.swift */, ); path = Models; sourceTree = ""; @@ -201,6 +210,7 @@ 0CA05456288EE58200850554 /* SettingsSourceListView.swift */, 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */, 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */, + 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */, ); path = SettingsViews; sourceTree = ""; @@ -251,6 +261,7 @@ 0CA148CB288903F000DE2211 /* Task.swift */, 0C32FB542890D1BF002BD219 /* UIApplication.swift */, 0C7D11FD28AA03FE00ED92DB /* View.swift */, + 0C78041C28BFB3EA001E8CA3 /* String.swift */, ); path = Extensions; sourceTree = ""; @@ -301,6 +312,7 @@ isa = PBXGroup; children = ( 0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */, + 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */, ); path = API; sourceTree = ""; @@ -444,9 +456,11 @@ 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, 0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */, + 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */, 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, + 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */, 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, @@ -458,7 +472,9 @@ 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */, 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */, + 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */, 0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */, + 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */, 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */, 0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */, 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, diff --git a/Ferrite/API/GithubWrapper.swift b/Ferrite/API/GithubWrapper.swift new file mode 100644 index 0000000..294164e --- /dev/null +++ b/Ferrite/API/GithubWrapper.swift @@ -0,0 +1,28 @@ +// +// GithubWrapper.swift +// Ferrite +// +// Created by Brian Dashore on 8/28/22. +// + +import Foundation + +public class Github { + public func fetchLatestRelease() async throws -> GithubRelease? { + let url = URL(string: "https://api.github.com/repos/bdashore3/Ferrite/releases/latest")! + + let (data, _) = try await URLSession.shared.data(from: url) + + let rawResponse = try JSONDecoder().decode(GithubRelease.self, from: data) + return rawResponse + } + + public func fetchReleases() async throws -> [GithubRelease]? { + let url = URL(string: "https://api.github.com/repos/bdashore3/Ferrite/releases")! + + let (data, _) = try await URLSession.shared.data(from: url) + + let rawResponse = try JSONDecoder().decode([GithubRelease].self, from: data) + return rawResponse + } +} diff --git a/Ferrite/Extensions/String.swift b/Ferrite/Extensions/String.swift new file mode 100644 index 0000000..b53d3ce --- /dev/null +++ b/Ferrite/Extensions/String.swift @@ -0,0 +1,42 @@ +// +// String.swift +// Ferrite +// +// Created by Brian Dashore on 8/31/22. +// +// From https://stackoverflow.com/a/59307884 +// + +import Foundation + +extension String { + private func compare(toVersion targetVersion: String) -> ComparisonResult { + let versionDelimiter = "." + var result: ComparisonResult = .orderedSame + var versionComponents = components(separatedBy: versionDelimiter) + var targetComponents = targetVersion.components(separatedBy: versionDelimiter) + + while versionComponents.count < targetComponents.count { + versionComponents.append("0") + } + + while targetComponents.count < versionComponents.count { + targetComponents.append("0") + } + + for (version, target) in zip(versionComponents, targetComponents) { + result = version.compare(target, options: .numeric) + if result != .orderedSame { + break + } + } + + return result + } + + static func ==(lhs: String, rhs: String) -> Bool { lhs.compare(toVersion: rhs) == .orderedSame } + static func <(lhs: String, rhs: String) -> Bool { lhs.compare(toVersion: rhs) == .orderedAscending } + static func <=(lhs: String, rhs: String) -> Bool { lhs.compare(toVersion: rhs) != .orderedDescending } + static func >(lhs: String, rhs: String) -> Bool { lhs.compare(toVersion: rhs) == .orderedDescending } + static func >=(lhs: String, rhs: String) -> Bool { lhs.compare(toVersion: rhs) != .orderedAscending } +} diff --git a/Ferrite/Models/GithubModels.swift b/Ferrite/Models/GithubModels.swift new file mode 100644 index 0000000..575dba5 --- /dev/null +++ b/Ferrite/Models/GithubModels.swift @@ -0,0 +1,18 @@ +// +// GithubModels.swift +// Ferrite +// +// Created by Brian Dashore on 8/28/22. +// + +import Foundation + +public struct GithubRelease: Codable, Hashable { + let htmlUrl: String + let tagName: String + + enum CodingKeys: String, CodingKey { + case htmlUrl = "html_url" + case tagName = "tag_name" + } +} diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 9494aa9..b1a05a9 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -26,6 +26,7 @@ public class DebridManager: ObservableObject { UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled") } } + @Published var realDebridAuthProcessing: Bool = false @Published var realDebridAuthUrl: String = "" diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 901e6ba..75b8333 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -897,7 +897,7 @@ class ScrapingViewModel: ObservableObject { responseArray.append("client ID") } - if clientIdReset && clientSecretReset { + if clientIdReset, clientSecretReset { responseArray.append("and") } diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index 8d61948..072a829 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -59,26 +59,7 @@ public class SourceManager: ObservableObject { return true } - var splitCurrentVersion = UIApplication.shared.appVersion - .split(separator: ".") - .map { Int($0) ?? 0 } - - if splitCurrentVersion.count < 3 { - splitCurrentVersion += [Int](repeating: 0, count: 3 - splitCurrentVersion.count) - } - - var splitMinVersion = minVersion - .split(separator: ".") - .map { Int($0) ?? 0 } - - if splitMinVersion.count < 3 { - splitMinVersion += [Int](repeating: 0, count: 3 - splitMinVersion.count) - } - - let combined = zip(splitCurrentVersion, splitMinVersion) - return combined.allSatisfy({ part, minPart in - part >= minPart - }) + return UIApplication.shared.appVersion >= minVersion } // Fetches sources using the background context diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 807b442..693795c 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -13,6 +13,13 @@ struct MainView: View { @EnvironmentObject var toastModel: ToastViewModel @EnvironmentObject var debridManager: DebridManager + @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true + + @State private var showUpdateAlert = false + @State private var releaseVersionString: String = "" + @State private var releaseUrlString: String = "" + @State private var viewTask: Task? + var body: some View { TabView(selection: $navModel.selectedTab) { ContentView() @@ -33,6 +40,45 @@ struct MainView: View { } .tag(ViewTab.settings) } + .alert(isPresented: $showUpdateAlert) { + Alert( + title: Text("Update available"), + message: Text("Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings."), + primaryButton: .default(Text("Download")) { + guard let releaseUrl = URL(string: releaseUrlString) else { + return + } + + UIApplication.shared.open(releaseUrl) + }, + secondaryButton: .cancel() + ) + } + .onAppear { + if autoUpdateNotifs { + viewTask = Task { + do { + guard let latestRelease = try await Github().fetchLatestRelease() else { + toastModel.updateToastDescription("Github error: No releases found") + return + } + + let releaseVersion = String(latestRelease.tagName.dropFirst()) + if releaseVersion > UIApplication.shared.appVersion { + print("Greater") + releaseVersionString = latestRelease.tagName + releaseUrlString = latestRelease.htmlUrl + showUpdateAlert.toggle() + } + } catch { + toastModel.updateToastDescription("Github error: \(error)") + } + } + } + } + .onDisappear { + viewTask?.cancel() + } .overlay { VStack { Spacer() diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 5eac649..2dcd3fd 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -13,6 +13,8 @@ struct SettingsView: View { let backgroundContext = PersistenceController.shared.backgroundContext + @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true + @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none @@ -92,9 +94,15 @@ struct SettingsView: View { ) } + Section(header: Text("Updates")) { + Toggle(isOn: $autoUpdateNotifs) { + Text("Show update alerts") + } + NavigationLink("Version history", destination: SettingsAppVersionView()) + } + Section { ListRowLinkView(text: "Report issues", link: "https://github.com/bdashore3/Ferrite/issues") - NavigationLink("About", destination: AboutView()) } } diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift new file mode 100644 index 0000000..eaf87eb --- /dev/null +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -0,0 +1,50 @@ +// +// SettingsAppVersionView.swift +// Ferrite +// +// Created by Brian Dashore on 8/29/22. +// + +import SwiftUI + +struct SettingsAppVersionView: View { + @EnvironmentObject var toastModel: ToastViewModel + + @State private var viewTask: Task? + @State private var releases: [GithubRelease] = [] + + var body: some View { + List { + Section(header: Text("GitHub links")) { + ForEach(releases, id: \.self) { release in + ListRowLinkView(text: release.tagName, link: release.htmlUrl) + } + } + } + .onAppear { + viewTask = Task { + do { + if let fetchedReleases = try await Github().fetchReleases() { + releases = fetchedReleases + } else { + toastModel.updateToastDescription("Github error: No releases found") + } + } catch { + toastModel.updateToastDescription("Github error: \(error)") + } + } + } + .onDisappear { + viewTask?.cancel() + } + .listStyle(.insetGrouped) + .navigationTitle("Version history") + .navigationBarTitleDisplayMode(.inline) + } +} + +struct SettingsAppVersionView_Previews: PreviewProvider { + static var previews: some View { + SettingsAppVersionView() + } +} diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index c3bd4d8..38f3618 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -22,31 +22,33 @@ struct SettingsSourceListView: View { var body: some View { List { - ForEach(sourceLists, id: \.self) { sourceList in - VStack(alignment: .leading, spacing: 5) { - Text(sourceList.name) + Section(header: Text("List information")) { + ForEach(sourceLists, id: \.self) { sourceList in + VStack(alignment: .leading, spacing: 5) { + Text(sourceList.name) - Text(sourceList.author) - .foregroundColor(.gray) + Text(sourceList.author) + .foregroundColor(.gray) - Text("ID: \(sourceList.id)") - .font(.caption) - .foregroundColor(.gray) - } - .contextMenu { - Button { - navModel.selectedSourceList = sourceList - presentSourceSheet.toggle() - } label: { - Text("Edit") - Image(systemName: "pencil") + Text("ID: \(sourceList.id)") + .font(.caption) + .foregroundColor(.gray) } + .contextMenu { + Button { + navModel.selectedSourceList = sourceList + presentSourceSheet.toggle() + } label: { + Text("Edit") + Image(systemName: "pencil") + } - Button { - PersistenceController.shared.delete(sourceList, context: backgroundContext) - } label: { - Text("Remove") - Image(systemName: "trash") + Button { + PersistenceController.shared.delete(sourceList, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } } } } @@ -61,6 +63,7 @@ struct SettingsSourceListView: View { } } .navigationTitle("Source lists") + .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button {