diff --git a/Open Wallpaper Engine.xcodeproj/project.pbxproj b/Open Wallpaper Engine.xcodeproj/project.pbxproj index ff5aec8..e3c85d8 100644 --- a/Open Wallpaper Engine.xcodeproj/project.pbxproj +++ b/Open Wallpaper Engine.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ BB0000000000000000000003 /* ObjCExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000002 /* ObjCExceptionCatcher.m */; }; EE0000000000000000000001 /* WELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0000000000000000000002 /* WELogger.swift */; }; CC0000000000000000000001 /* WallpaperDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000000000000000000002 /* WallpaperDirectory.swift */; }; + CC000000000000000000000D /* UpdateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000000000000000000000E /* UpdateService.swift */; }; DD0000000000000000000001 /* WallpaperWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000000000000000000002 /* WallpaperWindowManager.swift */; }; CC0000000000000000000003 /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000000000000000000004 /* ZipImporter.swift */; }; CC0000000000000000000005 /* SteamCmdService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000000000000000000006 /* SteamCmdService.swift */; }; @@ -129,6 +130,7 @@ BB0000000000000000000002 /* ObjCExceptionCatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ObjCExceptionCatcher.m; sourceTree = ""; }; EE0000000000000000000002 /* WELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WELogger.swift; sourceTree = ""; }; CC0000000000000000000002 /* WallpaperDirectory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperDirectory.swift; sourceTree = ""; }; + CC000000000000000000000E /* UpdateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateService.swift; sourceTree = ""; }; DD0000000000000000000002 /* WallpaperWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperWindowManager.swift; sourceTree = ""; }; CC0000000000000000000004 /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = ""; }; CC0000000000000000000006 /* SteamCmdService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteamCmdService.swift; sourceTree = ""; }; @@ -220,6 +222,7 @@ BB0000000000000000000002 /* ObjCExceptionCatcher.m */, EE0000000000000000000002 /* WELogger.swift */, CC0000000000000000000002 /* WallpaperDirectory.swift */, + CC000000000000000000000E /* UpdateService.swift */, DD0000000000000000000002 /* WallpaperWindowManager.swift */, CC0000000000000000000004 /* ZipImporter.swift */, CC0000000000000000000006 /* SteamCmdService.swift */, @@ -470,6 +473,7 @@ BB0000000000000000000003 /* ObjCExceptionCatcher.m in Sources */, EE0000000000000000000001 /* WELogger.swift in Sources */, CC0000000000000000000001 /* WallpaperDirectory.swift in Sources */, + CC000000000000000000000D /* UpdateService.swift in Sources */, DD0000000000000000000001 /* WallpaperWindowManager.swift in Sources */, CC0000000000000000000003 /* ZipImporter.swift in Sources */, CC0000000000000000000005 /* SteamCmdService.swift in Sources */, @@ -705,4 +709,4 @@ /* End XCConfigurationList section */ }; rootObject = 5EC47E6E2A2DB2E600120CCC /* Project object */; -} +} \ No newline at end of file diff --git a/Open Wallpaper Engine/AppDelegate.swift b/Open Wallpaper Engine/AppDelegate.swift index ab5b0a9..125f0ba 100644 --- a/Open Wallpaper Engine/AppDelegate.swift +++ b/Open Wallpaper Engine/AppDelegate.swift @@ -49,7 +49,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { func applicationDidFinishLaunching(_ notification: Notification) { wallpaperWindowManager.showAll() - + UpdateService.shared.checkForUpdates() } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { diff --git a/Open Wallpaper Engine/MenuBars/StatusBar.swift b/Open Wallpaper Engine/MenuBars/StatusBar.swift index 8a06cf0..a8a7e4f 100644 --- a/Open Wallpaper Engine/MenuBars/StatusBar.swift +++ b/Open Wallpaper Engine/MenuBars/StatusBar.swift @@ -26,6 +26,10 @@ extension AppDelegate { openMainWindow() } + @objc func checkForUpdates() { + UpdateService.shared.checkForUpdates(userInitiated: true) + } + @objc func openSupportWebpage() { NSWorkspace.shared.open(URL(string: "https://github.com/haren724/open-wallpaper-engine-mac/wiki")!) } @@ -86,6 +90,11 @@ extension AppDelegate { .separator(), + .init(title: String(localized: "Check for Updates"), + systemImage: "arrow.down.circle", + action: #selector(checkForUpdates), + keyEquivalent: "u"), + .init(title: String(localized: "Support & FAQ"), systemImage: "person.fill.questionmark", action: #selector(openSupportWebpage), @@ -145,10 +154,10 @@ extension AppDelegate { .sink { [weak self] rate in guard let menu = self?.statusItem.menu else { return } let isPaused = rate == 0 - if let idx = menu.items.firstIndex(where: { $0.title == "Pause" || $0.title == "Resume" }) { + if let idx = menu.items.firstIndex(where: { $0.title == String(localized: "Pause") || $0.title == String(localized: "Resume") }) { menu.items[idx] = isPaused - ? .init(title: "Resume", systemImage: "play.fill", action: #selector(AppDelegate.shared.resume), keyEquivalent: "p") - : .init(title: "Pause", systemImage: "pause.fill", action: #selector(AppDelegate.shared.pause), keyEquivalent: "p") + ? .init(title: String(localized: "Resume"), systemImage: "play.fill", action: #selector(AppDelegate.shared.resume), keyEquivalent: "p") + : .init(title: String(localized: "Pause"), systemImage: "pause.fill", action: #selector(AppDelegate.shared.pause), keyEquivalent: "p") } } .store(in: &cancellables) @@ -163,4 +172,4 @@ extension AppDelegate: NSMenuDelegate { recentItem.submenu = buildRecentWallpapersMenu() } } -} +} \ No newline at end of file diff --git a/Open Wallpaper Engine/Services/UpdateService.swift b/Open Wallpaper Engine/Services/UpdateService.swift new file mode 100644 index 0000000..c7e786e --- /dev/null +++ b/Open Wallpaper Engine/Services/UpdateService.swift @@ -0,0 +1,82 @@ +import Foundation +import AppKit + +class UpdateService { + static let shared = UpdateService() + + private let releasesURL = "https://api.github.com/repos/parrotxray/mac-wallpaperengine/releases/latest" + + private var currentVersion: String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" + } + + func checkForUpdates(userInitiated: Bool = false) { + guard let url = URL(string: releasesURL) else { return } + var request = URLRequest(url: url) + request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") + + URLSession.shared.dataTask(with: request) { [weak self] data, _, error in + guard let self, + let data, + error == nil, + let release = try? JSONDecoder().decode(GHRelease.self, from: data) else { + if userInitiated { + DispatchQueue.main.async { self?.showUpToDateAlert() } + } + return + } + + let latestVersion = release.tagName.trimmingCharacters(in: CharacterSet(charactersIn: "v")) + + if self.isNewer(latestVersion, than: self.currentVersion) { + DispatchQueue.main.async { self.showUpdateAlert(release: release) } + } else if userInitiated { + DispatchQueue.main.async { self.showUpToDateAlert() } + } + }.resume() + } + + private func isNewer(_ latest: String, than current: String) -> Bool { + func baseParts(_ v: String) -> [Int] { + let base = v.components(separatedBy: "-").first ?? v + return base.split(separator: ".").compactMap { Int($0) } + } + let latestParts = baseParts(latest) + let currentParts = baseParts(current) + let count = max(latestParts.count, currentParts.count) + for i in 0.. c } + } + return false + } + + private func showUpdateAlert(release: GHRelease) { + let alert = NSAlert() + alert.messageText = String(localized: "Update Available") + alert.informativeText = String(localized: "Version \(release.tagName) is available. You have \(currentVersion).") + alert.addButton(withTitle: String(localized: "Download")) + alert.addButton(withTitle: String(localized: "Later")) + if alert.runModal() == .alertFirstButtonReturn, let url = URL(string: release.htmlURL) { + NSWorkspace.shared.open(url) + } + } + + private func showUpToDateAlert() { + let alert = NSAlert() + alert.messageText = String(localized: "You're up to date") + alert.informativeText = String(localized: "Version \(currentVersion) is the latest.") + alert.runModal() + } +} + +private struct GHRelease: Decodable { + let tagName: String + let htmlURL: String + + enum CodingKeys: String, CodingKey { + case tagName = "tag_name" + case htmlURL = "html_url" + } +} \ No newline at end of file