mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-04-26 14:57:37 +00:00
Merge pull request #126 from RobotsAndPencils/Notifications
Adds MacOS notifications
This commit is contained in:
commit
fc7560d95f
7 changed files with 165 additions and 0 deletions
|
|
@ -101,7 +101,9 @@
|
||||||
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
|
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
|
||||||
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
|
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
|
||||||
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
|
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
|
||||||
|
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
|
||||||
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
|
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
|
||||||
|
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
|
||||||
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
|
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
|
||||||
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
|
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
@ -266,7 +268,9 @@
|
||||||
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
|
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
|
||||||
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
|
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
|
||||||
E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = "<group>"; };
|
E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = "<group>"; };
|
||||||
|
E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||||
E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
|
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||||
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
|
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
|
@ -431,6 +435,7 @@
|
||||||
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
|
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
|
||||||
CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */,
|
CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */,
|
||||||
CA42DD6D25AEA8B200BC0B0C /* Logger.swift */,
|
CA42DD6D25AEA8B200BC0B0C /* Logger.swift */,
|
||||||
|
E89342F925EDCC17007CF557 /* NotificationManager.swift */,
|
||||||
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */,
|
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */,
|
||||||
CABFA9AE2592EEE900380FEE /* Path+.swift */,
|
CABFA9AE2592EEE900380FEE /* Path+.swift */,
|
||||||
CABFA9B42592EEEA00380FEE /* Process.swift */,
|
CABFA9B42592EEEA00380FEE /* Process.swift */,
|
||||||
|
|
@ -552,6 +557,7 @@
|
||||||
CAFE4AAB25B7D2C70064FE51 /* GeneralPreferencePane.swift */,
|
CAFE4AAB25B7D2C70064FE51 /* GeneralPreferencePane.swift */,
|
||||||
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */,
|
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */,
|
||||||
E8977EA225C11E1500835F80 /* PreferencesView.swift */,
|
E8977EA225C11E1500835F80 /* PreferencesView.swift */,
|
||||||
|
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */,
|
||||||
);
|
);
|
||||||
path = Preferences;
|
path = Preferences;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -795,6 +801,7 @@
|
||||||
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */,
|
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */,
|
||||||
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
|
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
|
||||||
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */,
|
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */,
|
||||||
|
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */,
|
||||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
|
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
|
||||||
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
|
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
|
||||||
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
|
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
|
||||||
|
|
@ -822,6 +829,7 @@
|
||||||
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
|
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
|
||||||
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */,
|
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */,
|
||||||
536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */,
|
536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */,
|
||||||
|
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */,
|
||||||
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */,
|
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */,
|
||||||
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */,
|
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */,
|
||||||
CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */,
|
CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ extension AppState {
|
||||||
|
|
||||||
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher<InstalledXcode, Error> {
|
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher<InstalledXcode, Error> {
|
||||||
Logger.appState.info("Using \(downloader) downloader")
|
Logger.appState.info("Using \(downloader) downloader")
|
||||||
|
|
||||||
return getXcodeArchive(installationType, downloader: downloader)
|
return getXcodeArchive(installationType, downloader: downloader)
|
||||||
.flatMap { xcode, url -> AnyPublisher<InstalledXcode, Swift.Error> in
|
.flatMap { xcode, url -> AnyPublisher<InstalledXcode, Swift.Error> in
|
||||||
self.installArchivedXcode(xcode, at: url)
|
self.installArchivedXcode(xcode, at: url)
|
||||||
|
|
@ -442,6 +443,9 @@ extension AppState {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return }
|
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return }
|
||||||
self.allXcodes[index].installState = .installing(step)
|
self.allXcodes[index].installState = .installing(step)
|
||||||
|
|
||||||
|
let xcode = self.allXcodes[index]
|
||||||
|
Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ class AppState: ObservableObject {
|
||||||
@Published var authenticationState: AuthenticationState = .unauthenticated
|
@Published var authenticationState: AuthenticationState = .unauthenticated
|
||||||
@Published var availableXcodes: [AvailableXcode] = [] {
|
@Published var availableXcodes: [AvailableXcode] = [] {
|
||||||
willSet {
|
willSet {
|
||||||
|
if newValue.count > availableXcodes.count && availableXcodes.count != 0 {
|
||||||
|
Current.notificationManager.scheduleNotification(title: "New Xcode versions", body: "New Xcode versions are available to download.", category: .normal)
|
||||||
|
}
|
||||||
updateAllXcodes(
|
updateAllXcodes(
|
||||||
availableXcodes: newValue,
|
availableXcodes: newValue,
|
||||||
installedXcodes: Current.files.installedXcodes(Path.root/"Applications"),
|
installedXcodes: Current.files.installedXcodes(Path.root/"Applications"),
|
||||||
|
|
@ -297,6 +300,7 @@ class AppState: ObservableObject {
|
||||||
|
|
||||||
func install(id: Xcode.ID) {
|
func install(id: Xcode.ID) {
|
||||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||||
|
|
||||||
installationPublishers[id] = signInIfNeeded()
|
installationPublishers[id] = signInIfNeeded()
|
||||||
.flatMap { [unowned self] in
|
.flatMap { [unowned self] in
|
||||||
// signInIfNeeded might finish before the user actually authenticates if UI is involved.
|
// signInIfNeeded might finish before the user actually authenticates if UI is involved.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ public struct Environment {
|
||||||
public var defaults = Defaults()
|
public var defaults = Defaults()
|
||||||
public var date: () -> Date = Date.init
|
public var date: () -> Date = Date.init
|
||||||
public var helper = Helper()
|
public var helper = Helper()
|
||||||
|
public var notificationManager = NotificationManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var Current = Environment()
|
public var Current = Environment()
|
||||||
|
|
@ -228,6 +229,11 @@ public struct Defaults {
|
||||||
public func removeObject(forKey key: String) {
|
public func removeObject(forKey key: String) {
|
||||||
removeObject(key)
|
removeObject(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var get: (String) -> Any? = { UserDefaults.standard.value(forKey: $0) }
|
||||||
|
public func get(forKey key: String) -> Any? {
|
||||||
|
get(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let helperClient = HelperClient()
|
private let helperClient = HelperClient()
|
||||||
|
|
|
||||||
100
Xcodes/Backend/NotificationManager.swift
Normal file
100
Xcodes/Backend/NotificationManager.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import Foundation
|
||||||
|
import os.log
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
/// Representation of the 3 states of the Notifications permission prompt which may either have not been shown, or was shown and denied or accepted
|
||||||
|
/// Unknown is value to indicate that we have not yet determined the status and should not be used other than as a default value before determining the actual status
|
||||||
|
public enum NotificationPermissionPromptStatus: Int {
|
||||||
|
case unknown, notShown, shownAndDenied, shownAndAccepted
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum XcodesNotificationCategory: String {
|
||||||
|
case normal
|
||||||
|
case error
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum XcodesNotificationType: String, Identifiable, CaseIterable, CustomStringConvertible {
|
||||||
|
case newVersionAvailable
|
||||||
|
case finishedInstalling
|
||||||
|
|
||||||
|
public var id: Self { self }
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
switch self {
|
||||||
|
case .newVersionAvailable:
|
||||||
|
return "New version is available"
|
||||||
|
case .finishedInstalling:
|
||||||
|
return "Finished installing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||||
|
private let notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
|
||||||
|
@Published var notificationStatus = NotificationPermissionPromptStatus.unknown
|
||||||
|
|
||||||
|
public override init() {
|
||||||
|
super.init()
|
||||||
|
loadNotificationStatus()
|
||||||
|
|
||||||
|
notificationCenter.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadNotificationStatus() {
|
||||||
|
UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { [weak self] (settings) in
|
||||||
|
let status = NotificationManager.systemPromptStatusFromSettings(settings)
|
||||||
|
self?.notificationStatus = status
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private class func systemPromptStatusFromSettings(_ settings: UNNotificationSettings) -> NotificationPermissionPromptStatus {
|
||||||
|
switch settings.authorizationStatus {
|
||||||
|
case .notDetermined:
|
||||||
|
return .notShown
|
||||||
|
case .authorized, .provisional:
|
||||||
|
return .shownAndAccepted
|
||||||
|
case .denied:
|
||||||
|
return .shownAndDenied
|
||||||
|
@unknown default:
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func requestAccess() {
|
||||||
|
|
||||||
|
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let error = error {
|
||||||
|
// Handle the error here.
|
||||||
|
Logger.appState.error("Error requesting notification accesss: \(error.legibleLocalizedDescription)")
|
||||||
|
} else {
|
||||||
|
Logger.appState.log("User has \(granted ? "Granted" : "NOT GRANTED") notification permission")
|
||||||
|
}
|
||||||
|
self?.loadNotificationStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleNotification(title: String?, body: String, category: XcodesNotificationCategory) {
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
if let title = title {
|
||||||
|
content.title = title
|
||||||
|
}
|
||||||
|
content.body = body
|
||||||
|
content.sound = .default
|
||||||
|
content.categoryIdentifier = category.rawValue
|
||||||
|
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.3, repeats: false)
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
|
||||||
|
notificationCenter.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UNUserNotificationCenterDelegate
|
||||||
|
|
||||||
|
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
completionHandler( [.banner, .badge, .sound])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -14,6 +14,12 @@ struct GeneralPreferencePane: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
GroupBox(label: Text("Notifications")) {
|
||||||
|
NotificationsView().environmentObject(appState)
|
||||||
|
}
|
||||||
|
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||||
}
|
}
|
||||||
.frame(width: 400)
|
.frame(width: 400)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
Xcodes/Frontend/Preferences/NotificationsView.swift
Normal file
37
Xcodes/Frontend/Preferences/NotificationsView.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NotificationsView: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
switch Current.notificationManager.notificationStatus {
|
||||||
|
case .shownAndAccepted:
|
||||||
|
Text("Access Granted. You will receive notifications from Xcodes.")
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
case .shownAndDenied:
|
||||||
|
Text("⚠️ Access Denied ⚠️\n\nPlease open your Notification Settings and select Xcodes if you wish to allow access.")
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
Button("Notification Settings", action: {
|
||||||
|
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.notifications")!)
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
Button("Enable Notifications", action: {
|
||||||
|
Current.notificationManager.requestAccess()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
NotificationsView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue