~ Tagging now using SwiftData

This commit is contained in:
David Bureš 2025-05-18 17:30:25 +02:00
parent 0a818a717e
commit fc88b4f896
No known key found for this signature in database
11 changed files with 135 additions and 134 deletions

View File

@ -68,8 +68,6 @@ final class AppState
// MARK: - Tagging
var taggedPackageNames: Set<String> = .init()
var corruptedPackage: String = ""
// MARK: - Other

View File

@ -309,23 +309,11 @@ private extension View
// MARK: - Getting tagged packages
do
{
view.appState.taggedPackageNames = try loadTaggedIDsFromDisk()
AppConstants.shared.logger.info("Tagged packages in appState: \(view.appState.taggedPackageNames)")
do
{
try await view.brewPackagesTracker.applyTags(appState: view.appState)
}
catch let taggedStateApplicationError as NSError
{
AppConstants.shared.logger.error("Error while applying tagged state to packages: \(taggedStateApplicationError, privacy: .public)")
view.appState.showAlert(errorToShow: .couldNotApplyTaggedStateToPackages)
}
try await view.brewPackagesTracker.applyTags()
}
catch let uuidLoadingError as NSError
catch let taggedStateApplicationError as NSError
{
AppConstants.shared.logger.error("Failed while loading UUIDs from file: \(uuidLoadingError, privacy: .public)")
AppConstants.shared.logger.error("Error while applying tagged state to packages: \(taggedStateApplicationError, privacy: .public)")
view.appState.showAlert(errorToShow: .couldNotApplyTaggedStateToPackages)
}
@ -489,18 +477,6 @@ private extension View
{
restartApp()
}
.onChange(of: view.appState.taggedPackageNames)
{
AppConstants.shared.logger.info("Will try to save tagged IDs to disk")
do
{
try saveTaggedIDsToDisk(appState: view.appState)
}
catch let dataSavingError as NSError
{
AppConstants.shared.logger.error("Failed while trying to save data to disk: \(dataSavingError, privacy: .public)")
}
}
}
}

View File

