Skip to content

Commit

Permalink
Persist Quick Launch entries with SwiftData (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
lfroms authored Nov 26, 2024
1 parent 78bc9f3 commit 6259f23
Show file tree
Hide file tree
Showing 26 changed files with 664 additions and 531 deletions.
48 changes: 28 additions & 20 deletions Tophat.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Tophat/Models/LaunchContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

struct LaunchContext {
let appName: String?
let pinnedApplicationId: PinnedApplication.ID?
let quickLaunchEntryID: QuickLaunchEntry.ID?

init(appName: String? = nil, pinnedApplicationId: PinnedApplication.ID? = nil) {
init(appName: String? = nil, quickLaunchEntryID: QuickLaunchEntry.ID? = nil) {
self.appName = appName
self.pinnedApplicationId = pinnedApplicationId
self.quickLaunchEntryID = quickLaunchEntryID
}
}
49 changes: 0 additions & 49 deletions Tophat/Models/PinnedApplication.swift

This file was deleted.

42 changes: 42 additions & 0 deletions Tophat/Models/QuickLaunchEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// QuickLaunchEntry.swift
// Tophat
//
// Created by Lukas Romsicki on 2024-11-22.
// Copyright © 2024 Shopify. All rights reserved.
//

import Foundation
import TophatFoundation
import SwiftData

@Model
final class QuickLaunchEntry: Identifiable, Hashable {
typealias Source = InstallRecipe

@Attribute(.unique)
var id: String

var name: String

var iconURL: URL?

@Relationship(deleteRule: .cascade, minimumModelCount: 1)
var sources: [QuickLaunchEntrySource]

var order: Int = 0

init(id: String? = nil, name: String, iconURL: URL? = nil, sources: [QuickLaunchEntrySource], order: Int = 0) {
self.id = id ?? UUID().uuidString
self.name = name
self.iconURL = iconURL
self.sources = sources
self.order = order
}
}

extension QuickLaunchEntry {
var platforms: Set<Platform> {
Set(sources.compactMap { $0.platformHint })
}
}
34 changes: 34 additions & 0 deletions Tophat/Models/QuickLaunchEntrySource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// QuickLaunchEntrySource.swift
// Tophat
//
// Created by Lukas Romsicki on 2024-11-25.
// Copyright © 2024 Shopify. All rights reserved.
//

import Foundation
import TophatFoundation
import SwiftData

@Model
final class QuickLaunchEntrySource: Hashable {
var artifactProviderID: String
var artifactProviderParameters: [String: String]
var launchArguments: [String]
var platformHint: Platform
var destinationHint: DeviceType?

init(
artifactProviderID: String,
artifactProviderParameters: [String: String],
launchArguments: [String],
platformHint: Platform,
destinationHint: DeviceType? = nil
) {
self.artifactProviderID = artifactProviderID
self.artifactProviderParameters = artifactProviderParameters
self.launchArguments = launchArguments
self.platformHint = platformHint
self.destinationHint = destinationHint
}
}
56 changes: 41 additions & 15 deletions Tophat/TophatApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import AndroidDeviceKit
import AppleDeviceKit
import FluidMenuBarExtra
import TophatFoundation
import SwiftData

let log = Logger(label: Bundle.main.bundleIdentifier!)

Expand All @@ -43,11 +44,11 @@ struct TophatApp: App {
.environment(appDelegate.updateController)
.environment(appDelegate.extensionHost)
.environmentObject(appDelegate.deviceManager)
.environmentObject(appDelegate.pinnedApplicationState)
.environmentObject(appDelegate.utilityPathPreferences)
.environmentObject(appDelegate.launchAtLoginController)
.environmentObject(appDelegate.symbolicLinkManager)
}
.modelContainer(appDelegate.modelContainer)
}
}

