| | |
| | | var pendingTagRemovalDrop: PendingTagRemovalDrop? = nil |
| | | var uncategorizedDropSuppressFuturePrompt = false |
| | | var tagRemovalDropSuppressFuturePrompt = false |
| | | var usageTipsCloseReminderVisible = false |
| | | var usageTipsCloseSuppressFuturePrompt = false |
| | | var appDragResetToken = 0 |
| | | var bubbleDraftNote = "" |
| | | var tagNavigationHoveredGroupName: String? = nil |
| | |
| | | @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation |
| | | @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false |
| | | @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false |
| | | @AppStorage("skipUsageTipsCloseReminder") private var skipUsageTipsCloseReminder = false |
| | | |
| | | private let editSidebarWidth: CGFloat = 188 |
| | | private let editSidebarHorizontalInset: CGFloat = 12 |
| | |
| | | || appGridInteraction.appDragModeActive |
| | | || appGridInteraction.pendingUncategorizedDrop != nil |
| | | || appGridInteraction.pendingTagRemovalDrop != nil |
| | | || appGridInteraction.usageTipsCloseReminderVisible |
| | | } |
| | | private var appGridHighlightedGroupName: String? { |
| | | appGridInteraction.tagNavigationHoveredGroupName |
| | |
| | | } |
| | | |
| | | if shouldRenderAppGridBehindQuickSearch { |
| | | if notchHeight > 0 { |
| | | VStack { |
| | | Rectangle().fill(.black) |
| | | .frame(height: notchHeight) |
| | | .ignoresSafeArea(edges: .top) |
| | | Spacer() |
| | | } |
| | | .allowsHitTesting(false) |
| | | } |
| | | |
| | | switch editPhase { |
| | | case .none: |
| | | normalContent |
| | | case .editingTags: |
| | | editTagsView |
| | | case .editingApps: |
| | | editAppsView |
| | | } |
| | | |
| | | uncommonAppBubbleOverlay |
| | | smartStartNoticeOverlay |
| | | editActionFeedbackOverlay |
| | | uncategorizedDropConfirmOverlay |
| | | tagRemovalDropConfirmOverlay |
| | | appGridInteractionLayer |
| | | } |
| | | |
| | | quickSearchOverlay |
| | | |
| | | if shouldRenderAppGridBehindQuickSearch, let message = appGridInteraction.dropWarningToast { |
| | | Text(message) |
| | | .font(.system(size: 16, weight: .semibold)) |
| | | .foregroundStyle(.primary) |
| | | .padding(.horizontal, 22) |
| | | .padding(.vertical, 12) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 10) |
| | | .fill(.ultraThickMaterial) |
| | | .shadow(color: .black.opacity(0.22), radius: 18, y: 10) |
| | | ) |
| | | .transition(.scale(scale: 0.96).combined(with: .opacity)) |
| | | .allowsHitTesting(false) |
| | | } |
| | | |
| | | if shouldRenderAppGridBehindQuickSearch && appGridInteraction.dropRefreshVisible { |
| | | Color.black.opacity(0.08) |
| | | .ignoresSafeArea() |
| | | .transition(.opacity) |
| | | .allowsHitTesting(false) |
| | | |
| | | ProgressView() |
| | | .progressViewStyle(.circular) |
| | | .controlSize(.large) |
| | | .scaleEffect(1.15) |
| | | .padding(24) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 12) |
| | | .fill(.ultraThickMaterial) |
| | | .shadow(color: .black.opacity(0.26), radius: 24, y: 12) |
| | | ) |
| | | .transition(.scale(scale: 0.92).combined(with: .opacity)) |
| | | .allowsHitTesting(false) |
| | | } |
| | | dropWarningToastOverlay |
| | | dropRefreshOverlay |
| | | } |
| | | .onAppear { |
| | | refreshNotchHeight() |
| | |
| | | } |
| | | } |
| | | |
| | | @ViewBuilder |
| | | private var appGridInteractionLayer: some View { |
| | | if notchHeight > 0 { |
| | | VStack { |
| | | Rectangle().fill(.black) |
| | | .frame(height: notchHeight) |
| | | .ignoresSafeArea(edges: .top) |
| | | Spacer() |
| | | } |
| | | .allowsHitTesting(false) |
| | | } |
| | | |
| | | switch editPhase { |
| | | case .none: |
| | | normalContent |
| | | case .editingTags: |
| | | editTagsView |
| | | case .editingApps: |
| | | editAppsView |
| | | } |
| | | |
| | | uncommonAppBubbleOverlay |
| | | smartStartNoticeOverlay |
| | | editActionFeedbackOverlay |
| | | uncategorizedDropConfirmOverlay |
| | | tagRemovalDropConfirmOverlay |
| | | usageTipsCloseReminderOverlay |
| | | } |
| | | |
| | | @ViewBuilder |
| | | private var dropWarningToastOverlay: some View { |
| | | if shouldRenderAppGridBehindQuickSearch, let message = appGridInteraction.dropWarningToast { |
| | | Text(message) |
| | | .font(.system(size: 16, weight: .semibold)) |
| | | .foregroundStyle(.primary) |
| | | .padding(.horizontal, 22) |
| | | .padding(.vertical, 12) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 10) |
| | | .fill(.ultraThickMaterial) |
| | | .shadow(color: .black.opacity(0.22), radius: 18, y: 10) |
| | | ) |
| | | .transition(.scale(scale: 0.96).combined(with: .opacity)) |
| | | .allowsHitTesting(false) |
| | | } |
| | | } |
| | | |
| | | @ViewBuilder |
| | | private var dropRefreshOverlay: some View { |
| | | if shouldRenderAppGridBehindQuickSearch && appGridInteraction.dropRefreshVisible { |
| | | Color.black.opacity(0.08) |
| | | .ignoresSafeArea() |
| | | .transition(.opacity) |
| | | .allowsHitTesting(false) |
| | | |
| | | ProgressView() |
| | | .progressViewStyle(.circular) |
| | | .controlSize(.large) |
| | | .scaleEffect(1.15) |
| | | .padding(24) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 12) |
| | | .fill(.ultraThickMaterial) |
| | | .shadow(color: .black.opacity(0.26), radius: 24, y: 12) |
| | | ) |
| | | .transition(.scale(scale: 0.92).combined(with: .opacity)) |
| | | .allowsHitTesting(false) |
| | | } |
| | | } |
| | | |
| | | private func publishModalInteractionState() { |
| | | NotificationCenter.default.post( |
| | | name: .tagLauncherModalInteractionChanged, |
| | |
| | | "active": smartStartNotice != nil |
| | | || appGridInteraction.pendingUncategorizedDrop != nil |
| | | || appGridInteraction.pendingTagRemovalDrop != nil |
| | | || appGridInteraction.usageTipsCloseReminderVisible |
| | | ] |
| | | ) |
| | | } |
| | |
| | | }, |
| | | onScrollActivity: handleAppGridScrollActivity, |
| | | onDragModeChange: { setAppDragMode($0) }, |
| | | onHideUsageTips: { hideUsageTips = true }, |
| | | onHideUsageTips: requestHideUsageTips, |
| | | onUsageTipsHoverChange: handleUsageTipsHoverChange |
| | | ) |
| | | } |
| | |
| | | } |
| | | .ignoresSafeArea() |
| | | .allowsHitTesting(appGridInteraction.pendingTagRemovalDrop != nil) |
| | | } |
| | | |
| | | private var usageTipsCloseReminderOverlay: some View { |
| | | GeometryReader { proxy in |
| | | if appGridInteraction.usageTipsCloseReminderVisible { |
| | | ZStack { |
| | | Color.black.opacity(0.14) |
| | | .ignoresSafeArea() |
| | | |
| | | UsageTipsCloseReminderBubble( |
| | | title: tr("usageTips.closeReminder.title"), |
| | | message: tr("usageTips.closeReminder.message"), |
| | | previewCallout: tr("usageTips.closeReminder.previewCallout"), |
| | | doNotRemindTitle: tr("usageTips.closeReminder.doNotRemind"), |
| | | doNotRemind: $appGridInteraction.usageTipsCloseSuppressFuturePrompt, |
| | | confirmTitle: tr("edit.confirm"), |
| | | onCancel: dismissUsageTipsCloseReminder, |
| | | onConfirm: confirmUsageTipsCloseReminder |
| | | ) |
| | | .frame(width: min(760, max(420, proxy.size.width - 120))) |
| | | .position(x: proxy.size.width / 2, y: proxy.size.height / 2) |
| | | .transition(.scale(scale: 0.94).combined(with: .opacity)) |
| | | } |
| | | .zIndex(712) |
| | | } |
| | | } |
| | | .ignoresSafeArea() |
| | | .allowsHitTesting(appGridInteraction.usageTipsCloseReminderVisible) |
| | | } |
| | | |
| | | private func buildEditActionFeedback(for selectedApps: [AppInfo], tags: [String]) -> EditActionFeedback { |
| | |
| | | } |
| | | } |
| | | |
| | | private func requestHideUsageTips() { |
| | | guard !hideUsageTips else { return } |
| | | guard !skipUsageTipsCloseReminder else { |
| | | hideUsageTips = true |
| | | return |
| | | } |
| | | clearAppBubbleState() |
| | | appGridInteraction.usageTipsCloseSuppressFuturePrompt = false |
| | | withAnimation(.spring(response: 0.24, dampingFraction: 0.84)) { |
| | | appGridInteraction.usageTipsCloseReminderVisible = true |
| | | } |
| | | publishModalInteractionState() |
| | | } |
| | | |
| | | private func dismissUsageTipsCloseReminder() { |
| | | appGridInteraction.usageTipsCloseSuppressFuturePrompt = false |
| | | withAnimation(.easeOut(duration: 0.18)) { |
| | | appGridInteraction.usageTipsCloseReminderVisible = false |
| | | } |
| | | publishModalInteractionState() |
| | | } |
| | | |
| | | private func confirmUsageTipsCloseReminder() { |
| | | if appGridInteraction.usageTipsCloseSuppressFuturePrompt { |
| | | skipUsageTipsCloseReminder = true |
| | | } |
| | | appGridInteraction.usageTipsCloseSuppressFuturePrompt = false |
| | | withAnimation(.easeOut(duration: 0.16)) { |
| | | appGridInteraction.usageTipsCloseReminderVisible = false |
| | | } |
| | | hideUsageTips = true |
| | | publishModalInteractionState() |
| | | } |
| | | |
| | | private func removeTagFromDroppedApp(app: AppInfo, tagName: String) { |
| | | guard isRemovableRegularTag(tagName), |
| | | appHasTag(app, tagName: tagName) |