| | |
| | | |
| | | enum SettingsTabTarget { |
| | | static let userInfoKey = "tab" |
| | | static let theme = "theme" |
| | | static let tags = "tags" |
| | | static let data = "data" |
| | | static let about = "about" |
| | | } |
| | | |
| | | // MARK: - Edit Phase |
| | |
| | | var editingBubble: AppBubbleContext? = nil |
| | | var pendingUncategorizedDrop: PendingUncategorizedDrop? = nil |
| | | var pendingTagRemovalDrop: PendingTagRemovalDrop? = nil |
| | | var proPrompt: PendingProPrompt? = nil |
| | | var uncategorizedDropSuppressFuturePrompt = false |
| | | var tagRemovalDropSuppressFuturePrompt = false |
| | | var usageTipsCloseReminderVisible = false |
| | |
| | | let tagName: String |
| | | } |
| | | |
| | | private struct PendingProPrompt: Identifiable { |
| | | let id = UUID() |
| | | let feature: ProFeature |
| | | let noteQuotaStatus: ProNoteQuotaStatus? |
| | | } |
| | | |
| | | struct ContentView: View { |
| | | let hideOverlay: () -> Void |
| | | private let initialQuickSearchSource: String? |
| | | |
| | | @Environment(\.colorScheme) private var colorScheme |
| | | @ObservedObject private var proEntitlement = ProEntitlementCenter.shared |
| | | @State private var allApps: [AppInfo] = [] |
| | | @State private var displayGroups: [TagGroup] = [] |
| | | @State private var tagColors: [String: Int] = [:] |
| | |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips |
| | | @AppStorage(AppGridTheme.storageKey) private var appGridThemeID = AppDefaults.appGridThemeID |
| | | @AppStorage(AppGridTheme.deepBlueTuningStorageKey) private var deepBlueThemeTuning = AppGridTheme.deepBlue.defaultColorTuning |
| | | @AppStorage(AppGridTheme.colorfulTuningStorageKey) private var colorfulThemeTuning = AppGridTheme.colorful.defaultColorTuning |
| | | @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation |
| | | @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false |
| | | @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false |
| | |
| | | || appGridInteraction.pendingUncategorizedDrop != nil |
| | | || appGridInteraction.pendingTagRemovalDrop != nil |
| | | || appGridInteraction.usageTipsCloseReminderVisible |
| | | || appGridInteraction.proPrompt != nil |
| | | } |
| | | private var appGridHighlightedGroupName: String? { |
| | | appGridInteraction.tagNavigationHoveredGroupName |
| | |
| | | } |
| | | |
| | | private var renderedAppGridTheme: AppGridTheme { |
| | | editPhase == .none ? appGridTheme : .defaultLight |
| | | editPhase == .none |
| | | ? ProEntitlementPolicy.effectiveTheme(for: appGridTheme) |
| | | : .defaultLight |
| | | } |
| | | |
| | | private var renderedAppGridThemeTuning: Double? { |
| | | guard editPhase == .none else { return nil } |
| | | return ProEntitlementPolicy.effectiveThemeTuning( |
| | | for: appGridTheme, |
| | | storedTuning: storedThemeTuning(for: appGridTheme) |
| | | ) |
| | | } |
| | | |
| | | private func storedThemeTuning(for theme: AppGridTheme) -> Double? { |
| | | switch theme { |
| | | case .deepBlue: |
| | | return deepBlueThemeTuning |
| | | case .colorful: |
| | | return colorfulThemeTuning |
| | | case .defaultLight, .black, .pink, .purple, .green, .blue: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | private var isSideLayout: Bool { |
| | |
| | | } |
| | | |
| | | var body: some View { |
| | | contentStack |
| | | .onAppear(perform: handleContentAppear) |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow), perform: handleOverlayDidShow) |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide), perform: handleOverlayDidHide) |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchRequested), perform: handleQuickSearchRequested) |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested), perform: handleQuickSearchDismissRequested) |
| | | .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification), perform: handleApplicationDidBecomeActive) |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherDataDidChange), perform: handleDataDidChange) |
| | | .onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange), perform: handleAppLanguageDidChange) |
| | | .onChange(of: editPhase) { _, newPhase in |
| | | let active = newPhase != .none |
| | | dismissAppBubble() |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherEditModeChanged, |
| | | object: nil, |
| | | userInfo: ["active": active] |
| | | ) |
| | | } |
| | | .onChange(of: bubbleNoteFocused) { _, focused in |
| | | if appGridInteraction.editingBubble != nil && !focused { |
| | | commitBubbleNote() |
| | | } |
| | | } |
| | | .onChange(of: draggedTagNames) { _, newOrder in |
| | | rebuildDisplayGroups(apps: allApps, tagOrder: newOrder) |
| | | } |
| | | .onChange(of: defaultGroupName) { _, _ in |
| | | rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames) |
| | | } |
| | | .onChange(of: quickSearchQuery) { _, _ in |
| | | quickSearchErrorMessage = nil |
| | | refreshQuickSearchResults() |
| | | } |
| | | .onChange(of: appGridInteraction.pendingUncategorizedDrop != nil) { _, _ in |
| | | publishModalInteractionState() |
| | | } |
| | | .onChange(of: appGridInteraction.pendingTagRemovalDrop != nil) { _, _ in |
| | | publishModalInteractionState() |
| | | } |
| | | .onChange(of: appGridInteraction.proPrompt != nil) { _, _ in |
| | | publishModalInteractionState() |
| | | } |
| | | .onChange(of: smartStartNotice != nil) { _, _ in |
| | | publishModalInteractionState() |
| | | } |
| | | .onChange(of: proEntitlement.accessState) { _, newState in |
| | | handleProAccessStateChange(newState) |
| | | } |
| | | .onDisappear { |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherModalInteractionChanged, |
| | | object: nil, |
| | | userInfo: ["active": false] |
| | | ) |
| | | } |
| | | } |
| | | |
| | | @ViewBuilder |
| | | private var contentStack: some View { |
| | | ZStack { |
| | | if shouldRenderAppGridBehindQuickSearch { |
| | | appGridBackground |
| | |
| | | |
| | | dropWarningToastOverlay |
| | | dropRefreshOverlay |
| | | } |
| | | .onAppear { |
| | | refreshNotchHeight() |
| | | refreshAppsForOverlay() |
| | | if let initialQuickSearchSource, !initialQuickSearchConsumed { |
| | | initialQuickSearchConsumed = true |
| | | if initialQuickSearchSource == QuickSearchOpenSource.globalHidden { |
| | | quickSearchCloseHidesOverlay = true |
| | | quickSearchOnlySession = true |
| | | } |
| | | if !quickSearchVisible { |
| | | quickSearchVisible = true |
| | | quickSearchFocusToken &+= 1 |
| | | } |
| | | quickSearchOpenedAt = Date() |
| | | refreshQuickSearchResults() |
| | | refreshAppsForQuickSearch() |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherQuickSearchVisibilityChanged, |
| | | object: nil, |
| | | userInfo: [ |
| | | "active": true, |
| | | "hideOverlayOnClose": quickSearchCloseHidesOverlay |
| | | ] |
| | | ) |
| | | } |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in |
| | | resetTransientDragState() |
| | | refreshNotchHeight() |
| | | refreshAppsForOverlay() |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in |
| | | resetTransientDragState() |
| | | closeQuickSearch(notify: true, hideOverlayIfNeeded: false) |
| | | quickSearchOnlySession = false |
| | | quickSearchCloseHidesOverlay = false |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchRequested)) { notification in |
| | | let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay |
| | | openQuickSearch(source: source) |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested)) { notification in |
| | | let source = notification.userInfo?["source"] as? String |
| | | closeQuickSearch(dismissalSource: source) |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in |
| | | guard allApps.isEmpty, !refreshInProgress else { return } |
| | | refreshApps() |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .tagLauncherDataDidChange)) { _ in |
| | | refreshApps(forceLayoutRefresh: true) |
| | | } |
| | | .onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in |
| | | let appsSnapshot = allApps |
| | | DispatchQueue.global(qos: .userInitiated).async { |
| | | _ = AppleDefaultAppCatalog.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot) |
| | | _ = SmartStartService.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot) |
| | | DispatchQueue.main.async { |
| | | refreshApps(forceLayoutRefresh: true) |
| | | } |
| | | } |
| | | } |
| | | .onChange(of: editPhase) { _, newPhase in |
| | | let active = newPhase != .none |
| | | dismissAppBubble() |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherEditModeChanged, |
| | | object: nil, |
| | | userInfo: ["active": active] |
| | | ) |
| | | } |
| | | .onChange(of: bubbleNoteFocused) { _, focused in |
| | | if appGridInteraction.editingBubble != nil && !focused { |
| | | commitBubbleNote() |
| | | } |
| | | } |
| | | .onChange(of: draggedTagNames) { _, newOrder in |
| | | rebuildDisplayGroups(apps: allApps, tagOrder: newOrder) |
| | | } |
| | | .onChange(of: defaultGroupName) { _, _ in |
| | | rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames) |
| | | } |
| | | .onChange(of: quickSearchQuery) { _, _ in |
| | | quickSearchErrorMessage = nil |
| | | refreshQuickSearchResults() |
| | | } |
| | | .onChange(of: appGridInteraction.pendingUncategorizedDrop != nil) { _, _ in |
| | | publishModalInteractionState() |
| | | } |
| | | .onChange(of: appGridInteraction.pendingTagRemovalDrop != nil) { _, _ in |
| | | publishModalInteractionState() |
| | | } |
| | | .onChange(of: smartStartNotice != nil) { _, _ in |
| | | publishModalInteractionState() |
| | | } |
| | | .onDisappear { |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherModalInteractionChanged, |
| | | object: nil, |
| | | userInfo: ["active": false] |
| | | ) |
| | | } |
| | | } |
| | | |
| | |
| | | uncategorizedDropConfirmOverlay |
| | | tagRemovalDropConfirmOverlay |
| | | usageTipsCloseReminderOverlay |
| | | proAccessPromptOverlay |
| | | } |
| | | |
| | | @ViewBuilder |
| | |
| | | ) |
| | | .transition(.scale(scale: 0.96).combined(with: .opacity)) |
| | | .allowsHitTesting(false) |
| | | } |
| | | } |
| | | |
| | | @ViewBuilder |
| | | private var proAccessPromptOverlay: some View { |
| | | if let prompt = appGridInteraction.proPrompt { |
| | | ZStack { |
| | | Color.black.opacity(0.16) |
| | | .ignoresSafeArea() |
| | | |
| | | ProUpgradePromptView( |
| | | title: tr(prompt.feature.titleKey), |
| | | message: proPromptMessage(for: prompt), |
| | | benefitText: tr(prompt.feature.benefitKey), |
| | | operationText: proEntitlement.operationState.statusMessageKey.map(tr), |
| | | unlockTitle: tr("pro.card.unlock"), |
| | | restoreTitle: tr("pro.card.restore"), |
| | | isBusy: proEntitlement.operationState.isBusy, |
| | | onClose: dismissProPrompt, |
| | | onUnlock: { proEntitlement.purchasePro() }, |
| | | onRestore: { proEntitlement.restorePurchases() } |
| | | ) |
| | | } |
| | | .transition(.opacity) |
| | | .zIndex(920) |
| | | } |
| | | } |
| | | |
| | |
| | | || appGridInteraction.pendingUncategorizedDrop != nil |
| | | || appGridInteraction.pendingTagRemovalDrop != nil |
| | | || appGridInteraction.usageTipsCloseReminderVisible |
| | | || appGridInteraction.proPrompt != nil |
| | | ] |
| | | ) |
| | | } |
| | |
| | | |
| | | private var appGridBackground: some View { |
| | | let theme = renderedAppGridTheme |
| | | let tuning = renderedAppGridThemeTuning |
| | | return ZStack { |
| | | if theme.usesVisualEffectBackdrop { |
| | | VisualEffectView( |
| | |
| | | } |
| | | if !theme.isDefaultLight { |
| | | LinearGradient( |
| | | colors: theme.backgroundBaseColors, |
| | | colors: theme.backgroundBaseColors(tuning: tuning), |
| | | startPoint: .topLeading, |
| | | endPoint: .bottomTrailing |
| | | ) |
| | | LinearGradient( |
| | | colors: theme.backgroundAccentColors, |
| | | colors: theme.backgroundAccentColors(tuning: tuning), |
| | | startPoint: .topTrailing, |
| | | endPoint: .bottomLeading |
| | | ) |
| | |
| | | isEditing: editing, |
| | | placement: placement, |
| | | arrowOffset: metrics.arrowOffset, |
| | | noteQuotaText: editing ? noteQuotaHintText(for: context.app) : nil, |
| | | noteQuotaWarning: editing && isNoteQuotaWarning(for: context.app), |
| | | draftNote: $appGridInteraction.bubbleDraftNote, |
| | | noteFocused: $bubbleNoteFocused, |
| | | onCommit: commitBubbleNote, |
| | |
| | | |
| | | private func commitBubbleNote() { |
| | | guard let context = appGridInteraction.editingBubble else { return } |
| | | guard appGridInteraction.proPrompt == nil else { return } |
| | | let limited = String(appGridInteraction.bubbleDraftNote.prefix(TagDatabase.maxAppNoteLength)) |
| | | .trimmingCharacters(in: .whitespacesAndNewlines) |
| | | TagEditor.setAppNote(limited, for: context.app.path.path) |
| | | let decision = TagEditor.setAppNote(limited, for: context.app.path.path) |
| | | appGridInteraction.bubbleDraftNote = limited |
| | | guard case .allow = decision else { |
| | | if case .blocked(let status) = decision { |
| | | presentProPrompt(for: .unlimitedNotes, noteQuotaStatus: status) |
| | | } |
| | | bubbleNoteFocused = false |
| | | return |
| | | } |
| | | appGridInteraction.editingBubble = nil |
| | | bubbleNoteFocused = false |
| | | notifyAppNoteEditing(active: false) |
| | |
| | | return app.note ?? "" |
| | | } |
| | | |
| | | private func noteQuotaStatus(for app: AppInfo) -> ProNoteQuotaStatus? { |
| | | guard !proEntitlement.isUnlocked else { return nil } |
| | | let decision = ProEntitlementPolicy.noteSaveDecision( |
| | | note: appGridInteraction.bubbleDraftNote, |
| | | for: app.path.path |
| | | ) |
| | | switch decision { |
| | | case .allow(let status), .blocked(let status): |
| | | return status |
| | | } |
| | | } |
| | | |
| | | private func noteQuotaHintText(for app: AppInfo) -> String? { |
| | | guard let status = noteQuotaStatus(for: app), !status.isUnlimited else { return nil } |
| | | if status.remaining <= 0 { |
| | | return tr("pro.notes.limitReached") |
| | | } |
| | | return tr( |
| | | "pro.notes.remaining", |
| | | replacements: [ |
| | | "%remaining%": "\(status.remaining)", |
| | | "%limit%": "\(status.limit)" |
| | | ] |
| | | ) |
| | | } |
| | | |
| | | private func isNoteQuotaWarning(for app: AppInfo) -> Bool { |
| | | guard let status = noteQuotaStatus(for: app), !status.isUnlimited else { return false } |
| | | return status.remaining <= 0 |
| | | } |
| | | |
| | | private func proPromptMessage(for prompt: PendingProPrompt) -> String { |
| | | if prompt.feature == .unlimitedNotes, |
| | | let status = prompt.noteQuotaStatus, |
| | | !status.isUnlimited, |
| | | status.remaining <= 0 { |
| | | return tr("pro.notes.limitReached") |
| | | } |
| | | return tr(prompt.feature.promptMessageKey) |
| | | } |
| | | |
| | | private func presentProPrompt(for feature: ProFeature, noteQuotaStatus: ProNoteQuotaStatus? = nil) { |
| | | proEntitlement.clearTransientOperationState() |
| | | withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) { |
| | | appGridInteraction.proPrompt = PendingProPrompt( |
| | | feature: feature, |
| | | noteQuotaStatus: noteQuotaStatus |
| | | ) |
| | | } |
| | | } |
| | | |
| | | private func dismissProPrompt() { |
| | | proEntitlement.clearTransientOperationState() |
| | | withAnimation(.easeOut(duration: 0.18)) { |
| | | appGridInteraction.proPrompt = nil |
| | | } |
| | | } |
| | | |
| | | private func handleProAccessStateChange(_ newState: ProAccessState) { |
| | | guard newState.isUnlocked else { return } |
| | | |
| | | if let preview = proEntitlement.themePreviewState { |
| | | appGridThemeID = preview.theme.rawValue |
| | | proEntitlement.stopThemePreview() |
| | | } |
| | | |
| | | let hadPrompt = appGridInteraction.proPrompt != nil |
| | | if hadPrompt { |
| | | dismissProPrompt() |
| | | } |
| | | |
| | | if appGridInteraction.editingBubble != nil { |
| | | DispatchQueue.main.async { |
| | | bubbleNoteFocused = true |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func handleContentAppear() { |
| | | refreshNotchHeight() |
| | | refreshAppsForOverlay() |
| | | guard let initialQuickSearchSource, !initialQuickSearchConsumed else { return } |
| | | |
| | | initialQuickSearchConsumed = true |
| | | if initialQuickSearchSource == QuickSearchOpenSource.globalHidden { |
| | | quickSearchCloseHidesOverlay = true |
| | | quickSearchOnlySession = true |
| | | } |
| | | if !quickSearchVisible { |
| | | quickSearchVisible = true |
| | | quickSearchFocusToken &+= 1 |
| | | } |
| | | quickSearchOpenedAt = Date() |
| | | refreshQuickSearchResults() |
| | | refreshAppsForQuickSearch() |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherQuickSearchVisibilityChanged, |
| | | object: nil, |
| | | userInfo: [ |
| | | "active": true, |
| | | "hideOverlayOnClose": quickSearchCloseHidesOverlay |
| | | ] |
| | | ) |
| | | } |
| | | |
| | | private func handleOverlayDidShow(_ notification: Notification) { |
| | | _ = notification |
| | | resetTransientDragState() |
| | | refreshNotchHeight() |
| | | refreshAppsForOverlay() |
| | | } |
| | | |
| | | private func handleOverlayDidHide(_ notification: Notification) { |
| | | _ = notification |
| | | resetTransientDragState() |
| | | closeQuickSearch(notify: true, hideOverlayIfNeeded: false) |
| | | quickSearchOnlySession = false |
| | | quickSearchCloseHidesOverlay = false |
| | | } |
| | | |
| | | private func handleQuickSearchRequested(_ notification: Notification) { |
| | | let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay |
| | | openQuickSearch(source: source) |
| | | } |
| | | |
| | | private func handleQuickSearchDismissRequested(_ notification: Notification) { |
| | | let source = notification.userInfo?["source"] as? String |
| | | closeQuickSearch(dismissalSource: source) |
| | | } |
| | | |
| | | private func handleApplicationDidBecomeActive(_ notification: Notification) { |
| | | _ = notification |
| | | guard allApps.isEmpty, !refreshInProgress else { return } |
| | | refreshApps() |
| | | } |
| | | |
| | | private func handleDataDidChange(_ notification: Notification) { |
| | | _ = notification |
| | | refreshApps(forceLayoutRefresh: true) |
| | | } |
| | | |
| | | private func handleAppLanguageDidChange(_ notification: Notification) { |
| | | _ = notification |
| | | let appsSnapshot = allApps |
| | | DispatchQueue.global(qos: .userInitiated).async { |
| | | _ = AppleDefaultAppCatalog.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot) |
| | | _ = SmartStartService.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot) |
| | | DispatchQueue.main.async { |
| | | refreshApps(forceLayoutRefresh: true) |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func bubblePlacement(for frame: CGRect, rootFrame: CGRect) -> BubblePlacement { |
| | | frame.minY < 170 ? .below : .above |
| | | } |
| | |
| | | |
| | | private func estimatedBubbleHeight(for app: AppInfo, width: CGFloat, isEditing: Bool) -> CGFloat { |
| | | if isEditing { |
| | | return 120 |
| | | let hint = noteQuotaHintText(for: app) |
| | | return hint == nil ? 124 : 148 |
| | | } |
| | | let note = currentNote(for: app).trimmingCharacters(in: .whitespacesAndNewlines) |
| | | guard !note.isEmpty else { return 68 } |
| | |
| | | |
| | | containerAppOrder[key] = paths |
| | | rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames) |
| | | guard proEntitlement.isUnlocked else { |
| | | showDropWarning(message: tr("pro.sorting.previewToast")) |
| | | return |
| | | } |
| | | TagEditor.reorderApps(inContainer: key, orderedPaths: paths) |
| | | refreshApps(forceLayoutRefresh: true) |
| | | } |
| | |
| | | return !protectedNames.contains(tag) && tagColors[tag] != nil |
| | | } |
| | | |
| | | private func showDropWarning() { |
| | | private func showDropWarning(message: String = tr("drop.systemDefaultWarning")) { |
| | | withAnimation(.spring(response: 0.24, dampingFraction: 0.82)) { |
| | | appGridInteraction.dropWarningToast = tr("drop.systemDefaultWarning") |
| | | appGridInteraction.dropWarningToast = message |
| | | } |
| | | DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) { |
| | | withAnimation(.easeOut(duration: 0.18)) { |
| | |
| | | closeQuickSearch(hideOverlayIfNeeded: true) |
| | | hideOverlay() |
| | | } |
| | | if isCurrentApp(app) { |
| | | if closeQuickSearchOnSuccess && !closeQuickSearchAndOverlayBeforeOpening { |
| | | closeQuickSearch(hideOverlayIfNeeded: closeOverlayOnSuccess) |
| | | } |
| | | if closeOverlayOnSuccess && !closeQuickSearchAndOverlayBeforeOpening { |
| | | hideOverlay() |
| | | } |
| | | return |
| | | } |
| | | let configuration = NSWorkspace.OpenConfiguration() |
| | | NSWorkspace.shared.openApplication(at: app.path, configuration: configuration) { _, error in |
| | | DispatchQueue.main.async { |
| | |
| | | } |
| | | } |
| | | |
| | | private func isCurrentApp(_ app: AppInfo) -> Bool { |
| | | if app.bundleIdentifier?.caseInsensitiveCompare(AppIdentity.bundleIdentifier) == .orderedSame { |
| | | return true |
| | | } |
| | | |
| | | let currentURL = Bundle.main.bundleURL.standardizedFileURL.resolvingSymlinksInPath() |
| | | let appURL = app.path.standardizedFileURL.resolvingSymlinksInPath() |
| | | return currentURL == appURL |
| | | } |
| | | |
| | | func openApp(_ app: AppInfo) { |
| | | hideOverlay() |
| | | launchApp(app, closeOverlayOnSuccess: false) |