Expand All @@ -56,6 +57,8 @@ private final class AppDelegate: NSObject, NSApplicationDelegate {
@AppStorage("ListenPort") private var listenPort: Int = 29070
@AppStorage("HasCompletedFirstLaunch") private var hasCompletedFirstLaunch = false

let modelContainer = try! ModelContainer(for: QuickLaunchEntry.self)

private var menuBarExtra: FluidMenuBarExtra?

private let sparkleUpdaterController = SPUStandardUpdaterController(
Expand All @@ -69,7 +72,6 @@ private final class AppDelegate: NSObject, NSApplicationDelegate {
private let notificationHandler = NotificationHandler()

let deviceManager: DeviceManager
let pinnedApplicationState: PinnedApplicationState
let utilityPathPreferences: UtilityPathPreferences
let symbolicLinkManager = TophatCtlSymbolicLinkManager()
let launchAtLoginController = LaunchAtLoginController()
Expand Down Expand Up @@ -99,12 +101,10 @@ private final class AppDelegate: NSObject, NSApplicationDelegate {

self.deviceSelectionManager = DeviceSelectionManager(deviceManager: deviceManager)
self.taskStatusReporter = TaskStatusReporter()
self.pinnedApplicationState = PinnedApplicationState()

self.installCoordinator = InstallCoordinator(
deviceManager: deviceManager,
deviceSelectionManager: deviceSelectionManager,
pinnedApplicationState: pinnedApplicationState,
taskStatusReporter: taskStatusReporter,
extensionHost: extensionHost
)
Expand Down Expand Up @@ -164,12 +164,12 @@ private final class AppDelegate: NSObject, NSApplicationDelegate {
.environmentObject(self.deviceManager)
.environmentObject(self.deviceSelectionManager)
.environmentObject(self.taskStatusReporter)
.environmentObject(self.pinnedApplicationState)
.environment(self.updateController)
.environment(\.launchApp, self.launchApp)
.environment(\.prepareDevice, self.prepareDevice)
.environment(\.mirrorDeviceDisplay, self.mirrorDeviceDisplay)
.environment(\.showOnboardingWindow, self.showOnboardingWindow)
.modelContainer(self.modelContainer)
}

performFirstLaunchTasks()
Expand Down Expand Up @@ -262,21 +262,47 @@ extension AppDelegate: TophatServerDelegate {
// MARK: - NotificationHandlerDelegate

extension AppDelegate: NotificationHandlerDelegate {
func notificationHandler(didReceiveRequestToAddPinnedApplication pinnedApplication: PinnedApplication) {
if let existingIndex = pinnedApplicationState.pinnedApplications.firstIndex(where: { $0.id == pinnedApplication.id }) {
let existingItem = pinnedApplicationState.pinnedApplications[existingIndex]
func notificationHandler(didReceiveRequestToAddQuickLaunchEntry quickLaunchEntry: QuickLaunchEntry) {
let context = ModelContext(modelContainer)

let existingID = quickLaunchEntry.id
let existingEntryFetchDescriptor = FetchDescriptor<QuickLaunchEntry>(
predicate: #Predicate { $0.id == existingID }
)

do {
if let existingEntry = try context.fetch(existingEntryFetchDescriptor).first {
existingEntry.name = quickLaunchEntry.name
existingEntry.sources = quickLaunchEntry.sources
} else {
var fetchDescriptor = FetchDescriptor<QuickLaunchEntry>(
sortBy: [SortDescriptor(\.order, order: .reverse)]
)
fetchDescriptor.fetchLimit = 1

let existingEntries = try? context.fetch(fetchDescriptor)
let lastOrder = existingEntries?.first?.order ?? 0
quickLaunchEntry.order = lastOrder + 1

var newPinnedApplication = pinnedApplication
newPinnedApplication.icon = existingItem.icon
pinnedApplicationState.pinnedApplications[existingIndex] = newPinnedApplication
context.insert(quickLaunchEntry)
}

} else {
pinnedApplicationState.pinnedApplications.append(pinnedApplication)
} catch {
log.error("Failed to update Quick Launch entry!")
}
}

func notificationHandler(didReceiveRequestToRemovePinnedApplicationWithIdentifier pinnedApplicationIdentifier: PinnedApplication.ID) {
pinnedApplicationState.pinnedApplications.removeAll { $0.id == pinnedApplicationIdentifier }
func notificationHandler(didReceiveRequestToRemoveQuickLaunchEntryWithIdentifier quickLaunchEntryIdentifier: QuickLaunchEntry.ID) {
let context = ModelContext(modelContainer)

do {
try context.delete(
model: QuickLaunchEntry.self,
where: #Predicate { $0.id == quickLaunchEntryIdentifier }
)
} catch {
log.error("Failed to delete Quick Launch entry.")
}
}

func notificationHandler(didOpenURL url: URL, launchArguments: [String]) {
Expand Down
7 changes: 1 addition & 6 deletions Tophat/Utilities/InstallCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

import Foundation
import TophatFoundation
import SwiftData

final class InstallCoordinator {
private unowned let deviceManager: DeviceManager
private unowned let pinnedApplicationState: PinnedApplicationState
private unowned let taskStatusReporter: TaskStatusReporter
private let deviceSelectionManager: DeviceSelectionManager

Expand All @@ -20,12 +20,10 @@ final class InstallCoordinator {
init(
deviceManager: DeviceManager,
deviceSelectionManager: DeviceSelectionManager,
pinnedApplicationState: PinnedApplicationState,
taskStatusReporter: TaskStatusReporter,
extensionHost: ExtensionHost
) {
self.deviceManager = deviceManager
self.pinnedApplicationState = pinnedApplicationState
self.deviceSelectionManager = deviceSelectionManager
self.taskStatusReporter = taskStatusReporter

Expand All @@ -45,7 +43,6 @@ final class InstallCoordinator {

let fetchArtifact = FetchArtifactTask(
taskStatusReporter: taskStatusReporter,
pinnedApplicationState: pinnedApplicationState,
artifactDownloader: artifactDownloader,
context: context
)
Expand Down Expand Up @@ -77,7 +74,6 @@ final class InstallCoordinator {

let fetchArtifact = FetchArtifactTask(
taskStatusReporter: taskStatusReporter,
pinnedApplicationState: pinnedApplicationState,
artifactDownloader: artifactDownloader,
context: context
)
Expand Down Expand Up @@ -108,7 +104,6 @@ final class InstallCoordinator {
private func install(ticket: InstallationTicketMachine.Ticket, context: LaunchContext? = nil) async throws {
let fetchArtifact = FetchArtifactTask(
taskStatusReporter: taskStatusReporter,
pinnedApplicationState: pinnedApplicationState,
artifactDownloader: artifactDownloader,
context: context
)
Expand Down
21 changes: 11 additions & 10 deletions Tophat/Utilities/NotificationHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import TophatFoundation
import TophatUtilities

protocol NotificationHandlerDelegate: AnyObject {
func notificationHandler(didReceiveRequestToAddPinnedApplication pinnedApplication: PinnedApplication)
func notificationHandler(didReceiveRequestToRemovePinnedApplicationWithIdentifier pinnedApplicationIdentifier: PinnedApplication.ID)
func notificationHandler(didReceiveRequestToAddQuickLaunchEntry quickLaunchEntry: QuickLaunchEntry)
func notificationHandler(didReceiveRequestToRemoveQuickLaunchEntryWithIdentifier quickLaunchEntryIdentifier: QuickLaunchEntry.ID)
func notificationHandler(didReceiveRequestToLaunchApplicationWithRecipes recipes: [InstallRecipe])
func notificationHandler(didOpenURL url: URL, launchArguments: [String])
}
Expand Down Expand Up @@ -54,36 +54,37 @@ final class NotificationHandler {
.store(in: &cancellables)

notifier
.publisher(for: TophatAddPinnedApplicationNotification.self)
.publisher(for: TophatAddQuickLaunchEntryNotification.self)
.sink { [weak self] payload in
let configuration = payload.configuration

let pinnedApplication = PinnedApplication(
let quickLaunchEntry = QuickLaunchEntry(
id: configuration.id,
name: configuration.name,
recipes: configuration.sources.map { source in
sources: configuration.sources.map { source in
let artifactProviderMetadata = ArtifactProviderMetadata(
id: source.artifactProviderID,
parameters: source.artifactProviderParameters
)

return InstallRecipe(
source: .artifactProvider(metadata: artifactProviderMetadata),
return QuickLaunchEntrySource(
artifactProviderID: artifactProviderMetadata.id,
artifactProviderParameters: artifactProviderMetadata.parameters,
launchArguments: source.launchArguments,
platformHint: source.platformHint,
destinationHint: source.destinationHint
)
}
)

self?.delegate?.notificationHandler(didReceiveRequestToAddPinnedApplication: pinnedApplication)
self?.delegate?.notificationHandler(didReceiveRequestToAddQuickLaunchEntry: quickLaunchEntry)
}
.store(in: &cancellables)

notifier
.publisher(for: TophatRemovePinnedApplicationNotification.self)
.publisher(for: TophatRemoveQuickLaunchEntryNotification.self)
.sink { [weak self] payload in
self?.delegate?.notificationHandler(didReceiveRequestToRemovePinnedApplicationWithIdentifier: payload.id)
self?.delegate?.notificationHandler(didReceiveRequestToRemoveQuickLaunchEntryWithIdentifier: payload.id)
}
.store(in: &cancellables)
}
Expand Down
21 changes: 0 additions & 21 deletions Tophat/Utilities/PinnedApplicationState.swift

This file was deleted.

Loading

0 comments on commit 6259f23

Please sign in to comment.