From 1a5f0db71d5326b3389ec0b7760c158b6fe3a77d Mon Sep 17 00:00:00 2001 From: Stef Kors Date: Wed, 6 Nov 2024 11:56:04 +0100 Subject: [PATCH 1/3] WIP --- .../BackgroundFetcher.entitlements | 14 + BackgroundFetcher/BackgroundFetcher.swift | 106 ++++++++ .../BackgroundFetcherProtocol.swift | 35 +++ BackgroundFetcher/Info.plist | 11 + BackgroundFetcher/main.swift | 54 ++++ GitLab.xcodeproj/project.pbxproj | 255 +++++++++++++++++- .../xcschemes/xcschememanagement.plist | 5 + GitLab/ExtraWindow.swift | 61 +++++ GitLab/GitLabApp.swift | 82 +++++- GitLab/Info.plist | 5 + Shared/UserInterface/Extensions/IfView.swift | 17 -- Shared/UserInterface/MainContentView.swift | 3 + Shared/UserInterface/UserInterface.swift | 36 +-- Shared/UserInterface/Views/PipelineView.swift | 1 - 14 files changed, 633 insertions(+), 52 deletions(-) create mode 100644 BackgroundFetcher/BackgroundFetcher.entitlements create mode 100644 BackgroundFetcher/BackgroundFetcher.swift create mode 100644 BackgroundFetcher/BackgroundFetcherProtocol.swift create mode 100644 BackgroundFetcher/Info.plist create mode 100644 BackgroundFetcher/main.swift delete mode 100644 Shared/UserInterface/Extensions/IfView.swift diff --git a/BackgroundFetcher/BackgroundFetcher.entitlements b/BackgroundFetcher/BackgroundFetcher.entitlements new file mode 100644 index 0000000..f792f7d --- /dev/null +++ b/BackgroundFetcher/BackgroundFetcher.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.stefkors.GitLab + + com.apple.security.network.client + + + diff --git a/BackgroundFetcher/BackgroundFetcher.swift b/BackgroundFetcher/BackgroundFetcher.swift new file mode 100644 index 0000000..ba25fe6 --- /dev/null +++ b/BackgroundFetcher/BackgroundFetcher.swift @@ -0,0 +1,106 @@ +// +// BackgroundFetcher.swift +// BackgroundFetcher +// +// Created by Stef Kors on 03/11/2024. +// + +import Foundation +import OSLog +import SwiftData +/// This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. +class BackgroundFetcher: NSObject, BackgroundFetcherProtocol { + let log = Logger() + +// var helloWorldTimer = Timer.scheduledTimer( +// timeInterval: 6.0, +// target: BackgroundFetcher.self, +// selector: #selector(Self.sayHello), +// userInfo: nil, +// repeats: true +// ) +// +// @objc func sayHello() { +// print("hello World") +// log.warning("Jenga 5 (success say hello)") +// } +// +// var timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { +// (_) in +// let log2 = Logger() +// log2.warning("Jenga 2 (simple timer)") +// } + + let activity = NSBackgroundActivityScheduler(identifier: "com.stefkors.GitLab.updatecheck") + + var count: Int = 0 + var date = Date.now + + + /// This implements the example protocol. Replace the body of this class with the implementation of this service's protocol. + @objc func performCalculation(firstNumber: Int, secondNumber: Int) { + let response = firstNumber + secondNumber + print("response: \(response)") + if count == 0 { + startBackground() + } +// log.warning("Jenga 3 performCalc") +// timer.fire() +// activity.invalidate() +// activity.repeats = true +// activity.interval = 2 // seconds +// activity.tolerance = 0 +// activity.qualityOfService = .userInteractive +//// print("hi 2") +//// log.warning("Jenga 4") +// activity.schedule { completion in +// // perform activity +// let newDate = Date.now +// self.log.warning("Jenga 5 (success performCalc) \(response) \(self.count.description) sinceLast: \(newDate.timeIntervalSince(self.date)) seconds") +// self.count += 1 +// self.date = Date.now +// completion(.finished) +// } + } + + func startBackground() { + activity.invalidate() + activity.repeats = true + activity.interval = 8 // seconds + activity.tolerance = 1 + activity.qualityOfService = .userInteractive + // print("hi 2") + // log.warning("Jenga 4") + activity.schedule { completion in + + let context = ModelContext(.shared) + + // WORKS! +// let repo = LaunchpadRepo( +// id: UUID().uuidString, +// name: "Test Repo \(self.count.description)", +// group: "Apple", +// url: URL(string: "https://google.com")!, +// provider: .GitHub +// ) +// context.insert(repo) +// try? context.save() + + let newDate = Date.now + let interval = newDate.timeIntervalSince(self.date) + if interval < self.activity.interval { + return completion(.finished) + } + self.log.warning("Jenga 5 (success performCalc) \(self.count.description) sinceLast: \(interval) seconds shoulddefered? \(self.activity.shouldDefer)") + self.count += 1 + self.date = Date.now + completion(.finished) + } + } + + /// This implements the example protocol. Replace the body of this class with the implementation of this service's protocol. + @objc func performCalculation(firstNumber: Int, secondNumber: Int, with reply: @escaping (Int) -> Void) { + let response = firstNumber + secondNumber + reply(response) + } +} diff --git a/BackgroundFetcher/BackgroundFetcherProtocol.swift b/BackgroundFetcher/BackgroundFetcherProtocol.swift new file mode 100644 index 0000000..444b8e4 --- /dev/null +++ b/BackgroundFetcher/BackgroundFetcherProtocol.swift @@ -0,0 +1,35 @@ +// +// BackgroundFetcherProtocol.swift +// BackgroundFetcher +// +// Created by Stef Kors on 03/11/2024. +// + +import Foundation + +/// The protocol that this service will vend as its API. This protocol will also need to be visible to the process hosting the service. +@objc protocol BackgroundFetcherProtocol { + func performCalculation(firstNumber: Int, secondNumber: Int) + /// Replace the API of this protocol with an API appropriate to the service you are vending. + func performCalculation(firstNumber: Int, secondNumber: Int, with reply: @escaping (Int) -> Void) +} + +/* + To use the service from an application or other process, use NSXPCConnection to establish a connection to the service by doing something like this: + + connectionToService = NSXPCConnection(serviceName: "com.stefkors.BackgroundFetcher") + connectionToService.remoteObjectInterface = NSXPCInterface(with: BackgroundFetcherProtocol.self) + connectionToService.resume() + + Once you have a connection to the service, you can use it like this: + + if let proxy = connectionToService.remoteObjectProxy as? BackgroundFetcherProtocol { + proxy.performCalculation(firstNumber: ..., secondNumber: ...) { result in + NSLog("Result of calculation is: \(result)") + } + } + + And, when you are finished with the service, clean up the connection like this: + + connectionToService.invalidate() +*/ diff --git a/BackgroundFetcher/Info.plist b/BackgroundFetcher/Info.plist new file mode 100644 index 0000000..c123a5d --- /dev/null +++ b/BackgroundFetcher/Info.plist @@ -0,0 +1,11 @@ + + + + + XPCService + + ServiceType + Application + + + diff --git a/BackgroundFetcher/main.swift b/BackgroundFetcher/main.swift new file mode 100644 index 0000000..a017334 --- /dev/null +++ b/BackgroundFetcher/main.swift @@ -0,0 +1,54 @@ +// +// main.swift +// BackgroundFetcher +// +// Created by Stef Kors on 03/11/2024. +// + +import Foundation +import OSLog + +class ServiceDelegate: NSObject, NSXPCListenerDelegate { + +// var timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { +// (_) in +// let log2 = Logger() +// log2.warning("Jenga 5 (simple timer service delegate)") +// } + + /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. + func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { + + // Configure the connection. + // First, set the interface that the exported object implements. + newConnection.exportedInterface = NSXPCInterface(with: BackgroundFetcherProtocol.self) + + // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. + let exportedObject = BackgroundFetcher() + newConnection.exportedObject = exportedObject + + // Resuming the connection allows the system to deliver more incoming messages. + newConnection.resume() + +// timer.fire() + + // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false. + return true + } +} + +//var timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { +// (_) in +// let log2 = Logger() +// log2.warning("Jenga 5 (simple timer from main xpc)") +//} +//timer.fire() +// Create the delegate for the service. +let delegate = ServiceDelegate() + +// Set up the one NSXPCListener for this service. It will handle all incoming connections. +let listener = NSXPCListener.service() +listener.delegate = delegate + +// Resuming the serviceListener starts this service. This method does not return. +listener.resume() diff --git a/GitLab.xcodeproj/project.pbxproj b/GitLab.xcodeproj/project.pbxproj index 4acb5ea..896ae1f 100644 --- a/GitLab.xcodeproj/project.pbxproj +++ b/GitLab.xcodeproj/project.pbxproj @@ -3,14 +3,10 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ - 8A03BF222CD6D29100ACE1EC /* IfView.swift in Resources */ = {isa = PBXBuildFile; fileRef = 8A03BF212CD6D29100ACE1EC /* IfView.swift */; }; - 8A03BF232CD6D29100ACE1EC /* IfView.swift in Resources */ = {isa = PBXBuildFile; fileRef = 8A03BF212CD6D29100ACE1EC /* IfView.swift */; }; - 8A03BF242CD6D29100ACE1EC /* IfView.swift in Resources */ = {isa = PBXBuildFile; fileRef = 8A03BF212CD6D29100ACE1EC /* IfView.swift */; }; - 8A03BF252CD6D29100ACE1EC /* IfView.swift in Resources */ = {isa = PBXBuildFile; fileRef = 8A03BF212CD6D29100ACE1EC /* IfView.swift */; }; 8A0CDABB2A55932E0056B63F /* CIJobsNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80D192A55817400819B80 /* CIJobsNotificationView.swift */; }; 8A0F41652CB5CCA1006BD170 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AC4B4122865C1480054C601 /* WidgetKit.framework */; }; 8A111F1B2CC1118A00DEB0DD /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F1A2CC1118900DEB0DD /* Data.swift */; }; @@ -80,6 +76,41 @@ 8A36AF122869B4110008B949 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A36AEFB2868D2650008B949 /* UserNotificationsUI.framework */; }; 8A36AF182869B4110008B949 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8A36AF162869B4110008B949 /* MainInterface.storyboard */; }; 8A36AF1D2869B4110008B949 /* NotificationContent.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8A36AF102869B4110008B949 /* NotificationContent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 8A4533C82CD7C64A0011D5B5 /* BackgroundFetcher.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 8A4533BC2CD7C64A0011D5B5 /* BackgroundFetcher.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 8A4534382CD7E9D40011D5B5 /* LaunchpadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55E3C72CD6C8B2005B4AA5 /* LaunchpadState.swift */; }; + 8A4534392CD7E9E40011D5B5 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFDAA972A84FB26001937AC /* Account.swift */; }; + 8A45343A2CD7E9FA0011D5B5 /* ModelContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ADD570F2B8220D3001F8E8F /* ModelContainer.swift */; }; + 8A45343B2CD7EA0A0011D5B5 /* UniversalMergeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7C0AFC2CD3657100E479CA /* UniversalMergeRequest.swift */; }; + 8A45343C2CD7EA170011D5B5 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F1A2CC1118900DEB0DD /* Data.swift */; }; + 8A45343D2CD7EA210011D5B5 /* StructsGitLab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE72A55817400819B80 /* StructsGitLab.swift */; }; + 8A45343E2CD7EA280011D5B5 /* StructsGitHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7C0AD92CD29D1F00E479CA /* StructsGitHub.swift */; }; + 8A45343F2CD7EA3C0011D5B5 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7C0AF22CD3650000E479CA /* Date.swift */; }; + 8A4534402CD7EA960011D5B5 /* KeyedDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE7A3D62A83A3FF0004506F /* KeyedDecodingContainer.swift */; }; + 8A4534412CD7EA960011D5B5 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F1F2CC1161500DEB0DD /* String.swift */; }; + 8A4534422CD7EA960011D5B5 /* OptionalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A33E9452CD3D4D100F2C148 /* OptionalType.swift */; }; + 8A4534432CD7EA960011D5B5 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ADEBF9D2A83A227007C22CD /* URL.swift */; }; + 8A4534442CD7EA960011D5B5 /* Array+Difference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ABBD2052B7E2E70007C03E6 /* Array+Difference.swift */; }; + 8A4534452CD7EA960011D5B5 /* DateCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7C0ACC2CCFEBD100E479CA /* DateCompare.swift */; }; + 8A4534462CD7EA960011D5B5 /* IfViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE2743B2CC67A530059244E /* IfViewModifier.swift */; }; + 8A4534472CD7EA960011D5B5 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7C0AE82CD363B700E479CA /* Collection.swift */; }; + 8A4534482CD7EB0B0011D5B5 /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC0C2092A5D3D720096772B /* AccessToken.swift */; }; + 8A4534492CD7EB0B0011D5B5 /* CachedAsyncImage+ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE92A55817400819B80 /* CachedAsyncImage+ImageCache.swift */; }; + 8A45344A2CD7EB0B0011D5B5 /* gitlabISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE82A55817400819B80 /* gitlabISO8601DateFormatter.swift */; }; + 8A45344B2CD7EB0B0011D5B5 /* NetworkEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */; }; + 8A45344C2CD7EB0B0011D5B5 /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F522CC1388900DEB0DD /* NetworkInfo.swift */; }; + 8A45344D2CD7EB0B0011D5B5 /* NetworkManagerGitLab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE62A55817400819B80 /* NetworkManagerGitLab.swift */; }; + 8A45344E2CD7EB0B0011D5B5 /* NetworkManagerGitLab+repoLaunchPad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE02A55817400819B80 /* NetworkManagerGitLab+repoLaunchPad.swift */; }; + 8A45344F2CD7EB0B0011D5B5 /* NetworkManagerGitHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7C0AD42CD292A700E479CA /* NetworkManagerGitHub.swift */; }; + 8A4534502CD7EB0B0011D5B5 /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CEF2A55817400819B80 /* NetworkReachability.swift */; }; + 8A4534512CD7EB0B0011D5B5 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F482CC1386600DEB0DD /* NetworkState.swift */; }; + 8A4534522CD7EB0B0011D5B5 /* NoticeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF12A55817400819B80 /* NoticeMessage.swift */; }; + 8A4534532CD7EB0B0011D5B5 /* NoticeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF22A55817400819B80 /* NoticeType.swift */; }; + 8A4534542CD7EB0B0011D5B5 /* NoticeState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF32A55817400819B80 /* NoticeState.swift */; }; + 8A4534552CD7EB0B0011D5B5 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE52A55817400819B80 /* NotificationManager.swift */; }; + 8A4534562CD7EB0B0011D5B5 /* QueryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F3E2CC12C5F00DEB0DD /* QueryType.swift */; }; + 8A4534572CD7EB0B0011D5B5 /* PipelineStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A858F212CD527B10024795D /* PipelineStatus.swift */; }; + 8A4534592CD7EB190011D5B5 /* Get in Frameworks */ = {isa = PBXBuildFile; productRef = 8A4534582CD7EB190011D5B5 /* Get */; }; + 8A45345B2CD7EB1D0011D5B5 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 8A45345A2CD7EB1D0011D5B5 /* OrderedCollections */; }; 8A55E3C82CD6C8B2005B4AA5 /* LaunchpadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55E3C72CD6C8B2005B4AA5 /* LaunchpadState.swift */; }; 8A55E3C92CD6C8B2005B4AA5 /* LaunchpadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55E3C72CD6C8B2005B4AA5 /* LaunchpadState.swift */; }; 8A55E3CA2CD6C8B2005B4AA5 /* LaunchpadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55E3C72CD6C8B2005B4AA5 /* LaunchpadState.swift */; }; @@ -486,6 +517,13 @@ remoteGlobalIDString = 8A36AF0F2869B4110008B949; remoteInfo = NotificationContent; }; + 8A4533C62CD7C64A0011D5B5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8A5FC0EE26EFD08E004136AB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8A4533BB2CD7C64A0011D5B5; + remoteInfo = BackgroundFetcher; + }; 8A5FC10C26EFD08F004136AB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 8A5FC0EE26EFD08E004136AB /* Project object */; @@ -510,6 +548,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 8A4533C92CD7C64A0011D5B5 /* Embed XPC Services */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; + dstSubfolderSpec = 16; + files = ( + 8A4533C82CD7C64A0011D5B5 /* BackgroundFetcher.xpc in Embed XPC Services */, + ); + name = "Embed XPC Services"; + runOnlyForDeploymentPostprocessing = 0; + }; 8AC4B4262865C14A0054C601 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -525,7 +574,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 8A03BF212CD6D29100ACE1EC /* IfView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IfView.swift; sourceTree = ""; }; 8A07E5672890492C0042EACB /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 8A111F1A2CC1118900DEB0DD /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 8A111F1F2CC1161500DEB0DD /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; @@ -581,6 +629,7 @@ 8A36AF172869B4110008B949 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 8A36AF192869B4110008B949 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8A36AF1A2869B4110008B949 /* NotificationContent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationContent.entitlements; sourceTree = ""; }; + 8A4533BC2CD7C64A0011D5B5 /* BackgroundFetcher.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = BackgroundFetcher.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 8A55E3C72CD6C8B2005B4AA5 /* LaunchpadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchpadState.swift; sourceTree = ""; }; 8A5FC0F626EFD08E004136AB /* Merge Requests for GitLab.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Merge Requests for GitLab.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 8A5FC0F926EFD08E004136AB /* GitLabApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitLabApp.swift; sourceTree = ""; }; @@ -705,6 +754,27 @@ 8AFF87EE2BDA713800D21D16 /* IsInWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsInWidget.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 8A4533CC2CD7C64A0011D5B5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 8A4533BB2CD7C64A0011D5B5 /* BackgroundFetcher */; + }; + 8A4533CF2CD7C9270011D5B5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + BackgroundFetcherProtocol.swift, + ); + target = 8A5FC0F526EFD08E004136AB /* GitLab */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 8A4533BD2CD7C64A0011D5B5 /* BackgroundFetcher */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (8A4533CF2CD7C9270011D5B5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 8A4533CC2CD7C64A0011D5B5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = BackgroundFetcher; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 8A31CB372866334000C94AC1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -742,6 +812,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8A4533B92CD7C64A0011D5B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8A45345B2CD7EB1D0011D5B5 /* OrderedCollections in Frameworks */, + 8A4534592CD7EB190011D5B5 /* Get in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8A5FC0F326EFD08E004136AB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -876,6 +955,7 @@ 8AF80BE92A5580E700819B80 /* Shared */, 8A07E5672890492C0042EACB /* README.md */, 8AE8A6EB2B989E9A002B3C9E /* DesktopWidgetTool */, + 8A4533BD2CD7C64A0011D5B5 /* BackgroundFetcher */, 8AC4B4112865C1480054C601 /* Frameworks */, 8A5FC0F826EFD08E004136AB /* GitLab */, 8A31CB3B2866334000C94AC1 /* GitLab iOS */, @@ -900,6 +980,7 @@ 8A31CB532866334100C94AC1 /* GitLab iOSUITests.xctest */, 8A36AF102869B4110008B949 /* NotificationContent.appex */, 8AE8A6E82B989E9A002B3C9E /* DesktopWidgetToolExtension.appex */, + 8A4533BC2CD7C64A0011D5B5 /* BackgroundFetcher.xpc */, ); name = Products; sourceTree = ""; @@ -946,7 +1027,6 @@ 8A63DE6F2A83A1FC002DD636 /* Extensions */ = { isa = PBXGroup; children = ( - 8A03BF212CD6D29100ACE1EC /* IfView.swift */, 8A33E9452CD3D4D100F2C148 /* OptionalType.swift */, 8A7C0AF22CD3650000E479CA /* Date.swift */, 8A7C0AE82CD363B700E479CA /* Collection.swift */, @@ -1280,6 +1360,30 @@ productReference = 8A36AF102869B4110008B949 /* NotificationContent.appex */; productType = "com.apple.product-type.app-extension"; }; + 8A4533BB2CD7C64A0011D5B5 /* BackgroundFetcher */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8A4533CD2CD7C64A0011D5B5 /* Build configuration list for PBXNativeTarget "BackgroundFetcher" */; + buildPhases = ( + 8A4533B82CD7C64A0011D5B5 /* Sources */, + 8A4533B92CD7C64A0011D5B5 /* Frameworks */, + 8A4533BA2CD7C64A0011D5B5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 8A4533BD2CD7C64A0011D5B5 /* BackgroundFetcher */, + ); + name = BackgroundFetcher; + packageProductDependencies = ( + 8A4534582CD7EB190011D5B5 /* Get */, + 8A45345A2CD7EB1D0011D5B5 /* OrderedCollections */, + ); + productName = BackgroundFetcher; + productReference = 8A4533BC2CD7C64A0011D5B5 /* BackgroundFetcher.xpc */; + productType = "com.apple.product-type.xpc-service"; + }; 8A5FC0F526EFD08E004136AB /* GitLab */ = { isa = PBXNativeTarget; buildConfigurationList = 8A5FC11F26EFD08F004136AB /* Build configuration list for PBXNativeTarget "GitLab" */; @@ -1288,12 +1392,14 @@ 8A5FC0F326EFD08E004136AB /* Frameworks */, 8A5FC0F426EFD08E004136AB /* Resources */, 8AC4B4262865C14A0054C601 /* Embed Foundation Extensions */, + 8A4533C92CD7C64A0011D5B5 /* Embed XPC Services */, ); buildRules = ( ); dependencies = ( 8A36AF1C2869B4110008B949 /* PBXTargetDependency */, 8AE8A6F52B989E9B002B3C9E /* PBXTargetDependency */, + 8A4533C72CD7C64A0011D5B5 /* PBXTargetDependency */, ); name = GitLab; packageProductDependencies = ( @@ -1370,7 +1476,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1530; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1530; TargetAttributes = { 8A31CB392866334000C94AC1 = { @@ -1387,6 +1493,9 @@ 8A36AF0F2869B4110008B949 = { CreatedOnToolsVersion = 13.4.1; }; + 8A4533BB2CD7C64A0011D5B5 = { + CreatedOnToolsVersion = 16.0; + }; 8A5FC0F526EFD08E004136AB = { CreatedOnToolsVersion = 12.5.1; }; @@ -1429,6 +1538,7 @@ 8A31CB522866334100C94AC1 /* GitLab iOSUITests */, 8A36AF0F2869B4110008B949 /* NotificationContent */, 8AE8A6E72B989E9A002B3C9E /* DesktopWidgetToolExtension */, + 8A4533BB2CD7C64A0011D5B5 /* BackgroundFetcher */, ); }; /* End PBXProject section */ @@ -1440,7 +1550,6 @@ files = ( 8A31CB442866334100C94AC1 /* Preview Assets.xcassets in Resources */, 8AD294BF298AA72C006F1932 /* Settings.bundle in Resources */, - 8A03BF252CD6D29100ACE1EC /* IfView.swift in Resources */, 8A31CB412866334100C94AC1 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1463,16 +1572,21 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8A03BF242CD6D29100ACE1EC /* IfView.swift in Resources */, 8A36AF182869B4110008B949 /* MainInterface.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 8A4533BA2CD7C64A0011D5B5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8A5FC0F426EFD08E004136AB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8A03BF222CD6D29100ACE1EC /* IfView.swift in Resources */, 8A858F142CD4F25E0024795D /* .swiftlint.yml in Resources */, 8A5B657528E4E76000535C61 /* Assets.xcassets in Resources */, 8A13B85E2A66A3E30090A6D9 /* Credits.rtf in Resources */, @@ -1498,7 +1612,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8A03BF232CD6D29100ACE1EC /* IfView.swift in Resources */, 8AE8A6F12B989E9B002B3C9E /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1740,6 +1853,45 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8A4533B82CD7C64A0011D5B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8A4534482CD7EB0B0011D5B5 /* AccessToken.swift in Sources */, + 8A4534492CD7EB0B0011D5B5 /* CachedAsyncImage+ImageCache.swift in Sources */, + 8A45344A2CD7EB0B0011D5B5 /* gitlabISO8601DateFormatter.swift in Sources */, + 8A45344B2CD7EB0B0011D5B5 /* NetworkEvent.swift in Sources */, + 8A45344C2CD7EB0B0011D5B5 /* NetworkInfo.swift in Sources */, + 8A45344D2CD7EB0B0011D5B5 /* NetworkManagerGitLab.swift in Sources */, + 8A45344E2CD7EB0B0011D5B5 /* NetworkManagerGitLab+repoLaunchPad.swift in Sources */, + 8A45344F2CD7EB0B0011D5B5 /* NetworkManagerGitHub.swift in Sources */, + 8A4534502CD7EB0B0011D5B5 /* NetworkReachability.swift in Sources */, + 8A4534512CD7EB0B0011D5B5 /* NetworkState.swift in Sources */, + 8A4534522CD7EB0B0011D5B5 /* NoticeMessage.swift in Sources */, + 8A4534532CD7EB0B0011D5B5 /* NoticeType.swift in Sources */, + 8A4534542CD7EB0B0011D5B5 /* NoticeState.swift in Sources */, + 8A4534552CD7EB0B0011D5B5 /* NotificationManager.swift in Sources */, + 8A4534562CD7EB0B0011D5B5 /* QueryType.swift in Sources */, + 8A4534572CD7EB0B0011D5B5 /* PipelineStatus.swift in Sources */, + 8A45343B2CD7EA0A0011D5B5 /* UniversalMergeRequest.swift in Sources */, + 8A4534392CD7E9E40011D5B5 /* Account.swift in Sources */, + 8A45343A2CD7E9FA0011D5B5 /* ModelContainer.swift in Sources */, + 8A4534382CD7E9D40011D5B5 /* LaunchpadState.swift in Sources */, + 8A4534402CD7EA960011D5B5 /* KeyedDecodingContainer.swift in Sources */, + 8A4534412CD7EA960011D5B5 /* String.swift in Sources */, + 8A4534422CD7EA960011D5B5 /* OptionalType.swift in Sources */, + 8A4534432CD7EA960011D5B5 /* URL.swift in Sources */, + 8A4534442CD7EA960011D5B5 /* Array+Difference.swift in Sources */, + 8A4534452CD7EA960011D5B5 /* DateCompare.swift in Sources */, + 8A4534462CD7EA960011D5B5 /* IfViewModifier.swift in Sources */, + 8A4534472CD7EA960011D5B5 /* Collection.swift in Sources */, + 8A45343D2CD7EA210011D5B5 /* StructsGitLab.swift in Sources */, + 8A45343F2CD7EA3C0011D5B5 /* Date.swift in Sources */, + 8A45343C2CD7EA170011D5B5 /* Data.swift in Sources */, + 8A45343E2CD7EA280011D5B5 /* StructsGitHub.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8A5FC0F226EFD08E004136AB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2004,6 +2156,11 @@ target = 8A36AF0F2869B4110008B949 /* NotificationContent */; targetProxy = 8A36AF1B2869B4110008B949 /* PBXContainerItemProxy */; }; + 8A4533C72CD7C64A0011D5B5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8A4533BB2CD7C64A0011D5B5 /* BackgroundFetcher */; + targetProxy = 8A4533C62CD7C64A0011D5B5 /* PBXContainerItemProxy */; + }; 8A5FC10D26EFD08F004136AB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 8A5FC0F526EFD08E004136AB /* GitLab */; @@ -2264,6 +2421,61 @@ }; name = Release; }; + 8A4533CA2CD7C64A0011D5B5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = BackgroundFetcher/BackgroundFetcher.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RT5BSWJB4U; + ENABLE_HARDENED_RUNTIME = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = BackgroundFetcher/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = BackgroundFetcher; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.stefkors.BackgroundFetcher; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 8A4533CB2CD7C64A0011D5B5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = BackgroundFetcher/BackgroundFetcher.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RT5BSWJB4U; + ENABLE_HARDENED_RUNTIME = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = BackgroundFetcher/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = BackgroundFetcher; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.stefkors.BackgroundFetcher; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; 8A5FC11D26EFD08F004136AB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2667,6 +2879,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 8A4533CD2CD7C64A0011D5B5 /* Build configuration list for PBXNativeTarget "BackgroundFetcher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8A4533CA2CD7C64A0011D5B5 /* Debug */, + 8A4533CB2CD7C64A0011D5B5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 8A5FC0F126EFD08E004136AB /* Build configuration list for PBXProject "GitLab" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2742,6 +2963,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 8A4534582CD7EB190011D5B5 /* Get */ = { + isa = XCSwiftPackageProductDependency; + package = 8AF80DD52A5581A600819B80 /* XCRemoteSwiftPackageReference "Get" */; + productName = Get; + }; + 8A45345A2CD7EB1D0011D5B5 /* OrderedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = 8A858F262CD54D2B0024795D /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = OrderedCollections; + }; 8A7935FA2A5583F700F8FB6C /* Get */ = { isa = XCSwiftPackageProductDependency; package = 8AF80DD52A5581A600819B80 /* XCRemoteSwiftPackageReference "Get" */; diff --git a/GitLab.xcodeproj/xcuserdata/stefkors.xcuserdatad/xcschemes/xcschememanagement.plist b/GitLab.xcodeproj/xcuserdata/stefkors.xcuserdatad/xcschemes/xcschememanagement.plist index dfaa9e8..7d61f75 100644 --- a/GitLab.xcodeproj/xcuserdata/stefkors.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/GitLab.xcodeproj/xcuserdata/stefkors.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,6 +4,11 @@ SchemeUserState + BackgroundFetcher.xcscheme_^#shared#^_ + + orderHint + 5 + DesktopWidgetToolExtension.xcscheme_^#shared#^_ orderHint diff --git a/GitLab/ExtraWindow.swift b/GitLab/ExtraWindow.swift index 6686f00..d4cb06e 100644 --- a/GitLab/ExtraWindow.swift +++ b/GitLab/ExtraWindow.swift @@ -7,6 +7,8 @@ import SwiftUI import SwiftData +import BackgroundFetcher +import OSLog struct ExtraWindow: View { @Environment(\.openURL) private var openURL @@ -26,6 +28,19 @@ struct ExtraWindow: View { @Environment(\.appearsActive) private var appearsActive @State private var hasLoaded: Bool = false + let log = Logger(subsystem: "ExtraWindow", category: "ExtraBG") + + +// let backgroundUpdateTimer: NSBackgroundActivityScheduler = { +// let scheduler: NSBackgroundActivityScheduler = .init(identifier: "com.stefkors.GitLab.backgroundAutoUpdate") +// scheduler.repeats = true +// scheduler.interval = 3 +// scheduler.tolerance = 0 +// scheduler.qualityOfService = .userInteractive +// +// return scheduler +// }() + var body: some View { VStack { Divider() @@ -62,5 +77,51 @@ struct ExtraWindow: View { .pickerStyle(.segmented) } } +// .onAppear { +// callXPC() + + + +// if let session = try? XPCSession(xpcService: serviceName) { +// let request = CalculationRequest(firstNumber: 23, secondNumber: 19) +// try? session.send(request) +// } +// } +// .onAppear { +// log.warning("Jenga 1 start backgroundUpdateTimer") +// // Start the background update scheduler when the app starts +// backgroundUpdateTimer.schedule +// { (completion: NSBackgroundActivityScheduler.CompletionHandler) in +// log.warning("Jenga 5 (background task success)") +// completion(NSBackgroundActivityScheduler.Result.finished) +// +// } +// } } + +// @MainActor +// func callXPC() { +// let serviceName = "com.stefkors.BackgroundFetcher" +// let connection = NSXPCConnection(serviceName: serviceName) +// connection.remoteObjectInterface = NSXPCInterface(with: BackgroundFetcherProtocol.self) +// connection.resume() +// +// let service = connection.remoteObjectProxyWithErrorHandler { error in +// print("Received error:", error) +// } as? BackgroundFetcherProtocol +// +// log.warning("Jenga before calc 32") +// +// service?.performCalculation(firstNumber: 12, secondNumber: 20) +// } +} + +// A codable type that contains two numbers to add together. +struct CalculationRequest: Codable { + let firstNumber: Int + let secondNumber: Int +} + +struct CalcuationResponse: Codable { + let result: Int } diff --git a/GitLab/GitLabApp.swift b/GitLab/GitLabApp.swift index 1709335..9b372b1 100644 --- a/GitLab/GitLabApp.swift +++ b/GitLab/GitLabApp.swift @@ -8,18 +8,89 @@ import SwiftUI import SwiftData +//https://forums.developer.apple.com/forums/thread/108440 +//The only approach that’s compatible with the App Store is a sandboxed login item, as installed by +// +//SMLoginItemSetEnabled +// . That login item can be a normal app but in most cases you also need IPC between your app and your login item and you can do that using XPC, as illustrated by the AppSandboxLoginItemXPCDemo sample code. +//Share and Enjoy + +import AppKit +import OSLog + +class AppDelegate: NSObject, NSApplicationDelegate { + let log = Logger() + // Needs? "Permitted background task scheduler identifiers" +// let activity = NSBackgroundActivityScheduler(identifier: "updatecheck") + + var service: BackgroundFetcherProtocol? = nil + var connection: NSXPCConnection? = nil + // works here +// var timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { +// (_) in +// let log2 = Logger() +// log2.warning("Jenga 2 (simple timer)") +// } + + func applicationDidFinishLaunching(_ aNotification: Notification) { + print("hi from app delegate") + print("hi 1") +// timer.fire() +// activity.repeats = true +// activity.interval = 10 // seconds +//// activity.tolerance = 0 +//// activity.qualityOfService = .userInitiated +// print("hi 2") +// log.warning("Jenga 4") +//// activity.invalidate() +// activity.schedule { completion in +// // perform activity +// if self.activity.shouldDefer { +// self.log.warning("Jenga 5 (defered)") +// return completion(.deferred) +// } else { +// print("hi 3 from background activity") +// self.log.warning("Jenga 5 (success)") +// return completion(.finished) +// } +// } + +// activity.invalidate() + + + let serviceName = "com.stefkors.BackgroundFetcher" + connection = NSXPCConnection(serviceName: serviceName) + connection?.remoteObjectInterface = NSXPCInterface(with: BackgroundFetcherProtocol.self) + connection?.resume() + + service = connection?.remoteObjectProxyWithErrorHandler { error in + print("Received error:", error) + } as? BackgroundFetcherProtocol + + callXPC() + } + + func callXPC() { + + + service?.performCalculation(firstNumber: 122, secondNumber: 20) + } +} + @main struct GitLabApp: App { - var sharedModelContainer: ModelContainer = .shared + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate - @Environment(\.openURL) var openURL + private var sharedModelContainer: ModelContainer = .shared - @State var receivedURL: URL? + @State private var receivedURL: URL? + @State var searchText: String = "" + @Environment(\.openURL) private var openURL @Environment(\.openWindow) private var openWindow @Environment(\.dismissWindow) private var dismissWindow - @State var searchText: String = "" + let log = Logger() var body: some Scene { // TODO: close after opening link from widget @@ -32,6 +103,9 @@ struct GitLabApp: App { .windowToolbarStyle(.unified(showsTitle: true)) .windowResizability(.contentMinSize) .windowIdealSize(.fitToContent) + .backgroundTask(.urlSession("com.stefkors.GitLab.updatecheck2")) { value in + log.warning("Jenga 5 (success)") + } MenuBarExtra(content: { MainGitLabView() diff --git a/GitLab/Info.plist b/GitLab/Info.plist index ace761e..f3459e8 100644 --- a/GitLab/Info.plist +++ b/GitLab/Info.plist @@ -2,6 +2,11 @@ + BGTaskSchedulerPermittedIdentifiers + + com.stefkors.GitLab.updatecheck + updatecheck + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/Shared/UserInterface/Extensions/IfView.swift b/Shared/UserInterface/Extensions/IfView.swift deleted file mode 100644 index cc53d00..0000000 --- a/Shared/UserInterface/Extensions/IfView.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// IfView.swift -// GitLab -// -// Created by Stef Kors on 02/11/2024. -// - - -extension View { - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } -} diff --git a/Shared/UserInterface/MainContentView.swift b/Shared/UserInterface/MainContentView.swift index d06a0c8..cb93165 100644 --- a/Shared/UserInterface/MainContentView.swift +++ b/Shared/UserInterface/MainContentView.swift @@ -52,6 +52,9 @@ struct MainContentView: View { LastUpdateMessageView() } .frame(maxHeight: .infinity, alignment: .top) + .onChange(of: repos) { oldValue, newValue in + print("updated repos \(repos.count.description)") + } } } diff --git a/Shared/UserInterface/UserInterface.swift b/Shared/UserInterface/UserInterface.swift index 9e5e14f..c0d516a 100644 --- a/Shared/UserInterface/UserInterface.swift +++ b/Shared/UserInterface/UserInterface.swift @@ -33,7 +33,7 @@ struct UserInterface: View { @State private var selectedView: QueryType = .authoredMergeRequests @State private var timelineDate: Date = .now - private let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() +// private let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() @EnvironmentObject private var noticeState: NoticeState @EnvironmentObject private var networkState: NetworkState @@ -82,23 +82,23 @@ struct UserInterface: View { networkState.record = false } } - .task(id: "once") { - Task { - await fetchReviewRequestedMRs() - await fetchAuthoredMRs() - await fetchRepos() - await branchPushes() - } - } - .onReceive(timer) { _ in - timelineDate = .now - Task { - await fetchReviewRequestedMRs() - await fetchAuthoredMRs() - await fetchRepos() - await branchPushes() - } - } +// .task(id: "once") { +// Task { +// await fetchReviewRequestedMRs() +// await fetchAuthoredMRs() +// await fetchRepos() +// await branchPushes() +// } +// } +// .onReceive(timer) { _ in +// timelineDate = .now +// Task { +// await fetchReviewRequestedMRs() +// await fetchAuthoredMRs() +// await fetchRepos() +// await branchPushes() +// } +// } } /// TODO: Cleanup and move both into the same function diff --git a/Shared/UserInterface/Views/PipelineView.swift b/Shared/UserInterface/Views/PipelineView.swift index f3fbc11..30570d1 100644 --- a/Shared/UserInterface/Views/PipelineView.swift +++ b/Shared/UserInterface/Views/PipelineView.swift @@ -37,7 +37,6 @@ struct PipelineView: View { HStack(spacing: 0) { GitLabCIJobsView(stage: stage, instance: instance) .id(stage.id) - // Create a staggered effect by masking children to appear correctly .mask { Circle() .subtracting( From 7e779366c78f7fefe12cfefbb876fe6049a986b3 Mon Sep 17 00:00:00 2001 From: Stef Kors Date: Wed, 6 Nov 2024 18:26:41 +0100 Subject: [PATCH 2/3] fix background shadow for launchpad --- .../SwiftData/LaunchpadState.swift | 18 ++ .../UserInterface/Views/LaunchpadImage.swift | 164 +++++++++++++----- 2 files changed, 142 insertions(+), 40 deletions(-) diff --git a/Shared/UserInterface/SwiftData/LaunchpadState.swift b/Shared/UserInterface/SwiftData/LaunchpadState.swift index bdf6a15..61a8fc7 100644 --- a/Shared/UserInterface/SwiftData/LaunchpadState.swift +++ b/Shared/UserInterface/SwiftData/LaunchpadState.swift @@ -86,4 +86,22 @@ import SwiftData url: URL(string: "https://gitlab.com/stefkors/swiftui-launchpad")!, hasUpdatedSinceLaunch: false ) + + static let preview2 = LaunchpadRepo( + id: "uuid-1", + name: "SwiftUI Launchpad", + image: nil, + group: "StefKors", + url: URL(string: "https://gitlab.com/stefkors/swiftui-launchpad")!, + hasUpdatedSinceLaunch: false + ) + + static let preview3 = LaunchpadRepo( + id: "uuid-2", + name: "React", + image: nil, + group: "StefKors", + url: URL(string: "https://gitlab.com/stefkors/swiftui-launchpad")!, + hasUpdatedSinceLaunch: false + ) } diff --git a/Shared/UserInterface/Views/LaunchpadImage.swift b/Shared/UserInterface/Views/LaunchpadImage.swift index 84e7b56..fe17f40 100644 --- a/Shared/UserInterface/Views/LaunchpadImage.swift +++ b/Shared/UserInterface/Views/LaunchpadImage.swift @@ -5,6 +5,7 @@ // Created by Stef Kors on 17/10/2024. // +import Foundation import SwiftUI struct LaunchpadImage: View { @@ -22,6 +23,20 @@ struct LaunchpadImage: View { private let providerCircleSize: CGFloat = 14 + var placeholder: some View { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .fill(Color.generateHSLColor(for: repo.name)) + .overlay(content: { + if let char = repo.name.first { + Text(String(char).capitalized) + .font(.headline.bold()) + .foregroundStyle(.primary) + .colorInvert() + } + }) + .padding(2) + } + var body: some View { HStack { if let url { @@ -29,51 +44,35 @@ struct LaunchpadImage: View { image .resizable() .transition(.opacity.combined(with: .scale).combined(with: .blurReplace)) + .mask { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .padding(2) + } } placeholder: { - RoundedRectangle(cornerRadius: 6, style: .continuous) - .fill(Color.secondary) - .shadow(radius: 3) - .overlay(content: { - if let char = repo.name.first { - Text(String(char).capitalized) - .font(.headline.bold()) - .foregroundStyle(.primary) - .colorInvert() - } - }) - .padding(2) + placeholder } } else { - RoundedRectangle(cornerRadius: 6, style: .continuous) - .fill(Color.secondary) - .shadow(radius: 3) - .overlay(content: { - if let char = repo.name.first { - Text(String(char).capitalized) - .font(.headline.bold()) - .foregroundStyle(.primary) - .colorInvert() - } - }) - .padding(2) + placeholder } } .frame(width: 32.0, height: 32.0) - .if(repo.provider != nil, transform: { content in - content - .mask({ - Rectangle() - .fill(.white) - .overlay(alignment: .bottomTrailing) { - Circle() - .fill(.black) - .frame(width: providerCircleSize, height: providerCircleSize, alignment: .center) - } - .compositingGroup() - .luminanceToAlpha() - }) - .overlay(alignment: .bottomTrailing) { - if let provider = repo.provider { + .if( + repo.provider != nil, + transform: { content in + content + .mask({ + Rectangle() + .fill(.white) + .overlay(alignment: .bottomTrailing) { + Circle() + .fill(.black) + .frame(width: providerCircleSize, height: providerCircleSize, alignment: .center) + } + .compositingGroup() + .luminanceToAlpha() + }) + .overlay(alignment: .bottomTrailing) { + if let provider = repo.provider { Circle() .fill(.clear) .frame(width: providerCircleSize, height: providerCircleSize, alignment: .center) @@ -83,11 +82,96 @@ struct LaunchpadImage: View { } } }) + .shadow(radius: 3) } } +extension Color { + static func generateColor(for text: String) -> Color { + var hash = 0 + let colorConstant = 131 + let maxSafeValue = Int.max / colorConstant + for char in text.unicodeScalars{ + if hash > maxSafeValue { + hash = hash / colorConstant + } + hash = Int(char.value) + ((hash << 5) - hash) + } + let finalHash = abs(hash) % (256*256*256); + //let color = UIColor(hue:CGFloat(finalHash)/255.0 , saturation: 0.40, brightness: 0.75, alpha: 1.0) + return Color( + red: CGFloat((finalHash & 0xFF0000) >> 16) / 255.0, + green: CGFloat((finalHash & 0xFF00) >> 8) / 255.0, + blue: CGFloat((finalHash & 0xFF)) / 255.0 + ) + } + + static func generateHSLColor(for text: String) -> Color { + var hash = 0; + let colorConstant = 50 + let maxSafeValue = Int.max / colorConstant + for char in text.unicodeScalars { + if hash > maxSafeValue { + hash = hash / colorConstant + } + hash = Int(char.value) + ((hash << 5) - hash) + } + + let hue = hash % 360; + + let finalHue = CGFloat(hue.clamped(to: 0...360))/360 + return Color( + hue: finalHue, + saturation: 72/100, + lightness: 50/100, + opacity: 1.0 + ) + } + + init(hue: CGFloat, saturation: CGFloat, lightness: CGFloat, opacity: CGFloat) { + precondition(0...1 ~= hue && + 0...1 ~= saturation && + 0...1 ~= lightness && + 0...1 ~= opacity, "input range is out of range 0...1") + + //From HSL TO HSB --------- + var newSaturation: Double = 0.0 + + let brightness = lightness + saturation * min(lightness, 1-lightness) + + if brightness == 0 { newSaturation = 0.0 } + else { + newSaturation = 2 * (1 - lightness / brightness) + } + //--------- + + self.init(hue: hue, saturation: newSaturation, brightness: brightness, opacity: opacity) + } +} + +extension Comparable { + func clamped(to limits: ClosedRange) -> Self { + return min(max(self, limits.lowerBound), limits.upperBound) + } +} + #Preview { - LaunchpadImage(repo: .preview) + LazyVGrid(columns: [ + GridItem(.adaptive(minimum: 42), alignment: .leading) + ], alignment: .leading, spacing: 10) { + ForEach(0...26, id: \.self) { letter in + LaunchpadImage(repo: LaunchpadRepo( + id: "uuid", + name: UUID().uuidString, + image: .previewRepoImage, + group: "StefKors", + url: URL(string: "https://gitlab.com/stefkors/swiftui-launchpad")!, + provider: .GitHub, + hasUpdatedSinceLaunch: false + )) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) .scenePadding() } From 2c5dd255d021398f4bd76efe972323356427094f Mon Sep 17 00:00:00 2001 From: Stef Kors Date: Thu, 7 Nov 2024 00:40:26 +0100 Subject: [PATCH 3/3] WIP --- BackgroundFetcher/BackgroundFetcher.swift | 508 +++++++++++++++--- .../BackgroundFetcherProtocol.swift | 4 +- BackgroundFetcher/main.swift | 14 - GitLab iOS/GitLab_iOSApp.swift | 17 +- GitLab.xcodeproj/project.pbxproj | 60 +-- GitLab/ExtraWindow.swift | 54 -- GitLab/GitLabApp.swift | 62 +-- .../Extensions/ModelContainer.swift | 4 +- Shared/UserInterface/MainContentView.swift | 3 +- Shared/UserInterface/MainGitLabView.swift | 25 - Shared/UserInterface/Models/NetworkInfo.swift | 17 - .../Models/NetworkManagerGitHub.swift | 1 - .../UserInterface/Models/NetworkState.swift | 32 -- .../Models/NoticeState/NoticeState.swift | 65 --- .../UserInterface/Models/StructsGitLab.swift | 10 + .../SwiftData/LaunchpadState.swift | 12 +- .../{Models => SwiftData}/NetworkEvent.swift | 30 +- .../NoticeMessage.swift | 8 +- .../NoticeType.swift | 0 Shared/UserInterface/UserInterface.swift | 281 ---------- .../UserInterface/Views/LaunchpadImage.swift | 1 + .../Views/NetworkStateView.swift | 20 +- .../Views/NoticeViews/BaseNoticeItem.swift | 16 +- .../Views/NoticeViews/NoticeListView.swift | 9 +- 24 files changed, 522 insertions(+), 731 deletions(-) delete mode 100644 Shared/UserInterface/MainGitLabView.swift delete mode 100644 Shared/UserInterface/Models/NetworkInfo.swift delete mode 100644 Shared/UserInterface/Models/NetworkState.swift delete mode 100644 Shared/UserInterface/Models/NoticeState/NoticeState.swift rename Shared/UserInterface/{Models => SwiftData}/NetworkEvent.swift (55%) rename Shared/UserInterface/{Models/NoticeState => SwiftData}/NoticeMessage.swift (92%) rename Shared/UserInterface/{Models/NoticeState => SwiftData}/NoticeType.swift (100%) diff --git a/BackgroundFetcher/BackgroundFetcher.swift b/BackgroundFetcher/BackgroundFetcher.swift index ba25fe6..dc44f79 100644 --- a/BackgroundFetcher/BackgroundFetcher.swift +++ b/BackgroundFetcher/BackgroundFetcher.swift @@ -8,99 +8,463 @@ import Foundation import OSLog import SwiftData -/// This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. -class BackgroundFetcher: NSObject, BackgroundFetcherProtocol { - let log = Logger() +import Get -// var helloWorldTimer = Timer.scheduledTimer( -// timeInterval: 6.0, -// target: BackgroundFetcher.self, -// selector: #selector(Self.sayHello), -// userInfo: nil, -// repeats: true -// ) +/// ```swift +/// // It is important that this actor works as a mutex, +/// // so you must have one instance of the Actor for one container +// // for it to work correctly. +/// let actor = BackgroundSerialPersistenceActor(container: modelContainer) +/// +/// Task { +/// let data: [MyModel] = try? await actor.fetchData() +/// } +/// ``` +//@available(iOS 17, *) +//public actor BackgroundSerialPersistenceActor: ModelActor { +// +// public let modelContainer: ModelContainer +// public let modelExecutor: any ModelExecutor +// private var context: ModelContext { modelExecutor.modelContext } // -// @objc func sayHello() { -// print("hello World") -// log.warning("Jenga 5 (success say hello)") +// public init(container: ModelContainer) { +// self.modelContainer = container +// let context = ModelContext(modelContainer) +// modelExecutor = DefaultSerialModelExecutor(modelContext: context) // } // -// var timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { -// (_) in -// let log2 = Logger() -// log2.warning("Jenga 2 (simple timer)") +// public func fetchData( +// predicate: Predicate? = nil, +// sortBy: [SortDescriptor] = [] +// ) throws -> [T] { +// let fetchDescriptor = FetchDescriptor(predicate: predicate, sortBy: sortBy) +// let list: [T] = try context.fetch(fetchDescriptor) +// return list // } +// +// public func fetchCount( +// predicate: Predicate? = nil, +// sortBy: [SortDescriptor] = [] +// ) throws -> Int { +// let fetchDescriptor = FetchDescriptor(predicate: predicate, sortBy: sortBy) +// let count = try context.fetchCount(fetchDescriptor) +// return count +// } +// +// public func insert(_ data: T) { +// let context = data.modelContext ?? context +// context.insert(data) +// } +// +// public func save() throws { +// try context.save() +// } +// +// public func remove(predicate: Predicate? = nil) throws { +// try context.delete(model: T.self, where: predicate) +// } +// +// public func delete(_ model: T) throws { +// try context.delete(model) +// } +// +// public func saveAndInsertIfNeeded( +// data: T, +// predicate: Predicate +// ) throws { +// let descriptor = FetchDescriptor(predicate: predicate) +// let context = data.modelContext ?? context +// let savedCount = try context.fetchCount(descriptor) +// +// if savedCount == 0 { +// context.insert(data) +// } +// try context.save() +// } +//} +// +@ModelActor +public actor ModelActorDatabase { + @MainActor + public init(modelContainer: ModelContainer, mainActor _: Bool) { + let modelContext = modelContainer.mainContext + modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) + self.modelContainer = modelContainer + } + + public func delete(_ model: some PersistentModel) async { + self.modelContext.delete(model) + } + + public func insert(_ model: some PersistentModel) async { + self.modelContext.insert(model) + } + + public func delete( + where predicate: Predicate? + ) async throws { + try self.modelContext.delete(model: T.self, where: predicate) + } + + public func save() async throws { + try self.modelContext.save() + } + + public func fetch(_ descriptor: FetchDescriptor) async throws -> [T] where T: PersistentModel { + return try self.modelContext.fetch(descriptor) + } + + public func fetchData( + predicate: Predicate? = nil, + sortBy: [SortDescriptor] = [] + ) throws -> [T] { + let fetchDescriptor = FetchDescriptor(predicate: predicate, sortBy: sortBy) + let list: [T] = try self.modelContext.fetch(fetchDescriptor) + return list + } +} + +/// This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. +class BackgroundFetcher: NSObject, BackgroundFetcherProtocol { + let log = Logger() let activity = NSBackgroundActivityScheduler(identifier: "com.stefkors.GitLab.updatecheck") var count: Int = 0 var date = Date.now + let actor = ModelActorDatabase(modelContainer: .shared) - /// This implements the example protocol. Replace the body of this class with the implementation of this service's protocol. - @objc func performCalculation(firstNumber: Int, secondNumber: Int) { - let response = firstNumber + secondNumber - print("response: \(response)") - if count == 0 { - startBackground() - } -// log.warning("Jenga 3 performCalc") -// timer.fire() -// activity.invalidate() -// activity.repeats = true -// activity.interval = 2 // seconds -// activity.tolerance = 0 -// activity.qualityOfService = .userInteractive -//// print("hi 2") -//// log.warning("Jenga 4") -// activity.schedule { completion in -// // perform activity -// let newDate = Date.now -// self.log.warning("Jenga 5 (success performCalc) \(response) \(self.count.description) sinceLast: \(newDate.timeIntervalSince(self.date)) seconds") -// self.count += 1 -// self.date = Date.now -// completion(.finished) -// } - } - - func startBackground() { + func startFetching() { + log.info("startFetching is called") activity.invalidate() activity.repeats = true - activity.interval = 8 // seconds + activity.interval = 20 // seconds activity.tolerance = 1 activity.qualityOfService = .userInteractive - // print("hi 2") - // log.warning("Jenga 4") - activity.schedule { completion in - - let context = ModelContext(.shared) - - // WORKS! -// let repo = LaunchpadRepo( -// id: UUID().uuidString, -// name: "Test Repo \(self.count.description)", -// group: "Apple", -// url: URL(string: "https://google.com")!, -// provider: .GitHub -// ) -// context.insert(repo) -// try? context.save() - let newDate = Date.now - let interval = newDate.timeIntervalSince(self.date) - if interval < self.activity.interval { - return completion(.finished) +// Task { +// await self.fetchReviewRequestedMRs() +// await self.fetchAuthoredMRs() +// await self.fetchRepos() +// await self.branchPushes() +// } + + activity.schedule { [weak self] completion in + print("run schedule") + + Task { + do { + try await self?.fetchReviewRequestedMRs() + try await self?.fetchAuthoredMRs() + try await self?.fetchRepos() + try await self?.branchPushes() + } catch { + print("Error in background fetcher: \(error.localizedDescription)") + } } - self.log.warning("Jenga 5 (success performCalc) \(self.count.description) sinceLast: \(interval) seconds shoulddefered? \(self.activity.shouldDefer)") - self.count += 1 - self.date = Date.now + + let newDate = Date.now + let interval = newDate.timeIntervalSince(self?.date ?? Date.now) + self?.log.info("startFetching: \(self?.count.description ?? "") sinceLast: \(interval) seconds should defer? \(self?.activity.shouldDefer ?? false)") + self?.count += 1 + self?.date = Date.now completion(.finished) } } - /// This implements the example protocol. Replace the body of this class with the implementation of this service's protocol. - @objc func performCalculation(firstNumber: Int, secondNumber: Int, with reply: @escaping (Int) -> Void) { - let response = firstNumber + secondNumber - reply(response) + @MainActor + private func fetchReviewRequestedMRs() async throws { + let accounts: [Account] = try await actor.fetchData() +// let context = ModelContext(.shared) +// let accounts = (try? context.fetch(FetchDescriptor())) ?? [] + + for account in accounts { + if account.provider == .GitLab { + let info = NetworkInfo(label: "Fetch Review Requested Merge Requests", account: account, method: .get) + let results = await wrapRequest(info: info) { + try await NetworkManagerGitLab.shared.fetchReviewRequestedMergeRequests(with: account) + } + + if let results { + try await removeAndInsertUniversal( + .reviewRequestedMergeRequests, + account: account, + results: results + ) + } + } + } + } + + @MainActor + private func fetchAuthoredMRs() async throws { +// let context = ModelContainer.shared.mainContext +// let accounts = (try? context.fetch(FetchDescriptor())) ?? [] + let accounts: [Account] = try await actor.fetchData() + + for account in accounts { + if account.provider == .GitLab { + let info = NetworkInfo( + label: "Fetch Authored Merge Requests", + account: account, + method: .get + ) + let results = await wrapRequest(info: info) { + try await NetworkManagerGitLab.shared.fetchAuthoredMergeRequests(with: account) + } + + if let results { + try await removeAndInsertUniversal( + .authoredMergeRequests, + account: account, + results: results + ) + } + } else { + let info = NetworkInfo( + label: "Fetch Authored Pull Requests", + account: account, + method: .get + ) + let results = await wrapRequest(info: info) { + try await NetworkManagerGitHub.shared.fetchAuthoredPullRequests(with: account) + } + + if let results { + try await removeAndInsertUniversal( + .authoredMergeRequests, + account: account, + results: results + ) + } + } + } + } + + @MainActor + private func removeAndInsertUniversal(_ type: QueryType, account: Account, results: [GitLab.MergeRequest]) async throws { + // Map results to universal request + let requests = results.map { result in + return UniversalMergeRequest( + request: result, + account: account, + provider: .GitLab, + type: type + ) + } + // Call universal remove and insert + try await removeAndInsertUniversal(type, account: account, requests: requests) + } + + @MainActor + private func removeAndInsertUniversal(_ type: QueryType, account: Account, results: [GitHub.PullRequestsNode]) async throws { + // Map results to universal request + let requests = results.map { result in + return UniversalMergeRequest( + request: result, + account: account, + provider: .GitHub, + type: type + ) + } + // Call universal remove and insert + try await removeAndInsertUniversal(type, account: account, requests: requests) + } + + @MainActor + private func removeAndInsertUniversal(_ type: QueryType, account: Account, requests: [UniversalMergeRequest]) async throws { + let mergeRequests: [UniversalMergeRequest] = try await actor.fetchData() + let repos: [LaunchpadRepo] = try await actor.fetchData() +// let accounts = (try? context.fetch(FetchDescriptor())) ?? [] +// let mergeRequests = (try? context.fetch(FetchDescriptor())) ?? [] +// let repos = (try? context.fetch(FetchDescriptor())) ?? [] + + // Get array of ids of current of type + let existing = mergeRequests.filter({ $0.type == type }).map({ $0.requestID }) + // Get array of new of current of type + let updated = requests.map { $0.requestID } + // Compute difference + let difference = existing.difference(from: updated) + // Delete existing + for pullRequest in account.requests { + if difference.contains(pullRequest.requestID) { + print("removing \(pullRequest.requestID)") + await actor.delete(pullRequest) + try await actor.save() + } + } + + for request in requests { + // update values + if let existingMR = mergeRequests.first(where: { request.requestID == $0.requestID }) { + existingMR.mergeRequest = request.mergeRequest + existingMR.pullRequest = request.pullRequest + } else { + // if not insert + await actor.insert(request) + } + + // If no matching launchpad repo, insert a new one + let launchPadItem = repos.first { repo in + repo.url == request.repoUrl + } + + if let launchPadItem { + if launchPadItem.hasUpdatedSinceLaunch == false { + if let name = request.repoName { + launchPadItem.name = name + } + if let owner = request.repoOwner { + launchPadItem.group = owner + } + if let url = request.repoUrl { + launchPadItem.url = url + } + if let imageURL = request.repoImage { + launchPadItem.imageURL = imageURL + } + launchPadItem.provider = request.provider + launchPadItem.hasUpdatedSinceLaunch = true + } + } else if let name = request.repoName, + let owner = request.repoOwner, + let url = request.repoUrl { + + let repo = LaunchpadRepo( + id: request.repoId ?? UUID().uuidString, + name: name, + imageURL: request.repoImage, + group: owner, + url: url, + provider: request.provider + ) + + await actor.insert(repo) + } + } + } + + // TDOO: fix this mess with split gitlab (below) and github (above) logic + @MainActor + private func fetchRepos() async throws { + let accounts: [Account] = try await actor.fetchData() + let mergeRequests: [UniversalMergeRequest] = try await actor.fetchData() + let repos: [LaunchpadRepo] = try await actor.fetchData() +// let mergeRequests = (try? context.fetch(FetchDescriptor())) ?? [] +// let repos = (try? context.fetch(FetchDescriptor())) ?? [] + + for account in accounts { + if account.provider == .GitLab { + let ids = Array(Set(mergeRequests.compactMap { request in + if request.provider == .GitLab { + return request.mergeRequest?.targetProject?.id.split(separator: "/").last + } else { + return nil + } + }.compactMap({ Int($0) }))) + + let info = NetworkInfo(label: "Fetch Projects \(ids)", account: account, method: .get) + let results = await wrapRequest(info: info) { + try await NetworkManagerGitLab.shared.fetchProjects(with: account, ids: ids) + } + + if let results { + for result in results { + if let url = result.webURL { + // If no matching launchpad repo, insert a new one + let launchPadItem = repos.first { repo in + repo.url == url + } + + if let launchPadItem { + if launchPadItem.hasUpdatedSinceLaunch == false { + if let name = result.name { + launchPadItem.name = name + } + if let owner = result.group?.fullName ?? result.namespace?.fullName { + launchPadItem.group = owner + } + launchPadItem.url = url + if let image = await NetworkManagerGitLab.shared.getProjectImage(with: account, result) { + launchPadItem.image = image + } + launchPadItem.provider = account.provider + launchPadItem.hasUpdatedSinceLaunch = true + } + } else { + let repo = LaunchpadRepo( + id: result.id, + name: result.name ?? "", + image: await NetworkManagerGitLab.shared.getProjectImage(with: account, result), + group: result.group?.fullName ?? result.namespace?.fullName ?? "", + url: url, + hasUpdatedSinceLaunch: true + ) + await actor.insert(repo) + } + } + } + try await actor.save() + } + } + } + } + + @MainActor + private func branchPushes() async throws { + let accounts: [Account] = try await actor.fetchData() + let mergeRequests: [UniversalMergeRequest] = try await actor.fetchData() + let repos: [LaunchpadRepo] = try await actor.fetchData() + + + for account in accounts { + if account.provider == .GitLab { + let info = NetworkInfo(label: "Branch Push", account: account, method: .get) + let notice = await wrapRequest(info: info) { + try await NetworkManagerGitLab.shared.fetchLatestBranchPush(with: account, repos: repos) + } + + if let notice { + if notice.type == .branch, let branch = notice.branchRef { + + let matchedMR = mergeRequests.first { request in + return request.sourceBranch == branch + } + + let alreadyHasMR = matchedMR != nil + + if alreadyHasMR || !notice.createdAt.isWithinLastHours(1) { + return + } + } + await actor.insert(notice) + } + } + } + } + + @MainActor + private func wrapRequest(info: NetworkInfo, do request: () async throws -> T?) async -> T? { +// let context = ModelContainer.shared.mainContext + let event = NetworkEvent(info: info, status: nil, response: nil) + await actor.insert(event) + do { + let result = try await request() + event.status = 200 + event.response = result.debugDescription +// networkState.update(event) + return result + } catch APIError.unacceptableStatusCode(let statusCode) { + event.status = statusCode + event.response = "Unacceptable Status Code: \(statusCode)" +// networkState.update(event) + } catch let error { + event.status = 0 + event.response = error.localizedDescription +// networkState.update(event) + } + + return nil } } diff --git a/BackgroundFetcher/BackgroundFetcherProtocol.swift b/BackgroundFetcher/BackgroundFetcherProtocol.swift index 444b8e4..452ed0e 100644 --- a/BackgroundFetcher/BackgroundFetcherProtocol.swift +++ b/BackgroundFetcher/BackgroundFetcherProtocol.swift @@ -9,9 +9,7 @@ import Foundation /// The protocol that this service will vend as its API. This protocol will also need to be visible to the process hosting the service. @objc protocol BackgroundFetcherProtocol { - func performCalculation(firstNumber: Int, secondNumber: Int) - /// Replace the API of this protocol with an API appropriate to the service you are vending. - func performCalculation(firstNumber: Int, secondNumber: Int, with reply: @escaping (Int) -> Void) + func startFetching() } /* diff --git a/BackgroundFetcher/main.swift b/BackgroundFetcher/main.swift index a017334..4ff73c4 100644 --- a/BackgroundFetcher/main.swift +++ b/BackgroundFetcher/main.swift @@ -10,12 +10,6 @@ import OSLog class ServiceDelegate: NSObject, NSXPCListenerDelegate { -// var timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { -// (_) in -// let log2 = Logger() -// log2.warning("Jenga 5 (simple timer service delegate)") -// } - /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { @@ -30,19 +24,11 @@ class ServiceDelegate: NSObject, NSXPCListenerDelegate { // Resuming the connection allows the system to deliver more incoming messages. newConnection.resume() -// timer.fire() - // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false. return true } } -//var timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { -// (_) in -// let log2 = Logger() -// log2.warning("Jenga 5 (simple timer from main xpc)") -//} -//timer.fire() // Create the delegate for the service. let delegate = ServiceDelegate() diff --git a/GitLab iOS/GitLab_iOSApp.swift b/GitLab iOS/GitLab_iOSApp.swift index 8a99f04..c07f7f6 100644 --- a/GitLab iOS/GitLab_iOSApp.swift +++ b/GitLab iOS/GitLab_iOSApp.swift @@ -10,20 +10,6 @@ import SwiftData @main struct GitLab_iOSApp: App { - // Non-Persisted state objects - @StateObject private var noticeState = NoticeState() - - // Persistance objects - var sharedModelContainer: ModelContainer = { - let schema = Schema([Account.self, MergeRequest.self, UniversalMergeRequest.self, PullRequest.self, LaunchpadRepo.self]) - let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) - - do { - return try ModelContainer(for: schema, configurations: [modelConfiguration]) - } catch { - fatalError("Could not create ModelContainer: \(error)") - } - }() var body: some Scene { WindowGroup { @@ -41,8 +27,7 @@ struct GitLab_iOSApp: App { } } } - .environmentObject(self.noticeState) - .modelContainer(sharedModelContainer) + .modelContainer(.shared) } } } diff --git a/GitLab.xcodeproj/project.pbxproj b/GitLab.xcodeproj/project.pbxproj index 896ae1f..e155479 100644 --- a/GitLab.xcodeproj/project.pbxproj +++ b/GitLab.xcodeproj/project.pbxproj @@ -45,22 +45,11 @@ 8A111F452CC1384E00DEB0DD /* NetworkStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F432CC1384E00DEB0DD /* NetworkStateView.swift */; }; 8A111F462CC1384E00DEB0DD /* NetworkStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F432CC1384E00DEB0DD /* NetworkStateView.swift */; }; 8A111F472CC1384E00DEB0DD /* NetworkStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F432CC1384E00DEB0DD /* NetworkStateView.swift */; }; - 8A111F492CC1386600DEB0DD /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F482CC1386600DEB0DD /* NetworkState.swift */; }; - 8A111F4A2CC1386600DEB0DD /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F482CC1386600DEB0DD /* NetworkState.swift */; }; - 8A111F4B2CC1386600DEB0DD /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F482CC1386600DEB0DD /* NetworkState.swift */; }; - 8A111F4C2CC1386600DEB0DD /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F482CC1386600DEB0DD /* NetworkState.swift */; }; 8A111F4E2CC1387F00DEB0DD /* NetworkEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */; }; 8A111F4F2CC1387F00DEB0DD /* NetworkEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */; }; 8A111F502CC1387F00DEB0DD /* NetworkEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */; }; 8A111F512CC1387F00DEB0DD /* NetworkEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */; }; - 8A111F532CC1388900DEB0DD /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F522CC1388900DEB0DD /* NetworkInfo.swift */; }; - 8A111F542CC1388900DEB0DD /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F522CC1388900DEB0DD /* NetworkInfo.swift */; }; - 8A111F552CC1388900DEB0DD /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F522CC1388900DEB0DD /* NetworkInfo.swift */; }; - 8A111F562CC1388900DEB0DD /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F522CC1388900DEB0DD /* NetworkInfo.swift */; }; 8A13B85E2A66A3E30090A6D9 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 8A13B85D2A66A3E30090A6D9 /* Credits.rtf */; }; - 8A2E61852A9766A6001B6EAE /* MainGitLabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A2E61842A9766A6001B6EAE /* MainGitLabView.swift */; }; - 8A2E61862A9766A6001B6EAE /* MainGitLabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A2E61842A9766A6001B6EAE /* MainGitLabView.swift */; }; - 8A2E61872A9766A6001B6EAE /* MainGitLabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A2E61842A9766A6001B6EAE /* MainGitLabView.swift */; }; 8A31CB3D2866334000C94AC1 /* GitLab_iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A31CB3C2866334000C94AC1 /* GitLab_iOSApp.swift */; }; 8A31CB412866334100C94AC1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8A31CB402866334100C94AC1 /* Assets.xcassets */; }; 8A31CB442866334100C94AC1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8A31CB432866334100C94AC1 /* Preview Assets.xcassets */; }; @@ -97,15 +86,12 @@ 8A4534492CD7EB0B0011D5B5 /* CachedAsyncImage+ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE92A55817400819B80 /* CachedAsyncImage+ImageCache.swift */; }; 8A45344A2CD7EB0B0011D5B5 /* gitlabISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE82A55817400819B80 /* gitlabISO8601DateFormatter.swift */; }; 8A45344B2CD7EB0B0011D5B5 /* NetworkEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */; }; - 8A45344C2CD7EB0B0011D5B5 /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F522CC1388900DEB0DD /* NetworkInfo.swift */; }; 8A45344D2CD7EB0B0011D5B5 /* NetworkManagerGitLab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE62A55817400819B80 /* NetworkManagerGitLab.swift */; }; 8A45344E2CD7EB0B0011D5B5 /* NetworkManagerGitLab+repoLaunchPad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE02A55817400819B80 /* NetworkManagerGitLab+repoLaunchPad.swift */; }; 8A45344F2CD7EB0B0011D5B5 /* NetworkManagerGitHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7C0AD42CD292A700E479CA /* NetworkManagerGitHub.swift */; }; 8A4534502CD7EB0B0011D5B5 /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CEF2A55817400819B80 /* NetworkReachability.swift */; }; - 8A4534512CD7EB0B0011D5B5 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F482CC1386600DEB0DD /* NetworkState.swift */; }; 8A4534522CD7EB0B0011D5B5 /* NoticeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF12A55817400819B80 /* NoticeMessage.swift */; }; 8A4534532CD7EB0B0011D5B5 /* NoticeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF22A55817400819B80 /* NoticeType.swift */; }; - 8A4534542CD7EB0B0011D5B5 /* NoticeState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF32A55817400819B80 /* NoticeState.swift */; }; 8A4534552CD7EB0B0011D5B5 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE52A55817400819B80 /* NotificationManager.swift */; }; 8A4534562CD7EB0B0011D5B5 /* QueryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A111F3E2CC12C5F00DEB0DD /* QueryType.swift */; }; 8A4534572CD7EB0B0011D5B5 /* PipelineStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A858F212CD527B10024795D /* PipelineStatus.swift */; }; @@ -131,7 +117,6 @@ 8A7935CF2A5583E400F8FB6C /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CEF2A55817400819B80 /* NetworkReachability.swift */; }; 8A7935D02A5583E400F8FB6C /* NoticeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF12A55817400819B80 /* NoticeMessage.swift */; }; 8A7935D12A5583E400F8FB6C /* NoticeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF22A55817400819B80 /* NoticeType.swift */; }; - 8A7935D22A5583E400F8FB6C /* NoticeState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF32A55817400819B80 /* NoticeState.swift */; }; 8A7935D32A5583E400F8FB6C /* CISkippedIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF52A55817400819B80 /* CISkippedIcon.swift */; }; 8A7935D42A5583E400F8FB6C /* CIManualIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF62A55817400819B80 /* CIManualIcon.swift */; }; 8A7935D52A5583E400F8FB6C /* CICanceledIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF72A55817400819B80 /* CICanceledIcon.swift */; }; @@ -314,7 +299,6 @@ 8AE8A6FC2B989EFA002B3C9E /* NoticeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF22A55817400819B80 /* NoticeType.swift */; }; 8AE8A6FD2B989EFA002B3C9E /* ApprovedReviewIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CFA2A55817400819B80 /* ApprovedReviewIcon.swift */; }; 8AE8A6FE2B989EFA002B3C9E /* CIStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80D162A55817400819B80 /* CIStatusView.swift */; }; - 8AE8A6FF2B989EFA002B3C9E /* MainGitLabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A2E61842A9766A6001B6EAE /* MainGitLabView.swift */; }; 8AE8A7002B989EFA002B3C9E /* LaunchPadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80D1B2A55817400819B80 /* LaunchPadView.swift */; }; 8AE8A7012B989EFA002B3C9E /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC0C2092A5D3D720096772B /* AccessToken.swift */; }; 8AE8A7022B989EFA002B3C9E /* AccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80D0C2A55817400819B80 /* AccountSettingsView.swift */; }; @@ -325,7 +309,6 @@ 8AE8A7082B989EFA002B3C9E /* MenuBarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80D1F2A55817400819B80 /* MenuBarButtonStyle.swift */; }; 8AE8A7092B989EFA002B3C9E /* CISkippedIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF52A55817400819B80 /* CISkippedIcon.swift */; }; 8AE8A70A2B989EFA002B3C9E /* gitlabISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE82A55817400819B80 /* gitlabISO8601DateFormatter.swift */; }; - 8AE8A70B2B989EFA002B3C9E /* NoticeState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF32A55817400819B80 /* NoticeState.swift */; }; 8AE8A70D2B989EFA002B3C9E /* CachedAsyncImage+ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE92A55817400819B80 /* CachedAsyncImage+ImageCache.swift */; }; 8AE8A70E2B989EFA002B3C9E /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CE52A55817400819B80 /* NotificationManager.swift */; }; 8AE8A70F2B989EFA002B3C9E /* CICanceledIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF72A55817400819B80 /* CICanceledIcon.swift */; }; @@ -392,8 +375,6 @@ 8AF80D522A55817400819B80 /* NoticeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF12A55817400819B80 /* NoticeMessage.swift */; }; 8AF80D542A55817400819B80 /* NoticeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF22A55817400819B80 /* NoticeType.swift */; }; 8AF80D552A55817400819B80 /* NoticeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF22A55817400819B80 /* NoticeType.swift */; }; - 8AF80D572A55817400819B80 /* NoticeState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF32A55817400819B80 /* NoticeState.swift */; }; - 8AF80D582A55817400819B80 /* NoticeState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF32A55817400819B80 /* NoticeState.swift */; }; 8AF80D5A2A55817400819B80 /* CISkippedIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF52A55817400819B80 /* CISkippedIcon.swift */; }; 8AF80D5B2A55817400819B80 /* CISkippedIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF52A55817400819B80 /* CISkippedIcon.swift */; }; 8AF80D5D2A55817400819B80 /* CIManualIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF62A55817400819B80 /* CIManualIcon.swift */; }; @@ -584,11 +565,8 @@ 8A111F392CC1203700DEB0DD /* DoubleLineMergeRequestSubRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleLineMergeRequestSubRowView.swift; sourceTree = ""; }; 8A111F3E2CC12C5F00DEB0DD /* QueryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryType.swift; sourceTree = ""; }; 8A111F432CC1384E00DEB0DD /* NetworkStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStateView.swift; sourceTree = ""; }; - 8A111F482CC1386600DEB0DD /* NetworkState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkState.swift; sourceTree = ""; }; 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkEvent.swift; sourceTree = ""; }; - 8A111F522CC1388900DEB0DD /* NetworkInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInfo.swift; sourceTree = ""; }; 8A13B85D2A66A3E30090A6D9 /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; - 8A2E61842A9766A6001B6EAE /* MainGitLabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainGitLabView.swift; sourceTree = ""; }; 8A2F5B8A2A54C5BD00C0B52F /* Small Icon16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Small Icon16.png"; sourceTree = ""; }; 8A2F5B8B2A54C5BD00C0B52F /* Large Icon1024.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Large Icon1024.png"; sourceTree = ""; }; 8A2F5B8C2A54C5BD00C0B52F /* watchos.svg */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = watchos.svg; sourceTree = ""; }; @@ -706,7 +684,6 @@ 8AF80CEF2A55817400819B80 /* NetworkReachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkReachability.swift; sourceTree = ""; }; 8AF80CF12A55817400819B80 /* NoticeMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoticeMessage.swift; sourceTree = ""; }; 8AF80CF22A55817400819B80 /* NoticeType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoticeType.swift; sourceTree = ""; }; - 8AF80CF32A55817400819B80 /* NoticeState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoticeState.swift; sourceTree = ""; }; 8AF80CF52A55817400819B80 /* CISkippedIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CISkippedIcon.swift; sourceTree = ""; }; 8AF80CF62A55817400819B80 /* CIManualIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIManualIcon.swift; sourceTree = ""; }; 8AF80CF72A55817400819B80 /* CICanceledIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CICanceledIcon.swift; sourceTree = ""; }; @@ -1120,7 +1097,6 @@ 8AF80CF42A55817400819B80 /* Icons */, 8AF80D062A55817400819B80 /* UserInterface.swift */, 8AE274272CC3DD060059244E /* MainContentView.swift */, - 8A2E61842A9766A6001B6EAE /* MainGitLabView.swift */, 8AF80D072A55817400819B80 /* Views */, ); path = UserInterface; @@ -1132,14 +1108,10 @@ 8AC0C2092A5D3D720096772B /* AccessToken.swift */, 8AF80CE92A55817400819B80 /* CachedAsyncImage+ImageCache.swift */, 8AF80CE82A55817400819B80 /* gitlabISO8601DateFormatter.swift */, - 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */, - 8A111F522CC1388900DEB0DD /* NetworkInfo.swift */, 8AF80CE62A55817400819B80 /* NetworkManagerGitLab.swift */, 8AF80CE02A55817400819B80 /* NetworkManagerGitLab+repoLaunchPad.swift */, 8A7C0AD42CD292A700E479CA /* NetworkManagerGitHub.swift */, 8AF80CEF2A55817400819B80 /* NetworkReachability.swift */, - 8A111F482CC1386600DEB0DD /* NetworkState.swift */, - 8AF80CF02A55817400819B80 /* NoticeState */, 8AF80CE52A55817400819B80 /* NotificationManager.swift */, 8A111F3E2CC12C5F00DEB0DD /* QueryType.swift */, 8AF80CE72A55817400819B80 /* StructsGitLab.swift */, @@ -1149,16 +1121,6 @@ path = Models; sourceTree = ""; }; - 8AF80CF02A55817400819B80 /* NoticeState */ = { - isa = PBXGroup; - children = ( - 8AF80CF12A55817400819B80 /* NoticeMessage.swift */, - 8AF80CF22A55817400819B80 /* NoticeType.swift */, - 8AF80CF32A55817400819B80 /* NoticeState.swift */, - ); - path = NoticeState; - sourceTree = ""; - }; 8AF80CF42A55817400819B80 /* Icons */ = { isa = PBXGroup; children = ( @@ -1262,9 +1224,12 @@ 8AFDAA962A84FAED001937AC /* SwiftData */ = { isa = PBXGroup; children = ( + 8AF80CF12A55817400819B80 /* NoticeMessage.swift */, + 8AF80CF22A55817400819B80 /* NoticeType.swift */, 8A55E3C72CD6C8B2005B4AA5 /* LaunchpadState.swift */, 8A7C0AFC2CD3657100E479CA /* UniversalMergeRequest.swift */, 8AFDAA972A84FB26001937AC /* Account.swift */, + 8A111F4D2CC1387F00DEB0DD /* NetworkEvent.swift */, ); path = SwiftData; sourceTree = ""; @@ -1679,11 +1644,9 @@ 8AF80DC72A55817400819B80 /* GitLabCIJobsView.swift in Sources */, 8AE2743D2CC67A530059244E /* IfViewModifier.swift in Sources */, 8A858F242CD527B10024795D /* PipelineStatus.swift in Sources */, - 8A2E61862A9766A6001B6EAE /* MainGitLabView.swift in Sources */, 8A111F2A2CC11A8C00DEB0DD /* ShadedButton.swift in Sources */, 8A111F212CC1161500DEB0DD /* String.swift in Sources */, 8AF80D4F2A55817400819B80 /* NetworkReachability.swift in Sources */, - 8A111F552CC1388900DEB0DD /* NetworkInfo.swift in Sources */, 8A858F1E2CD522630024795D /* GitHubCIJobsView.swift in Sources */, 8A7C0ADB2CD29D2500E479CA /* StructsGitHub.swift in Sources */, 8AF80D732A55817400819B80 /* DiscussionCountIcon.swift in Sources */, @@ -1709,7 +1672,6 @@ 8ACBFD392CC270AC00A8753B /* PluralWidgetTitle.swift in Sources */, 8ADD57112B8220D3001F8E8F /* ModelContainer.swift in Sources */, 8AF80D672A55817400819B80 /* CIPreparingIcon.swift in Sources */, - 8AF80D582A55817400819B80 /* NoticeState.swift in Sources */, 8AF80D762A55817400819B80 /* CIScheduledIcon.swift in Sources */, 8A9D87902BD2786E00E2C0CD /* AutoSizingWebLinks.swift in Sources */, 8A111F3D2CC1203700DEB0DD /* DoubleLineMergeRequestSubRowView.swift in Sources */, @@ -1717,7 +1679,6 @@ 8AF80D8E2A55817400819B80 /* UserInterface.swift in Sources */, 8AF80D6D2A55817400819B80 /* CISuccessIcon.swift in Sources */, 8AF80D642A55817400819B80 /* CIFailedIcon.swift in Sources */, - 8A111F492CC1386600DEB0DD /* NetworkState.swift in Sources */, 8AE274262CC3DA9D0059244E /* OverflowContentViewModifier.swift in Sources */, 8A7C0AEE2CD364B900E479CA /* GitHubAccountView.swift in Sources */, 8AFDAAC62A850793001937AC /* AccountRow.swift in Sources */, @@ -1759,7 +1720,6 @@ 8A7935C82A5583E400F8FB6C /* gitlabISO8601DateFormatter.swift in Sources */, 8AC0C2102A5D51FA0096772B /* TokenInformationView.swift in Sources */, 8ABBD2082B7E2E70007C03E6 /* Array+Difference.swift in Sources */, - 8A111F562CC1388900DEB0DD /* NetworkInfo.swift in Sources */, 8A7C0ADD2CD29D2500E479CA /* StructsGitHub.swift in Sources */, 8A7C0AEF2CD364B900E479CA /* GitHubAccountView.swift in Sources */, 8A7935C92A5583E400F8FB6C /* CachedAsyncImage+ImageCache.swift in Sources */, @@ -1768,7 +1728,6 @@ 8A7935CF2A5583E400F8FB6C /* NetworkReachability.swift in Sources */, 8A7935D02A5583E400F8FB6C /* NoticeMessage.swift in Sources */, 8A7935D12A5583E400F8FB6C /* NoticeType.swift in Sources */, - 8A7935D22A5583E400F8FB6C /* NoticeState.swift in Sources */, 8AC7B4B72CB5BFD400CBD21C /* CIWarningIcon.swift in Sources */, 8A7935D32A5583E400F8FB6C /* CISkippedIcon.swift in Sources */, 8ACBFD3A2CC270AC00A8753B /* PluralWidgetTitle.swift in Sources */, @@ -1805,7 +1764,6 @@ 8A55E3C92CD6C8B2005B4AA5 /* LaunchpadState.swift in Sources */, 8A7935E02A5583E400F8FB6C /* CICreatedIcon.swift in Sources */, 8A7935E12A5583E400F8FB6C /* NeedsReviewIcon.swift in Sources */, - 8A2E61872A9766A6001B6EAE /* MainGitLabView.swift in Sources */, 8A7935E22A5583E400F8FB6C /* MergeTrainIcon.swift in Sources */, 8A7935E32A5583E400F8FB6C /* CIProgressIcon.swift in Sources */, 8A7C0AE52CD2AFAC00E479CA /* NetworkInspector.swift in Sources */, @@ -1839,7 +1797,6 @@ 8A7935F62A5583E400F8FB6C /* GitLabCIJobsView.swift in Sources */, 8A7935F72A5583E400F8FB6C /* MergeRequestRowView.swift in Sources */, 8A7C0AD72CD292B300E479CA /* NetworkManagerGitHub.swift in Sources */, - 8A111F4C2CC1386600DEB0DD /* NetworkState.swift in Sources */, 8A7935F82A5583E400F8FB6C /* MenuBarButtonStyle.swift in Sources */, 8A7935F92A5583E400F8FB6C /* LaunchpadItem.swift in Sources */, 8ADD57122B8220D3001F8E8F /* ModelContainer.swift in Sources */, @@ -1861,15 +1818,12 @@ 8A4534492CD7EB0B0011D5B5 /* CachedAsyncImage+ImageCache.swift in Sources */, 8A45344A2CD7EB0B0011D5B5 /* gitlabISO8601DateFormatter.swift in Sources */, 8A45344B2CD7EB0B0011D5B5 /* NetworkEvent.swift in Sources */, - 8A45344C2CD7EB0B0011D5B5 /* NetworkInfo.swift in Sources */, 8A45344D2CD7EB0B0011D5B5 /* NetworkManagerGitLab.swift in Sources */, 8A45344E2CD7EB0B0011D5B5 /* NetworkManagerGitLab+repoLaunchPad.swift in Sources */, 8A45344F2CD7EB0B0011D5B5 /* NetworkManagerGitHub.swift in Sources */, 8A4534502CD7EB0B0011D5B5 /* NetworkReachability.swift in Sources */, - 8A4534512CD7EB0B0011D5B5 /* NetworkState.swift in Sources */, 8A4534522CD7EB0B0011D5B5 /* NoticeMessage.swift in Sources */, 8A4534532CD7EB0B0011D5B5 /* NoticeType.swift in Sources */, - 8A4534542CD7EB0B0011D5B5 /* NoticeState.swift in Sources */, 8A4534552CD7EB0B0011D5B5 /* NotificationManager.swift in Sources */, 8A4534562CD7EB0B0011D5B5 /* QueryType.swift in Sources */, 8A4534572CD7EB0B0011D5B5 /* PipelineStatus.swift in Sources */, @@ -1915,7 +1869,6 @@ 8AF80D9F2A55817400819B80 /* BaseNoticeItem.swift in Sources */, 8ABBD2062B7E2E70007C03E6 /* Array+Difference.swift in Sources */, 8AC0C20A2A5D3D720096772B /* AccessToken.swift in Sources */, - 8A111F542CC1388900DEB0DD /* NetworkInfo.swift in Sources */, 8AF80DB72A55817400819B80 /* MergeRequestLabelView.swift in Sources */, 8AE2741F2CC3D3A90059244E /* WidgetMRRowIcon.swift in Sources */, 8AF80DCF2A55817400819B80 /* LaunchpadItem.swift in Sources */, @@ -1956,10 +1909,8 @@ 8A7C0AD62CD292B300E479CA /* NetworkManagerGitHub.swift in Sources */, 8A0CDABB2A55932E0056B63F /* CIJobsNotificationView.swift in Sources */, 8AF80D602A55817400819B80 /* CICanceledIcon.swift in Sources */, - 8A2E61852A9766A6001B6EAE /* MainGitLabView.swift in Sources */, 8AF80D632A55817400819B80 /* CIFailedIcon.swift in Sources */, 8A55E3C82CD6C8B2005B4AA5 /* LaunchpadState.swift in Sources */, - 8AF80D572A55817400819B80 /* NoticeState.swift in Sources */, 8A9D878A2BD2736C00E2C0CD /* ProjectLink.swift in Sources */, 8AE274252CC3DA9D0059244E /* OverflowContentViewModifier.swift in Sources */, 8AF80DC32A55817400819B80 /* WebLink.swift in Sources */, @@ -1990,7 +1941,6 @@ 8AF80D542A55817400819B80 /* NoticeType.swift in Sources */, 8AE2743E2CC67A530059244E /* IfViewModifier.swift in Sources */, 8AFDAAC92A85112E001937AC /* GitProviderView.swift in Sources */, - 8A111F4A2CC1386600DEB0DD /* NetworkState.swift in Sources */, 8A7C0AFE2CD3657100E479CA /* UniversalMergeRequest.swift in Sources */, 8AF80D7E2A55817400819B80 /* ShareMergeRequestIcon.swift in Sources */, 8ADD57102B8220D3001F8E8F /* ModelContainer.swift in Sources */, @@ -2028,7 +1978,6 @@ 8AE8A6FD2B989EFA002B3C9E /* ApprovedReviewIcon.swift in Sources */, 8AE8A6FE2B989EFA002B3C9E /* CIStatusView.swift in Sources */, 8A111F3C2CC1203700DEB0DD /* DoubleLineMergeRequestSubRowView.swift in Sources */, - 8AE8A6FF2B989EFA002B3C9E /* MainGitLabView.swift in Sources */, 8AE8A7002B989EFA002B3C9E /* LaunchPadView.swift in Sources */, 8AE8A7012B989EFA002B3C9E /* AccessToken.swift in Sources */, 8AE8A7022B989EFA002B3C9E /* AccountSettingsView.swift in Sources */, @@ -2045,12 +1994,10 @@ 8AE8A7082B989EFA002B3C9E /* MenuBarButtonStyle.swift in Sources */, 8AE8A7092B989EFA002B3C9E /* CISkippedIcon.swift in Sources */, 8AE8A70A2B989EFA002B3C9E /* gitlabISO8601DateFormatter.swift in Sources */, - 8AE8A70B2B989EFA002B3C9E /* NoticeState.swift in Sources */, 8A111F472CC1384E00DEB0DD /* NetworkStateView.swift in Sources */, 8AE274202CC3D3A90059244E /* WidgetMRRowIcon.swift in Sources */, 8A9D878D2BD2736C00E2C0CD /* ProjectLink.swift in Sources */, 8ACBFD3B2CC270AC00A8753B /* PluralWidgetTitle.swift in Sources */, - 8A111F532CC1388900DEB0DD /* NetworkInfo.swift in Sources */, 8A33E9462CD3D4D100F2C148 /* OptionalType.swift in Sources */, 8AE8A70D2B989EFA002B3C9E /* CachedAsyncImage+ImageCache.swift in Sources */, 8AFAE1952BEB7C1C0030541E /* MergeRequestList.swift in Sources */, @@ -2112,7 +2059,6 @@ 8AE274242CC3DA9D0059244E /* OverflowContentViewModifier.swift in Sources */, 8AB969112BDBC0EB0078E5CD /* MediumMergeRequestWidgetInterface.swift in Sources */, 8AE8A7322B989EFA002B3C9E /* NeedsReviewIcon.swift in Sources */, - 8A111F4B2CC1386600DEB0DD /* NetworkState.swift in Sources */, 8AB969262BDBC2C00078E5CD /* MediumLaunchPadWidgetView.swift in Sources */, 8AE8A7332B989EFA002B3C9E /* NoticeListView.swift in Sources */, 8A111F222CC1161500DEB0DD /* String.swift in Sources */, diff --git a/GitLab/ExtraWindow.swift b/GitLab/ExtraWindow.swift index d4cb06e..b2b4387 100644 --- a/GitLab/ExtraWindow.swift +++ b/GitLab/ExtraWindow.swift @@ -8,12 +8,9 @@ import SwiftUI import SwiftData import BackgroundFetcher -import OSLog struct ExtraWindow: View { @Environment(\.openURL) private var openURL - @StateObject private var noticeState = NoticeState() - @StateObject private var networkState = NetworkState() @Query(sort: \UniversalMergeRequest.createdAt, order: .reverse) private var mergeRequests: [UniversalMergeRequest] @Query private var accounts: [Account] @Query(sort: \LaunchpadRepo.createdAt, order: .reverse) private var repos: [LaunchpadRepo] @@ -28,19 +25,6 @@ struct ExtraWindow: View { @Environment(\.appearsActive) private var appearsActive @State private var hasLoaded: Bool = false - let log = Logger(subsystem: "ExtraWindow", category: "ExtraBG") - - -// let backgroundUpdateTimer: NSBackgroundActivityScheduler = { -// let scheduler: NSBackgroundActivityScheduler = .init(identifier: "com.stefkors.GitLab.backgroundAutoUpdate") -// scheduler.repeats = true -// scheduler.interval = 3 -// scheduler.tolerance = 0 -// scheduler.qualityOfService = .userInteractive -// -// return scheduler -// }() - var body: some View { VStack { Divider() @@ -61,8 +45,6 @@ struct ExtraWindow: View { hasLoaded = true } }) - .environmentObject(self.noticeState) - .environmentObject(self.networkState) .onOpenURL { url in openURL(url) } @@ -77,43 +59,7 @@ struct ExtraWindow: View { .pickerStyle(.segmented) } } -// .onAppear { -// callXPC() - - - -// if let session = try? XPCSession(xpcService: serviceName) { -// let request = CalculationRequest(firstNumber: 23, secondNumber: 19) -// try? session.send(request) -// } -// } -// .onAppear { -// log.warning("Jenga 1 start backgroundUpdateTimer") -// // Start the background update scheduler when the app starts -// backgroundUpdateTimer.schedule -// { (completion: NSBackgroundActivityScheduler.CompletionHandler) in -// log.warning("Jenga 5 (background task success)") -// completion(NSBackgroundActivityScheduler.Result.finished) -// -// } -// } } - -// @MainActor -// func callXPC() { -// let serviceName = "com.stefkors.BackgroundFetcher" -// let connection = NSXPCConnection(serviceName: serviceName) -// connection.remoteObjectInterface = NSXPCInterface(with: BackgroundFetcherProtocol.self) -// connection.resume() -// -// let service = connection.remoteObjectProxyWithErrorHandler { error in -// print("Received error:", error) -// } as? BackgroundFetcherProtocol -// -// log.warning("Jenga before calc 32") -// -// service?.performCalculation(firstNumber: 12, secondNumber: 20) -// } } // A codable type that contains two numbers to add together. diff --git a/GitLab/GitLabApp.swift b/GitLab/GitLabApp.swift index 9b372b1..6503a47 100644 --- a/GitLab/GitLabApp.swift +++ b/GitLab/GitLabApp.swift @@ -25,39 +25,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { var service: BackgroundFetcherProtocol? = nil var connection: NSXPCConnection? = nil - // works here -// var timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { -// (_) in -// let log2 = Logger() -// log2.warning("Jenga 2 (simple timer)") -// } func applicationDidFinishLaunching(_ aNotification: Notification) { - print("hi from app delegate") - print("hi 1") -// timer.fire() -// activity.repeats = true -// activity.interval = 10 // seconds -//// activity.tolerance = 0 -//// activity.qualityOfService = .userInitiated -// print("hi 2") -// log.warning("Jenga 4") -//// activity.invalidate() -// activity.schedule { completion in -// // perform activity -// if self.activity.shouldDefer { -// self.log.warning("Jenga 5 (defered)") -// return completion(.deferred) -// } else { -// print("hi 3 from background activity") -// self.log.warning("Jenga 5 (success)") -// return completion(.finished) -// } -// } - -// activity.invalidate() - - let serviceName = "com.stefkors.BackgroundFetcher" connection = NSXPCConnection(serviceName: serviceName) connection?.remoteObjectInterface = NSXPCInterface(with: BackgroundFetcherProtocol.self) @@ -67,13 +36,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { print("Received error:", error) } as? BackgroundFetcherProtocol - callXPC() - } - - func callXPC() { - - - service?.performCalculation(firstNumber: 122, secondNumber: 20) + service?.startFetching() } } @@ -81,7 +44,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { struct GitLabApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate - private var sharedModelContainer: ModelContainer = .shared +// private var sharedModelContainer: ModelContainer = .shared @State private var receivedURL: URL? @State var searchText: String = "" @@ -95,21 +58,20 @@ struct GitLabApp: App { var body: some Scene { // TODO: close after opening link from widget Window("GitLab", id: "GitLab-Window") { - ExtraWindow() - .modelContainer(sharedModelContainer) + Text("hi") +// ExtraWindow() +// .modelContainer(sharedModelContainer) .navigationTitle("GitLab") .presentedWindowBackgroundStyle(.translucent) } .windowToolbarStyle(.unified(showsTitle: true)) .windowResizability(.contentMinSize) .windowIdealSize(.fitToContent) - .backgroundTask(.urlSession("com.stefkors.GitLab.updatecheck2")) { value in - log.warning("Jenga 5 (success)") - } MenuBarExtra(content: { - MainGitLabView() - .modelContainer(sharedModelContainer) +// Text("menu") + UserInterface() + .modelContainer(.shared) .frame(width: 600) .onOpenURL { url in openURL(url) @@ -124,9 +86,9 @@ struct GitLabApp: App { .menuBarExtraStyle(.window) .windowResizability(.contentSize) - Settings { - SettingsView() - .modelContainer(sharedModelContainer) - } +// Settings { +// SettingsView() +// .modelContainer(sharedModelContainer) +// } } } diff --git a/Shared/UserInterface/Extensions/ModelContainer.swift b/Shared/UserInterface/Extensions/ModelContainer.swift index a01521d..c184d51 100644 --- a/Shared/UserInterface/Extensions/ModelContainer.swift +++ b/Shared/UserInterface/Extensions/ModelContainer.swift @@ -10,7 +10,7 @@ import SwiftData extension ModelContainer { static var previews: ModelContainer = { - let schema = Schema([Account.self, UniversalMergeRequest.self, LaunchpadRepo.self]) + let schema = Schema([Account.self, UniversalMergeRequest.self, LaunchpadRepo.self, NetworkEvent.self]) let modelConfiguration = ModelConfiguration("MergeRequests", schema: schema, isStoredInMemoryOnly: false) do { @@ -21,7 +21,7 @@ extension ModelContainer { }() static var shared: ModelContainer = { - let schema = Schema([Account.self, UniversalMergeRequest.self, LaunchpadRepo.self]) + let schema = Schema([Account.self, UniversalMergeRequest.self, LaunchpadRepo.self, NetworkEvent.self]) let modelConfiguration = ModelConfiguration("MergeRequests", schema: schema, isStoredInMemoryOnly: false) do { diff --git a/Shared/UserInterface/MainContentView.swift b/Shared/UserInterface/MainContentView.swift index cb93165..7a3065c 100644 --- a/Shared/UserInterface/MainContentView.swift +++ b/Shared/UserInterface/MainContentView.swift @@ -48,7 +48,7 @@ struct MainContentView: View { .scrollBounceBehavior(.basedOnSize) } - Spacer() +// Spacer() LastUpdateMessageView() } .frame(maxHeight: .infinity, alignment: .top) @@ -65,5 +65,4 @@ struct MainContentView: View { accounts: [.preview], selectedView: .constant(.authoredMergeRequests) ) - .environmentObject(NoticeState()) } diff --git a/Shared/UserInterface/MainGitLabView.swift b/Shared/UserInterface/MainGitLabView.swift deleted file mode 100644 index 98ed296..0000000 --- a/Shared/UserInterface/MainGitLabView.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// MainGitLabView.swift -// GitLab -// -// Created by Stef Kors on 24/08/2023. -// - -import SwiftUI -import SwiftData - -struct MainGitLabView: View { - // Non-Persisted state objects - @StateObject private var noticeState = NoticeState() - @StateObject private var networkState = NetworkState() - - var body: some View { - UserInterface() - .environmentObject(self.noticeState) - .environmentObject(self.networkState) - } -} - -#Preview { - MainGitLabView() -} diff --git a/Shared/UserInterface/Models/NetworkInfo.swift b/Shared/UserInterface/Models/NetworkInfo.swift deleted file mode 100644 index 60790fc..0000000 --- a/Shared/UserInterface/Models/NetworkInfo.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// NetworkInfo.swift -// GitLab -// -// Created by Stef Kors on 17/10/2024. -// - -import Foundation -import Get - -struct NetworkInfo: Identifiable, Equatable { - let label: String - let account: Account - let method: HTTPMethod - let timestamp: Date = .now - let id: UUID = UUID() -} diff --git a/Shared/UserInterface/Models/NetworkManagerGitHub.swift b/Shared/UserInterface/Models/NetworkManagerGitHub.swift index d05bd65..bc28404 100644 --- a/Shared/UserInterface/Models/NetworkManagerGitHub.swift +++ b/Shared/UserInterface/Models/NetworkManagerGitHub.swift @@ -44,7 +44,6 @@ class NetworkManagerGitHub { // https://api.github.com/graphql func fetchAuthoredPullRequests(with account: Account) async throws -> [GitHub.PullRequestsNode]? { let client = APIClient(baseURL: URL(string: account.instance)) - print("doing request to \(account.instance) with token: \(account.token)") let response: GitHub.Query = try await client.send(authoredMergeRequestsReq(with: account)).value let result = response.authoredMergeRequests print("recieved \(result.count) pull requests") diff --git a/Shared/UserInterface/Models/NetworkState.swift b/Shared/UserInterface/Models/NetworkState.swift deleted file mode 100644 index 69f60d7..0000000 --- a/Shared/UserInterface/Models/NetworkState.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// NetworkState.swift -// GitLab -// -// Created by Stef Kors on 17/10/2024. -// - -import SwiftUI - -class NetworkState: ObservableObject { - @Published var events: [NetworkEvent] = [] - @Published var record: Bool = false - - func add(_ event: NetworkEvent) { - if record { - events.append(event) - } - } - - func update(_ newEvent: NetworkEvent) { - if record { - let newEvents = events.map { event in - if event.identifier == newEvent.identifier { - return newEvent - } - return event - } - - events = newEvents - } - } -} diff --git a/Shared/UserInterface/Models/NoticeState/NoticeState.swift b/Shared/UserInterface/Models/NoticeState/NoticeState.swift deleted file mode 100644 index 320804f..0000000 --- a/Shared/UserInterface/Models/NoticeState/NoticeState.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// File.swift -// -// -// Created by Stef Kors on 29/07/2022. -// - -import Foundation - -class NoticeState: ObservableObject { - @Published var notices: [NoticeMessage] = [] { - didSet { - // limit notice list - if notices.count > 10 { - notices.removeFirst(1) - } - } - } - - func addNotice(notice: NoticeMessage) { - // If the new notice is basically the same as the last notice, Don't add new notice - for existingNotice in notices { - if existingNotice.type == notice.type, - existingNotice.statusCode == notice.statusCode, - existingNotice.label == notice.label { - - // skip adding duplicate notice even if branch notice is dismissed - if notice.type == .branch { - return - } - - if existingNotice.dismissed == false { - print("skipping adding duplicate notice") - return - } - } - } - - notices.append(notice) - } - - func dismissNotice(id: UUID) { - let index = notices.lastIndex(where: { notice in - notice.id == id - }) - - if let index = index { - notices[index].dismiss() - } - } - - func clearNetworkNotices() { - for (index, notice) in notices.enumerated() { - if notice.type == .network { - notices[index].dismiss() - } - } - } - - func clearAllNotices() { - for index in notices.indices { - notices[index].dismiss() - } - } -} diff --git a/Shared/UserInterface/Models/StructsGitLab.swift b/Shared/UserInterface/Models/StructsGitLab.swift index ce26896..9e42962 100644 --- a/Shared/UserInterface/Models/StructsGitLab.swift +++ b/Shared/UserInterface/Models/StructsGitLab.swift @@ -1,6 +1,15 @@ import Foundation import SwiftData +@propertyWrapper +struct Preview { +#if DEBUG + let wrappedValue: Value +#else + // omitted in build +#endif +} + class GitLab { // MARK: - GitLabQuery struct GitLabQuery: Codable, Equatable, Hashable { @@ -539,6 +548,7 @@ class GitLab { let label: String? let group: String? let tooltip: String? + @available(iOS 17, *) let icon: String? init(id: String?, detailsPath: String?, text: String?, label: String?, group: String?, tooltip: String?, icon: String?) { diff --git a/Shared/UserInterface/SwiftData/LaunchpadState.swift b/Shared/UserInterface/SwiftData/LaunchpadState.swift index 61a8fc7..6fca959 100644 --- a/Shared/UserInterface/SwiftData/LaunchpadState.swift +++ b/Shared/UserInterface/SwiftData/LaunchpadState.swift @@ -1,6 +1,6 @@ // // RepoLaunchpadState.swift -// +// // // Created by Stef Kors on 16/09/2022. // @@ -54,11 +54,11 @@ import SwiftData self.image = try container.decodeIfPresent(Data.self, forKey: LaunchpadRepo.CodingKeys.image) self.imageURL = try container.decodeIfPresent(URL.self, forKey: LaunchpadRepo.CodingKeys.imageURL) self.group = try container.decode(String.self, forKey: LaunchpadRepo.CodingKeys.group) -// if let url = try container.decodeURLWithEncodingIfPresent(forKey: LaunchpadRepo.CodingKeys.url) { -// self.url = url -// } else { -// self.url = try container.decode(URL.self, forKey: LaunchpadRepo.CodingKeys.url) -// } + // if let url = try container.decodeURLWithEncodingIfPresent(forKey: LaunchpadRepo.CodingKeys.url) { + // self.url = url + // } else { + // self.url = try container.decode(URL.self, forKey: LaunchpadRepo.CodingKeys.url) + // } self.url = try container.decode(URL.self, forKey: LaunchpadRepo.CodingKeys.url) self.createdAt = try container.decode(Date.self, forKey: LaunchpadRepo.CodingKeys.createdAt) self.provider = try container.decodeIfPresent(GitProvider.self, forKey: LaunchpadRepo.CodingKeys.provider) diff --git a/Shared/UserInterface/Models/NetworkEvent.swift b/Shared/UserInterface/SwiftData/NetworkEvent.swift similarity index 55% rename from Shared/UserInterface/Models/NetworkEvent.swift rename to Shared/UserInterface/SwiftData/NetworkEvent.swift index e1419de..c78b620 100644 --- a/Shared/UserInterface/Models/NetworkEvent.swift +++ b/Shared/UserInterface/SwiftData/NetworkEvent.swift @@ -6,7 +6,10 @@ // import Foundation +import SwiftData +import Get +@Model class NetworkEvent: Identifiable, Equatable { static func == (lhs: NetworkEvent, rhs: NetworkEvent) -> Bool { lhs.identifier == rhs.identifier && @@ -15,11 +18,11 @@ class NetworkEvent: Identifiable, Equatable { lhs.response == rhs.response } - let info: NetworkInfo + var info: NetworkInfo var status: Int? var response: String? - let timestamp: Date = .now - let identifier: UUID = UUID() + var timestamp: Date = Date.now + var identifier: UUID = UUID() var id: String { "\(identifier)-\(status?.description ?? "nil")-\(timestamp)" } @@ -30,3 +33,24 @@ class NetworkEvent: Identifiable, Equatable { self.response = response } } + + +@Model class NetworkInfo { + var label: String + var account: Account + + private var storedMethod: String + var method: HTTPMethod { + HTTPMethod(rawValue: storedMethod) + } + + var timestamp: Date = Date.now + var id: UUID = UUID() + + init(label: String, account: Account, method: HTTPMethod) { + self.label = label + self.account = account + self.storedMethod = method.rawValue + } +} + diff --git a/Shared/UserInterface/Models/NoticeState/NoticeMessage.swift b/Shared/UserInterface/SwiftData/NoticeMessage.swift similarity index 92% rename from Shared/UserInterface/Models/NoticeState/NoticeMessage.swift rename to Shared/UserInterface/SwiftData/NoticeMessage.swift index f571cfe..d5ce376 100644 --- a/Shared/UserInterface/Models/NoticeState/NoticeMessage.swift +++ b/Shared/UserInterface/SwiftData/NoticeMessage.swift @@ -7,9 +7,9 @@ import Foundation import SwiftUI +import SwiftData -struct NoticeMessage: Codable, Equatable, Hashable, Identifiable { - var id: UUID +@Model class NoticeMessage { var label: String var statusCode: Int? var webLink: URL? @@ -19,7 +19,6 @@ struct NoticeMessage: Codable, Equatable, Hashable, Identifiable { var createdAt: Date init( - id: UUID = UUID(), label: String, statusCode: Int? = nil, webLink: URL? = nil, @@ -28,7 +27,6 @@ struct NoticeMessage: Codable, Equatable, Hashable, Identifiable { branchRef: String? = nil, createdAt: Date = .now ) { - self.id = id self.label = label self.statusCode = statusCode self.webLink = webLink @@ -51,7 +49,7 @@ struct NoticeMessage: Codable, Equatable, Hashable, Identifiable { } } - mutating func dismiss() { + func dismiss() { dismissed = true } } diff --git a/Shared/UserInterface/Models/NoticeState/NoticeType.swift b/Shared/UserInterface/SwiftData/NoticeType.swift similarity index 100% rename from Shared/UserInterface/Models/NoticeState/NoticeType.swift rename to Shared/UserInterface/SwiftData/NoticeType.swift diff --git a/Shared/UserInterface/UserInterface.swift b/Shared/UserInterface/UserInterface.swift index c0d516a..22d4cfc 100644 --- a/Shared/UserInterface/UserInterface.swift +++ b/Shared/UserInterface/UserInterface.swift @@ -33,10 +33,6 @@ struct UserInterface: View { @State private var selectedView: QueryType = .authoredMergeRequests @State private var timelineDate: Date = .now -// private let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect() - - @EnvironmentObject private var noticeState: NoticeState - @EnvironmentObject private var networkState: NetworkState private var filteredMergeRequests: [UniversalMergeRequest] { mergeRequests.filter { $0.type == selectedView } @@ -75,13 +71,6 @@ struct UserInterface: View { .frame(maxHeight: .infinity, alignment: .top) .background(.regularMaterial) - .onChange(of: selectedView) { _, newValue in - if newValue == .networkDebug { - networkState.record = true - } else { - networkState.record = false - } - } // .task(id: "once") { // Task { // await fetchReviewRequestedMRs() @@ -100,282 +89,12 @@ struct UserInterface: View { // } // } } - - /// TODO: Cleanup and move both into the same function - @MainActor - private func fetchReviewRequestedMRs() async { - for account in accounts { - if account.provider == .GitLab { - let info = NetworkInfo(label: "Fetch Review Requested Merge Requests", account: account, method: .get) - let results = await wrapRequest(info: info) { - try await NetworkManagerGitLab.shared.fetchReviewRequestedMergeRequests(with: account) - } - - if let results { - removeAndInsertUniversal( - .reviewRequestedMergeRequests, - account: account, - results: results - ) - } - } - } - } - - @MainActor - private func fetchAuthoredMRs() async { - for account in accounts { - if account.provider == .GitLab { - let info = NetworkInfo( - label: "Fetch Authored Merge Requests", - account: account, - method: .get - ) - let results = await wrapRequest(info: info) { - try await NetworkManagerGitLab.shared.fetchAuthoredMergeRequests(with: account) - } - - if let results { - removeAndInsertUniversal( - .authoredMergeRequests, - account: account, - results: results - ) - } - } else { - let info = NetworkInfo( - label: "Fetch Authored Pull Requests", - account: account, - method: .get - ) - let results = await wrapRequest(info: info) { - try await NetworkManagerGitHub.shared.fetchAuthoredPullRequests(with: account) - } - - if let results { - removeAndInsertUniversal( - .authoredMergeRequests, - account: account, - results: results - ) - } - } - } - } - - private func removeAndInsertUniversal(_ type: QueryType, account: Account, results: [GitLab.MergeRequest]) { - // Map results to universal request - let requests = results.map { result in - return UniversalMergeRequest( - request: result, - account: account, - provider: .GitLab, - type: type - ) - } - // Call universal remove and insert - removeAndInsertUniversal(type, account: account, requests: requests) - } - - private func removeAndInsertUniversal(_ type: QueryType, account: Account, results: [GitHub.PullRequestsNode]) { - // Map results to universal request - let requests = results.map { result in - return UniversalMergeRequest( - request: result, - account: account, - provider: .GitHub, - type: type - ) - } - // Call universal remove and insert - removeAndInsertUniversal(type, account: account, requests: requests) - } - - private func removeAndInsertUniversal(_ type: QueryType, account: Account, requests: [UniversalMergeRequest]) { - // Get array of ids of current of type - let existing = mergeRequests.filter({ $0.type == type }).map({ $0.requestID }) - // Get arary of new of current of type - let updated = requests.map { $0.requestID } - // Compute difference - let difference = existing.difference(from: updated) - // Delete existing - for pullRequest in account.requests { - if difference.contains(pullRequest.requestID) { - print("removing \(pullRequest.requestID)") - modelContext.delete(pullRequest) - try? modelContext.save() - } - } - - for request in requests { - // update values - if let existingMR = mergeRequests.first(where: { request.requestID == $0.requestID }) { - existingMR.mergeRequest = request.mergeRequest - existingMR.pullRequest = request.pullRequest - } else { - // if not insert - modelContext.insert(request) - } - - // If no matching launchpad repo, insert a new one - let launchPadItem = repos.first { repo in - repo.url == request.repoUrl - } - - if let launchPadItem { - if launchPadItem.hasUpdatedSinceLaunch == false { - if let name = request.repoName { - launchPadItem.name = name - } - if let owner = request.repoOwner { - launchPadItem.group = owner - } - if let url = request.repoUrl { - launchPadItem.url = url - } - if let imageURL = request.repoImage { - launchPadItem.imageURL = imageURL - } - launchPadItem.provider = request.provider - launchPadItem.hasUpdatedSinceLaunch = true - } - } else if let name = request.repoName, - let owner = request.repoOwner, - let url = request.repoUrl { - - let repo = LaunchpadRepo( - id: request.repoId ?? UUID().uuidString, - name: name, - imageURL: request.repoImage, - group: owner, - url: url, - provider: request.provider - ) - - modelContext.insert(repo) - } - } - } - - // TDOO: fix this mess with split gitlab (below) and github (above) logic - @MainActor - private func fetchRepos() async { - for account in accounts { - if account.provider == .GitLab { - let ids = Array(Set(mergeRequests.compactMap { request in - if request.provider == .GitLab { - return request.mergeRequest?.targetProject?.id.split(separator: "/").last - } else { - return nil - } - }.compactMap({ Int($0) }))) - - let info = NetworkInfo(label: "Fetch Projects \(ids)", account: account, method: .get) - let results = await wrapRequest(info: info) { - try await NetworkManagerGitLab.shared.fetchProjects(with: account, ids: ids) - } - - if let results { - for result in results { - if let url = result.webURL { - // If no matching launchpad repo, insert a new one - let launchPadItem = repos.first { repo in - repo.url == url - } - - if let launchPadItem { - if launchPadItem.hasUpdatedSinceLaunch == false { - if let name = result.name { - launchPadItem.name = name - } - if let owner = result.group?.fullName ?? result.namespace?.fullName { - launchPadItem.group = owner - } - launchPadItem.url = url - if let image = await NetworkManagerGitLab.shared.getProjectImage(with: account, result) { - launchPadItem.image = image - } - launchPadItem.provider = account.provider - launchPadItem.hasUpdatedSinceLaunch = true - } - } else { - let repo = LaunchpadRepo( - id: result.id, - name: result.name ?? "", - image: await NetworkManagerGitLab.shared.getProjectImage(with: account, result), - group: result.group?.fullName ?? result.namespace?.fullName ?? "", - url: url, - hasUpdatedSinceLaunch: true - ) - modelContext.insert(repo) - } - } - } - try? modelContext.save() - } - } - } - } - - @MainActor - private func branchPushes() async { - for account in accounts { - if account.provider == .GitLab { - let info = NetworkInfo(label: "Branch Push", account: account, method: .get) - let notice = await wrapRequest(info: info) { - try await NetworkManagerGitLab.shared.fetchLatestBranchPush(with: account, repos: repos) - } - - if let notice { - if notice.type == .branch, let branch = notice.branchRef { - - let matchedMR = filteredMergeRequests.first { request in - return request.sourceBranch == branch - } - - let alreadyHasMR = matchedMR != nil - - if alreadyHasMR || !notice.createdAt.isWithinLastHours(1) { - return - } - } - noticeState.addNotice(notice: notice) - } - } - } - } - - @MainActor - private func wrapRequest(info: NetworkInfo, do request: () async throws -> T?) async -> T? { - let event = NetworkEvent(info: info, status: nil, response: nil) - networkState.add(event) - do { - let result = try await request() - event.status = 200 - event.response = result.debugDescription - networkState.update(event) - return result - } catch APIError.unacceptableStatusCode(let statusCode) { - event.status = statusCode - event.response = "Unacceptable Status Code: \(statusCode)" - networkState.update(event) - } catch let error { - event.status = 0 - event.response = error.localizedDescription - networkState.update(event) - } - - return nil - - } } #Preview { HStack(alignment: .top) { UserInterface() .modelContainer(.previews) - .environmentObject(NoticeState()) - .environmentObject(NetworkState()) .frame(maxHeight: .infinity, alignment: .top) .scenePadding() } diff --git a/Shared/UserInterface/Views/LaunchpadImage.swift b/Shared/UserInterface/Views/LaunchpadImage.swift index fe17f40..942c3f8 100644 --- a/Shared/UserInterface/Views/LaunchpadImage.swift +++ b/Shared/UserInterface/Views/LaunchpadImage.swift @@ -12,6 +12,7 @@ struct LaunchpadImage: View { let repo: LaunchpadRepo private var url: URL? { + print("url? \(repo.name)") if let image = repo.image { return URL(string: "data:image/png;base64," + image.base64EncodedString()) } else if let imageURL = repo.imageURL { diff --git a/Shared/UserInterface/Views/NetworkStateView.swift b/Shared/UserInterface/Views/NetworkStateView.swift index c4b7a05..967ecea 100644 --- a/Shared/UserInterface/Views/NetworkStateView.swift +++ b/Shared/UserInterface/Views/NetworkStateView.swift @@ -6,18 +6,14 @@ // import SwiftUI +import SwiftData struct NetworkStateView: View { - @EnvironmentObject private var networkState: NetworkState - - @State private var sortOrder = [KeyPathComparator(\NetworkEvent.timestamp, - order: .reverse)] - - // @SceneStorage("NetworkEventTableConfig") - // @State private var columnCustomization: TableColumnCustomization - - // var tableData: [NetworkEvent] { networkState.events.sorted(using: sortOrder) } + @State private var sortOrder = [ + KeyPathComparator(\NetworkEvent.timestamp,order: .reverse) + ] + @Query private var eventsData: [NetworkEvent] @State private var events: [NetworkEvent] = [] @State private var selectedEvents = Set() @@ -27,7 +23,7 @@ struct NetworkStateView: View { VStack(alignment: .leading) { HStack { Image(systemName: "circle.fill") - .foregroundStyle(networkState.record ? .red : .secondary) + .foregroundStyle(.red) Text("Network Events") .font(.headline) } @@ -70,8 +66,8 @@ struct NetworkStateView: View { } } } - .onChange(of: networkState.events) { _, newEvents in - events = newEvents.sorted(using: sortOrder) + .onChange(of: eventsData) { _, newEvents in + events = eventsData.sorted(using: sortOrder) } .onChange(of: sortOrder) { _, sortOrder in events.sort(using: sortOrder) diff --git a/Shared/UserInterface/Views/NoticeViews/BaseNoticeItem.swift b/Shared/UserInterface/Views/NoticeViews/BaseNoticeItem.swift index e0020ef..bb6e489 100644 --- a/Shared/UserInterface/Views/NoticeViews/BaseNoticeItem.swift +++ b/Shared/UserInterface/Views/NoticeViews/BaseNoticeItem.swift @@ -6,18 +6,15 @@ // import SwiftUI +import SwiftData struct BaseNoticeItem: View { - @EnvironmentObject private var noticeState: NoticeState + @Environment(\.modelContext) private var modelContext @Environment(\.openURL) private var openURL @State private var isHovering: Bool = false var notice: NoticeMessage - init(notice: NoticeMessage) { - self.notice = notice - } - var body: some View { HStack(alignment: .center, spacing: 0) { if let statusCode = notice.statusCode { @@ -51,7 +48,7 @@ struct BaseNoticeItem: View { openURL(url) DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { withAnimation(.interpolatingSpring(stiffness: 500, damping: 15)) { - noticeState.dismissNotice(id: notice.id) + modelContext.delete(notice) } } }, label: { @@ -68,7 +65,7 @@ struct BaseNoticeItem: View { isHovering = hoverState } .onTapGesture { - noticeState.dismissNotice(id: notice.id) + modelContext.delete(notice) } } .animation(.spring(), value: isHovering) @@ -122,7 +119,7 @@ struct NoticeTypeBackground: ViewModifier { } struct BaseNoticeItem_Previews: PreviewProvider { - static let noticeState = NoticeState() + static var previews: some View { VStack(spacing: 25) { BaseNoticeItem(notice: .previewInformationNotice) @@ -159,7 +156,6 @@ struct BaseNoticeItem_Previews: PreviewProvider { } .padding() .frame(height: 400) - // .environmentObject(self.networkManager) - .environmentObject(self.noticeState) + .modelContainer(.previews) } } diff --git a/Shared/UserInterface/Views/NoticeViews/NoticeListView.swift b/Shared/UserInterface/Views/NoticeViews/NoticeListView.swift index d2c86b2..dffc4f7 100644 --- a/Shared/UserInterface/Views/NoticeViews/NoticeListView.swift +++ b/Shared/UserInterface/Views/NoticeViews/NoticeListView.swift @@ -6,19 +6,20 @@ // import SwiftUI +import SwiftData struct NoticeListView: View { - @EnvironmentObject var noticeState: NoticeState + @Query private var notices: [NoticeMessage] var body: some View { - if !noticeState.notices.isEmpty { + if !notices.isEmpty { VStack { - ForEach(noticeState.notices.filter({ $0.dismissed == false }), id: \.id) { notice in + ForEach(notices.filter({ $0.dismissed == false }), id: \.id) { notice in BaseNoticeItem(notice: notice) .id(notice.id) } } - .animation(.spring(), value: noticeState.notices) + .animation(.spring(), value: notices) } } }