@ -7,13 +7,14 @@
// swiftlint:disable file_length
import ButtonKit
import CorkNotifications
import CorkShared
import DavidFoundation
import Defaults
import SwiftData
import SwiftUI
import UserNotifications
import ButtonKit
import Defaults
// swiftlint:disable type_body_length
@main
@ -23,7 +24,7 @@ struct CorkApp: App
@State var brewPackagesTracker: BrewPackagesTracker = .init()
@State var tapTracker: TapTracker = .init()
@State var cachedDownloadsTracker: CachedDownloadsTracker = .init()
@State var topPackagesTracker: TopPackagesTracker = .init()
@ -39,14 +40,14 @@ struct CorkApp: App
@Default(.hasFinishedLicensingWorkflow) var hasFinishedLicensingWorkflow: Bool
@Environment(\.openWindow) private var openWindow: OpenWindowAction
@Default(.showInMenuBar) var showInMenuBar: Bool
@Default(.areNotificationsEnabled) var areNotificationsEnabled: Bool
@Default(.outdatedPackageNotificationType) var outdatedPackageNotificationType: OutdatedPackageNotificationType
@Default(.lastSubmittedCorkVersion) var lastSubmittedCorkVersion: String
@AppStorage("defaultBackupDateFormat") var defaultBackupDateFormat: Date.FormatStyle.DateStyle = .numeric
@State private var sendStandardUpdatesAvailableNotification: Bool = true
@ -89,6 +90,9 @@ struct CorkApp: App
.environment(updateProgressTracker)
.environment(outdatedPackagesTracker)
.environment(topPackagesTracker)
.modelContainer(for: [
SavedTaggedPackage.self
])
.task
{
NSWindow.allowsAutomaticWindowTabbing = false
@ -420,7 +424,7 @@ struct CorkApp: App
}
.windowResizability(.contentSize)
.windowToolbarStyle(.unifiedCompact)
WindowGroup(id: .errorInspectorWindowID, for: String.self)
{ $errorToInspect in
if let errorToInspect
@ -727,7 +731,7 @@ struct CorkApp: App
} label: {
Text("debug.action.licensing")
}
Menu
{
Button
@ -757,7 +761,7 @@ struct CorkApp: App
NSApp.dockTile.badgeLabel = ""
}
}
private func setWhetherToSendStandardUpdatesAvailableNotification(to newValue: Bool)
{
self.sendStandardUpdatesAvailableNotification = newValue

View File

@ -7,13 +7,39 @@
import Foundation
import CorkShared
import SwiftData
extension BrewPackagesTracker
{
/// Load tagged package from storage, and apply them to the relevant packages
@MainActor
func applyTags(appState: AppState) async throws
func applyTags() async throws
{
for taggedName in appState.taggedPackageNames
let storageContext: ModelContext = AppConstants.shared.modelContainer.mainContext
let taggedPackageFetcher: FetchDescriptor = FetchDescriptor<SavedTaggedPackage>(
predicate: #Predicate { _ in
return true
}
)
/// Try to fetch the saved tagged packages, and stop execution if there are none
guard let loadedTaggedPackages = try? storageContext.fetch(taggedPackageFetcher) else
{
AppConstants.shared.logger.log("Failed to load tagged packages")
return
}
guard !loadedTaggedPackages.isEmpty else
{
AppConstants.shared.logger.log("There are no tagged packages to apply the tagged status to")
return
}
/// Change the custom saveable object into strings
let taggedPackagesFullNames: [String] = loadedTaggedPackages.map({ $0.fullName })
for taggedName in taggedPackagesFullNames
{
AppConstants.shared.logger.log("Will attempt to place package name \(taggedName, privacy: .public)")
self.installedFormulae = Set(self.installedFormulae.map
@ -23,7 +49,7 @@ extension BrewPackagesTracker
case .success(var brewPackage):
if brewPackage.name == taggedName
{
brewPackage.changeTaggedStatus()
brewPackage.changeTaggedStatus(purpose: .justLoading)
}
return .success(brewPackage)
case .failure(let error):
@ -38,7 +64,7 @@ extension BrewPackagesTracker
case .success(var brewPackage):
if brewPackage.name == taggedName
{
brewPackage.changeTaggedStatus()
brewPackage.changeTaggedStatus(purpose: .justLoading)
}
return .success(brewPackage)
case .failure(let error):

View File

@ -1,33 +0,0 @@
//
// Load Tagged IDs from Disk.swift
// Cork
//
// Created by David Bureš on 21.03.2023.
//
import Foundation
import CorkShared
func loadTaggedIDsFromDisk() throws -> Set<String>
{
var nameSet: Set<String> = .init()
do
{
let rawPackageNamesFromFile: String = try String(contentsOf: AppConstants.shared.metadataFilePath, encoding: .utf8)
let packageNamesAsArray: [String] = rawPackageNamesFromFile.components(separatedBy: ":")
for packageNameAsString in packageNamesAsArray
{
nameSet.insert(packageNameAsString)
}
}
catch let dataReadingError as NSError
{
AppConstants.shared.logger.error("Failed while reading data from disk: \(dataReadingError, privacy: .public)")
}
AppConstants.shared.logger.debug("Loaded name set: \(nameSet, privacy: .public)")
return nameSet
}

View File

@ -1,25 +0,0 @@
//
// Save Tagged IDs to Disk.swift
// Cork
//
// Created by David Bureš on 21.03.2023.
//
import Foundation
import CorkShared
@MainActor
func saveTaggedIDsToDisk(appState: AppState) throws
{
let namesAsString: String = appState.taggedPackageNames.compactMap { $0 }.joined(separator: ":")
AppConstants.shared.logger.debug("Names as string: \(namesAsString, privacy: .public)")
do
{
try namesAsString.write(to: AppConstants.shared.metadataFilePath, atomically: true, encoding: .utf8)
}
catch let writingError as NSError
{
AppConstants.shared.logger.error("Error while writing to file: \(writingError, privacy: .public)")
}
}

View File

@ -74,9 +74,46 @@ struct BrewPackage: Identifiable, Equatable, Hashable, Codable
{
return versions.formatted(.list(type: .and))
}
mutating func changeTaggedStatus()
/// The purpose of the tagged status change operation
enum TaggedStatusChangePurpose: String
{
/// Only load and apply the tagged status to packages
///
/// For when the tagged packages are just being loaded and applied to the packages
case justLoading = "loading"
/// Change and persist the change.
///
/// For when the user initiates the change.
case actuallyChangingTheTaggedState = "actually changing the tagged state"
}
/// Change the tagged status of a package, and optionally persist that change in the database
///
/// - Parameter purpose: The purpose of this operation
@MainActor
mutating func changeTaggedStatus(purpose: TaggedStatusChangePurpose)
{
let packageName: String = self.name
AppConstants.shared.logger.debug("Will change the tagged status of package \(packageName) for the purpose of \(purpose.rawValue)")
if purpose == .actuallyChangingTheTaggedState
{
let modelContext: ModelContext = AppConstants.shared.modelContainer.mainContext
if !isTagged
{
modelContext.insert(SavedTaggedPackage(fullName: self.name))
}
else
{
modelContext.delete(SavedTaggedPackage(fullName: self.name))
}
}
isTagged.toggle()
}

View File

@ -5,44 +5,26 @@
// Created by David Bureš - P on 22.04.2025.
//
import SwiftUI
import CorkShared
import SwiftUI
struct TagUntagButton: View
{
@Environment(AppState.self) var appState: AppState
@Environment(BrewPackagesTracker.self) var brewPackagesTracker: BrewPackagesTracker
let package: BrewPackage
var body: some View {
var body: some View
{
Button
{
changeTaggedStatus()
brewPackagesTracker.updatePackageInPlace(package)
{ package in
package.changeTaggedStatus(purpose: .actuallyChangingTheTaggedState)
}
} label: {
Label(package.isTagged ? "sidebar.section.all.contextmenu.untag-\(package.name)" : "sidebar.section.all.contextmenu.tag-\(package.name)", systemImage: package.isTagged ? "tag.slash" : "tag")
}
}
func changeTaggedStatus()
{
AppConstants.shared.logger.info("Will change tagged status of \(package.name). Current state of the tagged package tracker: \(appState.taggedPackageNames)")
brewPackagesTracker.updatePackageInPlace(package)
{ package in
package.changeTaggedStatus()
}
if package.isTagged
{
AppConstants.shared.logger.info("Tagged package tracker DOES contain \(package.name). Will remove")
appState.taggedPackageNames.remove(package.name)
}
else
{
AppConstants.shared.logger.info("Tagged package tracker does NOT contain \(package.name). Will insert")
appState.taggedPackageNames.insert(package.name)
}
}
}

View File

@ -7,6 +7,7 @@
import Foundation
import OSLog
import SwiftData
@preconcurrency import UserNotifications
public struct AppConstants: Sendable
@ -67,12 +68,24 @@ public struct AppConstants: Sendable
self.homebrewVariablesPath = localHomebrewVariablesPath
self.logger = internalLogger
let modelConfiguration: ModelConfiguration = .init(isStoredInMemoryOnly: false)
guard let initializedModelContainer = try? ModelContainer(for: SavedTaggedPackage.self, configurations: modelConfiguration) else
{
fatalError("Failed to initialize persistence container")
}
self.modelContainer = initializedModelContainer
}
// MARK: - Shared Instance
public static let shared: AppConstants = .init()
// MARK: - Persistence
public let modelContainer: ModelContainer
// MARK: - Logging
public let logger: Logger

View File

@ -0,0 +1,21 @@
//
// Tagged Packages Storage.swift
// Cork
//
// Created by David Bureš - P on 18.05.2025.
//
import Foundation
import SwiftData
@Model
public final class SavedTaggedPackage
{
/// Full names of packages, which includes the Homebrew version
@Attribute(.unique) @Attribute(.spotlight)
public var fullName: String
public init(fullName: String) {
self.fullName = fullName
}
}

View File

@ -15,7 +15,7 @@ func corkTarget(configureWithSelfCompiled: Bool) -> ProjectDescription.Target {
destinations: [.mac],
product: .app,
productName: "Cork",
bundleId: "com.davidbures.cork",
bundleId: "eu.davidbures.cork",
deploymentTargets: .macOS("14.0.0"),
infoPlist: .file(path: "Cork/Info.plist"),
sources: [
@ -83,7 +83,8 @@ let project = Project(
name: "CorkShared",
destinations: [.mac],
product: .staticLibrary,
bundleId: "com.davidbures.cork-shared",
bundleId: "eu.davidbures.cork-shared",
deploymentTargets: .macOS("14.0.0"),
sources: [
"Modules/Shared/**/*.swift"
],
@ -105,7 +106,8 @@ let project = Project(
name: "CorkNotifications",
destinations: [.mac],
product: .staticLibrary,
bundleId: "com.davidbures.cork-notifications",
bundleId: "eu.davidbures.cork-notifications",
deploymentTargets: .macOS("14.0.0"),
sources: [
"Modules/Notifications/**/*.swift"
],
@ -127,7 +129,7 @@ let project = Project(
name: "CorkHelp",
destinations: [.mac],
product: .bundle,
bundleId: "com.davidbures.corkhelp",
bundleId: "eu.davidbures.corkhelp",
settings: .settings(configurations: [
.debug(
name: "Debug",
@ -143,7 +145,7 @@ let project = Project(
name: "CorkTests",
destinations: [.mac],
product: .unitTests,
bundleId: "com.davidbures.cork-tests",
bundleId: "eu.davidbures.cork-tests",
sources: [
"Tests/**",
"Cork/**/*.swift"