diff --git a/Sora.xcodeproj/project.pbxproj b/Sora.xcodeproj/project.pbxproj index 1fd50df..27d5a22 100644 --- a/Sora.xcodeproj/project.pbxproj +++ b/Sora.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */; }; 1352BA712D1ABC30000A9AF9 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1352BA702D1ABC30000A9AF9 /* URLSession.swift */; }; 13B3A4B22D1477F100BCC0D5 /* StorageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B3A4B12D1477F100BCC0D5 /* StorageSettingsView.swift */; }; + 13C9821F2D2152B1007A0132 /* GitHubRelease.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C9821E2D2152B1007A0132 /* GitHubRelease.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -77,6 +78,7 @@ 1352BA6F2D1AB113000A9AF9 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = ""; }; 1352BA702D1ABC30000A9AF9 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; 13B3A4B12D1477F100BCC0D5 /* StorageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSettingsView.swift; sourceTree = ""; }; + 13C9821E2D2152B1007A0132 /* GitHubRelease.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubRelease.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -152,6 +154,7 @@ 132417912D1319E800B4F2D2 /* Utils */ = { isa = PBXGroup; children = ( + 13C9821D2D2152A0007A0132 /* GitHub */, 1308CFBA2D19843E004CD38C /* CustomPlayer */, 132417922D1319E800B4F2D2 /* Miru */, 132417942D1319E800B4F2D2 /* Extensions */, @@ -276,6 +279,14 @@ path = EpisodesCell; sourceTree = ""; }; + 13C9821D2D2152A0007A0132 /* GitHub */ = { + isa = PBXGroup; + children = ( + 13C9821E2D2152B1007A0132 /* GitHubRelease.swift */, + ); + path = GitHub; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -381,6 +392,7 @@ 1324179E2D1319E800B4F2D2 /* MiruDataStruct.swift in Sources */, 1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */, 132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */, + 13C9821F2D2152B1007A0132 /* GitHubRelease.swift in Sources */, 1352BA712D1ABC30000A9AF9 /* URLSession.swift in Sources */, 132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */, ); @@ -528,7 +540,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -561,7 +573,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; diff --git a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate index e2cb642..c41b391 100644 Binary files a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate and b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sora/ContentView.swift b/Sora/ContentView.swift index eaeea09..28f9a7a 100644 --- a/Sora/ContentView.swift +++ b/Sora/ContentView.swift @@ -29,6 +29,84 @@ struct ContentView: View { Label("Settings", systemImage: "gearshape") } } + .onAppear { + checkForUpdate() + } + } + + func checkForUpdate() { + fetchLatestRelease { release in + guard let release = release else { return } + + let latestVersion = release.tagName + let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" + + if latestVersion.compare(currentVersion, options: .numeric) == .orderedDescending { + DispatchQueue.main.async { + showUpdateAlert(release: release) + } + } + } + } + + func fetchLatestRelease(completion: @escaping (GitHubRelease?) -> Void) { + let url = URL(string: "https://api.github.com/repos/cranci1/Sora/releases/latest")! + + URLSession.shared.dataTask(with: url) { data, response, error in + guard let data = data, error == nil else { + completion(nil) + return + } + + let release = try? JSONDecoder().decode(GitHubRelease.self, from: data) + completion(release) + }.resume() + } + + func showUpdateAlert(release: GitHubRelease) { + let alert = UIAlertController(title: "Update Available", message: "A new version (\(release.tagName)) is available. Would you like to update Sora?", preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "Update", style: .default, handler: { _ in + self.showInstallOptionsAlert(release: release) + })) + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alert, animated: true, completion: nil) + } + } + + func showInstallOptionsAlert(release: GitHubRelease) { + let installAlert = UIAlertController(title: "Install Update", message: "Choose an installation method:", preferredStyle: .alert) + + let downloadUrl = release.assets.first?.browserDownloadUrl ?? "" + + installAlert.addAction(UIAlertAction(title: "Install in AltStore", style: .default, handler: { _ in + if let url = URL(string: "altstore://install?url=\(downloadUrl)") { + UIApplication.shared.open(url) + } + })) + + installAlert.addAction(UIAlertAction(title: "Install in Sidestore", style: .default, handler: { _ in + if let url = URL(string: "sidestore://install?url=\(downloadUrl)") { + UIApplication.shared.open(url) + } + })) + + installAlert.addAction(UIAlertAction(title: "Open in Safari", style: .default, handler: { _ in + if let url = URL(string: downloadUrl) { + UIApplication.shared.open(url) + } + })) + + installAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(installAlert, animated: true, completion: nil) + } } } diff --git a/Sora/Info.plist b/Sora/Info.plist index 81f2e0b..305d2ec 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -15,9 +15,9 @@ - UIBackgroundModes - - audio - + UIBackgroundModes + + audio + diff --git a/Sora/Utils/GitHub/GitHubRelease.swift b/Sora/Utils/GitHub/GitHubRelease.swift new file mode 100644 index 0000000..4803aa3 --- /dev/null +++ b/Sora/Utils/GitHub/GitHubRelease.swift @@ -0,0 +1,124 @@ +// +// GitHubRelease.swift +// Sora +// +// Created by Francesco on 29/12/24. +// + +import Foundation + +struct GitHubRelease: Codable { + let url: String + let assetsUrl: String + let uploadUrl: String + let htmlUrl: String + let id: Int + let author: Author + let nodeId: String + let tagName: String + let targetCommitish: String + let name: String + let draft: Bool + let prerelease: Bool + let createdAt: String + let publishedAt: String + let assets: [Asset] + let tarballUrl: String + let zipballUrl: String + let body: String + + enum CodingKeys: String, CodingKey { + case url + case assetsUrl = "assets_url" + case uploadUrl = "upload_url" + case htmlUrl = "html_url" + case id + case author + case nodeId = "node_id" + case tagName = "tag_name" + case targetCommitish = "target_commitish" + case name + case draft + case prerelease + case createdAt = "created_at" + case publishedAt = "published_at" + case assets + case tarballUrl = "tarball_url" + case zipballUrl = "zipball_url" + case body + } + + struct Author: Codable { + let login: String + let id: Int + let nodeId: String + let avatarUrl: String + let gravatarId: String + let url: String + let htmlUrl: String + let followersUrl: String + let followingUrl: String + let gistsUrl: String + let starredUrl: String + let subscriptionsUrl: String + let organizationsUrl: String + let reposUrl: String + let eventsUrl: String + let receivedEventsUrl: String + let type: String + let siteAdmin: Bool + + enum CodingKeys: String, CodingKey { + case login + case id + case nodeId = "node_id" + case avatarUrl = "avatar_url" + case gravatarId = "gravatar_id" + case url + case htmlUrl = "html_url" + case followersUrl = "followers_url" + case followingUrl = "following_url" + case gistsUrl = "gists_url" + case starredUrl = "starred_url" + case subscriptionsUrl = "subscriptions_url" + case organizationsUrl = "organizations_url" + case reposUrl = "repos_url" + case eventsUrl = "events_url" + case receivedEventsUrl = "received_events_url" + case type + case siteAdmin = "site_admin" + } + } + + struct Asset: Codable { + let url: String + let id: Int + let nodeId: String + let name: String + let label: String? + let uploader: Author + let contentType: String + let state: String + let size: Int + let downloadCount: Int + let createdAt: String + let updatedAt: String + let browserDownloadUrl: String + + enum CodingKeys: String, CodingKey { + case url + case id + case nodeId = "node_id" + case name + case label + case uploader + case contentType = "content_type" + case state + case size + case downloadCount = "download_count" + case createdAt = "created_at" + case updatedAt = "updated_at" + case browserDownloadUrl = "browser_download_url" + } + } +}