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..dc44f79 --- /dev/null +++ b/BackgroundFetcher/BackgroundFetcher.swift @@ -0,0 +1,470 @@ +// +// BackgroundFetcher.swift +// BackgroundFetcher +// +// Created by Stef Kors on 03/11/2024. +// + +import Foundation +import OSLog +import SwiftData +import Get + +/// ```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 } +// +// public init(container: ModelContainer) { +// self.modelContainer = container +// let context = ModelContext(modelContainer) +// modelExecutor = DefaultSerialModelExecutor(modelContext: context) +// } +// +// 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) + + func startFetching() { + log.info("startFetching is called") + activity.invalidate() + activity.repeats = true + activity.interval = 20 // seconds + activity.tolerance = 1 + activity.qualityOfService = .userInteractive + +// 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)") + } + } + + 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) + } + } + + @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 new file mode 100644 index 0000000..452ed0e --- /dev/null +++ b/BackgroundFetcher/BackgroundFetcherProtocol.swift @@ -0,0 +1,33 @@ +// +// 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 startFetching() +} + +/* + 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..4ff73c4 --- /dev/null +++ b/BackgroundFetcher/main.swift @@ -0,0 +1,40 @@ +// +// main.swift +// BackgroundFetcher +// +// Created by Stef Kors on 03/11/2024. +// + +import Foundation +import OSLog + +class ServiceDelegate: NSObject, NSXPCListenerDelegate { + + /// 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() + + // 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 + } +} + +// 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 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 4acb5ea..e155479 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 */; }; @@ -49,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 */; }; @@ -80,6 +65,38 @@ 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 */; }; + 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 */; }; + 8A4534522CD7EB0B0011D5B5 /* NoticeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF12A55817400819B80 /* NoticeMessage.swift */; }; + 8A4534532CD7EB0B0011D5B5 /* NoticeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF80CF22A55817400819B80 /* NoticeType.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 */; }; @@ -100,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 */; }; @@ -283,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 */; }; @@ -294,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 */; }; @@ -361,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 */; }; @@ -486,6 +498,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 +529,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 +555,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 = ""; }; @@ -536,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 = ""; }; @@ -581,6 +607,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 = ""; }; @@ -657,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 = ""; }; @@ -705,6 +731,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 +789,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 +932,7 @@ 8AF80BE92A5580E700819B80 /* Shared */, 8A07E5672890492C0042EACB /* README.md */, 8AE8A6EB2B989E9A002B3C9E /* DesktopWidgetTool */, + 8A4533BD2CD7C64A0011D5B5 /* BackgroundFetcher */, 8AC4B4112865C1480054C601 /* Frameworks */, 8A5FC0F826EFD08E004136AB /* GitLab */, 8A31CB3B2866334000C94AC1 /* GitLab iOS */, @@ -900,6 +957,7 @@ 8A31CB532866334100C94AC1 /* GitLab iOSUITests.xctest */, 8A36AF102869B4110008B949 /* NotificationContent.appex */, 8AE8A6E82B989E9A002B3C9E /* DesktopWidgetToolExtension.appex */, + 8A4533BC2CD7C64A0011D5B5 /* BackgroundFetcher.xpc */, ); name = Products; sourceTree = ""; @@ -946,7 +1004,6 @@ 8A63DE6F2A83A1FC002DD636 /* Extensions */ = { isa = PBXGroup; children = ( - 8A03BF212CD6D29100ACE1EC /* IfView.swift */, 8A33E9452CD3D4D100F2C148 /* OptionalType.swift */, 8A7C0AF22CD3650000E479CA /* Date.swift */, 8A7C0AE82CD363B700E479CA /* Collection.swift */, @@ -1040,7 +1097,6 @@ 8AF80CF42A55817400819B80 /* Icons */, 8AF80D062A55817400819B80 /* UserInterface.swift */, 8AE274272CC3DD060059244E /* MainContentView.swift */, - 8A2E61842A9766A6001B6EAE /* MainGitLabView.swift */, 8AF80D072A55817400819B80 /* Views */, ); path = UserInterface; @@ -1052,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 */, @@ -1069,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 = ( @@ -1182,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 = ""; @@ -1280,6 +1325,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 +1357,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 +1441,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1530; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1530; TargetAttributes = { 8A31CB392866334000C94AC1 = { @@ -1387,6 +1458,9 @@ 8A36AF0F2869B4110008B949 = { CreatedOnToolsVersion = 13.4.1; }; + 8A4533BB2CD7C64A0011D5B5 = { + CreatedOnToolsVersion = 16.0; + }; 8A5FC0F526EFD08E004136AB = { CreatedOnToolsVersion = 12.5.1; }; @@ -1429,6 +1503,7 @@ 8A31CB522866334100C94AC1 /* GitLab iOSUITests */, 8A36AF0F2869B4110008B949 /* NotificationContent */, 8AE8A6E72B989E9A002B3C9E /* DesktopWidgetToolExtension */, + 8A4533BB2CD7C64A0011D5B5 /* BackgroundFetcher */, ); }; /* End PBXProject section */ @@ -1440,7 +1515,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 +1537,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 +1577,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8A03BF232CD6D29100ACE1EC /* IfView.swift in Resources */, 8AE8A6F12B989E9B002B3C9E /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1566,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 */, @@ -1596,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 */, @@ -1604,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 */, @@ -1646,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 */, @@ -1655,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 */, @@ -1692,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 */, @@ -1726,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 */, @@ -1740,6 +1810,42 @@ ); 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 */, + 8A45344D2CD7EB0B0011D5B5 /* NetworkManagerGitLab.swift in Sources */, + 8A45344E2CD7EB0B0011D5B5 /* NetworkManagerGitLab+repoLaunchPad.swift in Sources */, + 8A45344F2CD7EB0B0011D5B5 /* NetworkManagerGitHub.swift in Sources */, + 8A4534502CD7EB0B0011D5B5 /* NetworkReachability.swift in Sources */, + 8A4534522CD7EB0B0011D5B5 /* NoticeMessage.swift in Sources */, + 8A4534532CD7EB0B0011D5B5 /* NoticeType.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; @@ -1763,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 */, @@ -1804,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 */, @@ -1838,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 */, @@ -1876,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 */, @@ -1893,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 */, @@ -1960,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 */, @@ -2004,6 +2102,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 +2367,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 +2825,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 +2909,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..b2b4387 100644 --- a/GitLab/ExtraWindow.swift +++ b/GitLab/ExtraWindow.swift @@ -7,11 +7,10 @@ import SwiftUI import SwiftData +import BackgroundFetcher 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] @@ -46,8 +45,6 @@ struct ExtraWindow: View { hasLoaded = true } }) - .environmentObject(self.noticeState) - .environmentObject(self.networkState) .onOpenURL { url in openURL(url) } @@ -64,3 +61,13 @@ struct ExtraWindow: View { } } } + +// 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..6503a47 100644 --- a/GitLab/GitLabApp.swift +++ b/GitLab/GitLabApp.swift @@ -8,24 +8,59 @@ 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 + + func applicationDidFinishLaunching(_ aNotification: Notification) { + 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 + + service?.startFetching() + } +} + @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 Window("GitLab", id: "GitLab-Window") { - ExtraWindow() - .modelContainer(sharedModelContainer) + Text("hi") +// ExtraWindow() +// .modelContainer(sharedModelContainer) .navigationTitle("GitLab") .presentedWindowBackgroundStyle(.translucent) } @@ -34,8 +69,9 @@ struct GitLabApp: App { .windowIdealSize(.fitToContent) MenuBarExtra(content: { - MainGitLabView() - .modelContainer(sharedModelContainer) +// Text("menu") + UserInterface() + .modelContainer(.shared) .frame(width: 600) .onOpenURL { url in openURL(url) @@ -50,9 +86,9 @@ struct GitLabApp: App { .menuBarExtraStyle(.window) .windowResizability(.contentSize) - Settings { - SettingsView() - .modelContainer(sharedModelContainer) - } +// Settings { +// SettingsView() +// .modelContainer(sharedModelContainer) +// } } } 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/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 d06a0c8..7a3065c 100644 --- a/Shared/UserInterface/MainContentView.swift +++ b/Shared/UserInterface/MainContentView.swift @@ -48,10 +48,13 @@ struct MainContentView: View { .scrollBounceBehavior(.basedOnSize) } - Spacer() +// Spacer() LastUpdateMessageView() } .frame(maxHeight: .infinity, alignment: .top) + .onChange(of: repos) { oldValue, newValue in + print("updated repos \(repos.count.description)") + } } } @@ -62,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 bdf6a15..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) @@ -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/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 9e5e14f..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,298 +71,23 @@ 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() - 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 - @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 - +// .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() +// } +// } } } @@ -374,8 +95,6 @@ struct UserInterface: View { 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 84e7b56..942c3f8 100644 --- a/Shared/UserInterface/Views/LaunchpadImage.swift +++ b/Shared/UserInterface/Views/LaunchpadImage.swift @@ -5,12 +5,14 @@ // Created by Stef Kors on 17/10/2024. // +import Foundation import SwiftUI 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 { @@ -22,6 +24,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 +45,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 +83,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() } 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) } } } 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(