Ferrite: Add updater

Updates are sent via an alert on starting the app. This can be
disabled in the settings menu.

A full version struct has been completed for flexible comparisons.

Version history can also be viewed in settings in case a user wants
to download an earlier version of the app.

Updates track Github releases.

Signed-off-by: kingbri <bdashore3@gmail.com>
This commit is contained in:
kingbri 2022-08-31 00:30:27 -04:00 committed by kingbri
parent 1bf64a8934
commit 664c57b751
11 changed files with 236 additions and 43 deletions

View file

@ -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 = "<group>"; };
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
@ -85,9 +90,12 @@
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = "<group>"; };
0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchProgressView.swift; sourceTree = "<group>"; };
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = "<group>"; };
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C78041C28BFB3EA001E8CA3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = "<group>"; };
0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceView.swift; sourceTree = "<group>"; };
0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogView.swift; sourceTree = "<group>"; };
@ -180,6 +188,7 @@
0CA148C4288903F000DE2211 /* RealDebridModels.swift */,
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
);
path = Models;
sourceTree = "<group>";
@ -201,6 +210,7 @@
0CA05456288EE58200850554 /* SettingsSourceListView.swift */,
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */,
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */,
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
);
path = SettingsViews;
sourceTree = "<group>";
@ -251,6 +261,7 @@
0CA148CB288903F000DE2211 /* Task.swift */,
0C32FB542890D1BF002BD219 /* UIApplication.swift */,
0C7D11FD28AA03FE00ED92DB /* View.swift */,
0C78041C28BFB3EA001E8CA3 /* String.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -301,6 +312,7 @@
isa = PBXGroup;
children = (
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
);
path = API;
sourceTree = "<group>";
@ -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 */,

View file

@ -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
}
}

View file

@ -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 }
}

View file

@ -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"
}
}

View file

@ -26,6 +26,7 @@ public class DebridManager: ObservableObject {
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
}
}
@Published var realDebridAuthProcessing: Bool = false
@Published var realDebridAuthUrl: String = ""

View file

@ -897,7 +897,7 @@ class ScrapingViewModel: ObservableObject {
responseArray.append("client ID")
}
if clientIdReset && clientSecretReset {
if clientIdReset, clientSecretReset {
responseArray.append("and")
}

View file

@ -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

View file

@ -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<Void, Never>?
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()

View file

@ -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())
}
}

View file

@ -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<Void, Never>?
@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()
}
}

View file

@ -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 {