From 69a667f6009842994f82ac610939ef4a9ec72692 Mon Sep 17 00:00:00 2001 From: streetturtle Date: Sun, 29 Oct 2023 21:33:44 -0400 Subject: [PATCH] Add automatic check for updates --- JiraBar/AppDelegate.swift | 35 +++++++++---- JiraBar/Github/GithubClient.swift | 36 +++++++++++++ JiraBar/Github/GithubDtos.swift | 29 +++++++++++ JiraBar/Views/PreferencesView.swift | 80 ++++++++++++++--------------- jiraBar.xcodeproj/project.pbxproj | 16 ++++++ 5 files changed, 146 insertions(+), 50 deletions(-) create mode 100644 JiraBar/Github/GithubClient.swift create mode 100644 JiraBar/Github/GithubDtos.swift diff --git a/JiraBar/AppDelegate.swift b/JiraBar/AppDelegate.swift index fed7560..5cbccc4 100644 --- a/JiraBar/AppDelegate.swift +++ b/JiraBar/AppDelegate.swift @@ -48,7 +48,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { let config = NSImage.SymbolConfiguration(pointSize: 24, weight: .regular) unknownPersonAvatar = NSImage(systemSymbolName: "person.crop.circle.badge.questionmark", accessibilityDescription: nil)!.withSymbolConfiguration(config)! - + checkForUpdates() } func applicationWillTerminate(_ aNotification: Notification) { @@ -72,11 +72,11 @@ extension AppDelegate { self.statusBarItem.button?.title = String(issues.count) let issuesByStatus = Dictionary(grouping: issues) { $0.fields.status.name } .sorted { $0.key < $1.key } - + for (status, issuess) in issuesByStatus { self.menu.addItem(.separator()) self.menu.addItem(withTitle: status, action: nil, keyEquivalent: "") - + for issue in issuess { let issueItem = NSMenuItem(title: "", action: #selector(self.openLink), keyEquivalent: "") @@ -90,19 +90,19 @@ extension AppDelegate { .appendString(string: issue.fields.assignee?.displayName ?? "Unassign", color: "#888888") .appendSeparator() .appendString(string: issue.fields.issuetype.name, color: "#888888") - + issueItem.attributedTitle = issueItemTitle if issue.fields.summary.count > 50 { issueItem.toolTip = issue.fields.summary } issueItem.representedObject = URL(string: "\(self.jiraHost)/browse/\(issue.key)") - + self.jiraClient.getTransitionsByIssueKey(issueKey: issue.key) { transitions in if !transitions.isEmpty { let transitionsMenu = NSMenu() issueItem.submenu = transitionsMenu - + for transition in transitions { let transitionItem = NSMenuItem(title: transition.name, action: #selector(self.transitionIssue), keyEquivalent: "") transitionItem.representedObject = [issue.key, transition.id] @@ -131,7 +131,7 @@ extension AppDelegate { let createNewItem = NSMenuItem(title: "Create issue", action: #selector(self.openCreateNewIssue), keyEquivalent: "") createNewItem.image = NSImage(systemSymbolName: "plus", accessibilityDescription: nil) self.menu.addItem(createNewItem) - + self.menu.addItem(.separator()) self.menu.addItem(withTitle: "Preferences...", action: #selector(self.openPrefecencesWindow), keyEquivalent: "") self.menu.addItem(withTitle: "About JiraBar", action: #selector(self.openAboutWindow), keyEquivalent: "") @@ -149,7 +149,7 @@ extension AppDelegate { self.refreshMenu() } } - + @objc func openSearchResults() { let encodedPath = jql.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) @@ -184,7 +184,7 @@ extension AppDelegate { preferencesWindow.contentView = NSHostingView(rootView: contentView) preferencesWindow.makeKeyAndOrderFront(nil) preferencesWindow.styleMask.remove(.resizable) - + // allow the preference window can be focused automatically when opened NSApplication.shared.activate(ignoringOtherApps: true) @@ -213,7 +213,7 @@ extension AppDelegate { aboutWindow.contentView = NSHostingView(rootView: contentView) aboutWindow.makeKeyAndOrderFront(nil) aboutWindow.styleMask.remove(.resizable) - + // allow the preference window can be focused automatically when opened NSApplication.shared.activate(ignoringOtherApps: true) @@ -247,5 +247,20 @@ extension AppDelegate { } } } + + @objc + func checkForUpdates() { + let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + GithubClient().getLatestRelease { latestRelease in + if let latestRelease = latestRelease { + let versionComparison = currentVersion.compare(latestRelease.name.replacingOccurrences(of: "v", with: ""), options: .numeric) + if versionComparison == .orderedAscending { + let newVersionItem = NSMenuItem(title: "New version available", action: #selector(self.openLink), keyEquivalent: "") + newVersionItem.representedObject = URL(string: latestRelease.htmlUrl) + self.menu.addItem(newVersionItem) + } + } + } + } } diff --git a/JiraBar/Github/GithubClient.swift b/JiraBar/Github/GithubClient.swift new file mode 100644 index 0000000..d52b4f5 --- /dev/null +++ b/JiraBar/Github/GithubClient.swift @@ -0,0 +1,36 @@ +// +// GithubClient.swift +// jiraBar +// +// Created by Pavel Makhov on 2023-10-29. +// + +import Foundation +import Alamofire + +public class GithubClient { + + func getLatestRelease(completion:@escaping (((LatestRelease?) -> Void))) -> Void { + let headers: HTTPHeaders = [ + .contentType("application/json"), + .accept("application/json") + ] + AF.request("https://api.github.com/repos/menubar-apps/JiraBar/releases/latest", + method: .get, + encoding: JSONEncoding.default, + headers: headers) + .validate(statusCode: 200..<300) + .responseDecodable(of: LatestRelease.self) { response in + switch response.result { + case .success(let latestRelease): + completion(latestRelease) + case .failure(let error): + completion(nil) + if let data = response.data { + let json = String(data: data, encoding: String.Encoding.utf8) + } + sendNotification(body: error.localizedDescription) + } + } + } +} diff --git a/JiraBar/Github/GithubDtos.swift b/JiraBar/Github/GithubDtos.swift new file mode 100644 index 0000000..07ac3b9 --- /dev/null +++ b/JiraBar/Github/GithubDtos.swift @@ -0,0 +1,29 @@ +// +// GithubDtos.swift +// jiraBar +// +// Created by Pavel Makhov on 2023-10-29. +// + +import Foundation + +struct LatestRelease: Codable { + + var name: String + var htmlUrl: String + var assets: [Asset] + + enum CodingKeys: String, CodingKey { + case name + case assets + case htmlUrl = "html_url" + } +} + +struct Asset: Codable { + var name: String + + enum CodingKeys: String, CodingKey { + case name + } +} diff --git a/JiraBar/Views/PreferencesView.swift b/JiraBar/Views/PreferencesView.swift index 3c58cd9..23e6360 100644 --- a/JiraBar/Views/PreferencesView.swift +++ b/JiraBar/Views/PreferencesView.swift @@ -16,48 +16,48 @@ struct PreferencesView: View { Spacer() HStack { - Spacer() - Form { - TextField("Username:", text: $jiraUsername) - .textFieldStyle(RoundedBorderTextFieldStyle()) - TextField("Host:", text: $jiraHost) - .textFieldStyle(RoundedBorderTextFieldStyle()) - SecureField("Token:", text: $jiraToken) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .overlay( - Image(systemName: jiraTokenValidator.iconName).foregroundColor(jiraTokenValidator.iconColor) - .frame(maxWidth: .infinity, alignment: .trailing) - .padding(.trailing, 8) - ) - .onChange(of: jiraToken) { _ in - jiraTokenValidator.validate() - } - - Text("Jira Cloud: generate an [API Token](https://id.atlassian.com/manage/api-tokens)") - .font(.footnote) - Text("Jira Server: use your password as a token") - .font(.footnote) - - Divider() - - TextField("JQL Query:", text: $jql) - .textFieldStyle(RoundedBorderTextFieldStyle()) - Text("Use advanced search in Jira to create a JQL query and then paste it here") - .font(.footnote) - TextField("Max Results:", text: $maxResults) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .frame(width: 120) - Picker("Refresh Rate:", selection: $refreshRate) { - Text("1 minute").tag(1) - Text("5 minutes").tag(5) - Text("10 minutes").tag(10) - Text("15 minutes").tag(15) - Text("30 minutes").tag(30) + Spacer() + Form { + TextField("Username:", text: $jiraUsername) + .textFieldStyle(RoundedBorderTextFieldStyle()) + TextField("Host:", text: $jiraHost) + .textFieldStyle(RoundedBorderTextFieldStyle()) + SecureField("Token:", text: $jiraToken) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .overlay( + Image(systemName: jiraTokenValidator.iconName).foregroundColor(jiraTokenValidator.iconColor) + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.trailing, 8) + ) + .onChange(of: jiraToken) { _ in + jiraTokenValidator.validate() } - .frame(width: 200) + + Text("Jira Cloud: generate an [API Token](https://id.atlassian.com/manage/api-tokens)") + .font(.footnote) + Text("Jira Server: use your password as a token") + .font(.footnote) + + Divider() + + TextField("JQL Query:", text: $jql) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Text("Use advanced search in Jira to create a JQL query and then paste it here") + .font(.footnote) + TextField("Max Results:", text: $maxResults) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .frame(width: 120) + Picker("Refresh Rate:", selection: $refreshRate) { + Text("1 minute").tag(1) + Text("5 minutes").tag(5) + Text("10 minutes").tag(10) + Text("15 minutes").tag(15) + Text("30 minutes").tag(30) + } + .frame(width: 200) } - Spacer() - + Spacer() + } .padding() .frame(width: 500) diff --git a/jiraBar.xcodeproj/project.pbxproj b/jiraBar.xcodeproj/project.pbxproj index 7214d49..f8ba161 100644 --- a/jiraBar.xcodeproj/project.pbxproj +++ b/jiraBar.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 768D68872AEF367F004261DB /* GithubClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D68862AEF367F004261DB /* GithubClient.swift */; }; + 768D68892AEF3746004261DB /* GithubDtos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D68882AEF3746004261DB /* GithubDtos.swift */; }; 769F4E4F2775640900594911 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F4E4E2775640900594911 /* AppDelegate.swift */; }; 769F4E532775640A00594911 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 769F4E522775640A00594911 /* Assets.xcassets */; }; 769F4E562775640A00594911 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 769F4E542775640A00594911 /* Main.storyboard */; }; @@ -27,6 +29,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 768D68862AEF367F004261DB /* GithubClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubClient.swift; sourceTree = ""; }; + 768D68882AEF3746004261DB /* GithubDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubDtos.swift; sourceTree = ""; }; 769F4E4B2775640900594911 /* jiraBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = jiraBar.app; sourceTree = BUILT_PRODUCTS_DIR; }; 769F4E4E2775640900594911 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 769F4E522775640A00594911 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -59,6 +63,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 768D68852AEF3666004261DB /* Github */ = { + isa = PBXGroup; + children = ( + 768D68862AEF367F004261DB /* GithubClient.swift */, + 768D68882AEF3746004261DB /* GithubDtos.swift */, + ); + path = Github; + sourceTree = ""; + }; 769F4E422775640900594911 = { isa = PBXGroup; children = ( @@ -80,6 +93,7 @@ children = ( 769F4E73277795D700594911 /* Views */, 769F4E6927764BFE00594911 /* Extensions */, + 768D68852AEF3666004261DB /* Github */, 769F4E60277569D900594911 /* Jira */, 769F4E4E2775640900594911 /* AppDelegate.swift */, 769F4E522775640A00594911 /* Assets.xcassets */, @@ -213,6 +227,8 @@ 769F4E6B27764C3000594911 /* NSMutableAttributedString.swift in Sources */, 76D5F9E128D2606D009EBD80 /* JiraTokenValidator.swift in Sources */, 769F4E62277569E700594911 /* JiraClient.swift in Sources */, + 768D68892AEF3746004261DB /* GithubDtos.swift in Sources */, + 768D68872AEF367F004261DB /* GithubClient.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };