| | |
| | | var editingBubble: AppBubbleContext? = nil |
| | | var pendingUncategorizedDrop: PendingUncategorizedDrop? = nil |
| | | var pendingTagRemovalDrop: PendingTagRemovalDrop? = nil |
| | | var uncategorizedDropSuppressFuturePrompt = false |
| | | var tagRemovalDropSuppressFuturePrompt = false |
| | | var appDragResetToken = 0 |
| | | var bubbleDraftNote = "" |
| | |
| | | let id = UUID() |
| | | let app: AppInfo |
| | | let tagName: String |
| | | } |
| | | |
| | | private enum SmartStartNoticeMode { |
| | | case autoApplied |
| | | case suggestionOnly |
| | | case manuallyApplied |
| | | } |
| | | |
| | | private struct SmartStartNotice: Identifiable { |
| | | let id = UUID() |
| | | let mode: SmartStartNoticeMode |
| | | let title: String |
| | | let message: String |
| | | let summary: SmartStartSummary |
| | | } |
| | | |
| | | struct ContentView: View { |
| | |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation |
| | | @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false |
| | | @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false |
| | | |
| | | private let editSidebarWidth: CGFloat = 188 |
| | | private let editSidebarHorizontalInset: CGFloat = 12 |
| | |
| | | } |
| | | |
| | | private var smartStartNoticeOverlay: some View { |
| | | GeometryReader { proxy in |
| | | if let notice = smartStartNotice { |
| | | let panelWidth = min(560, max(320, proxy.size.width - 64)) |
| | | |
| | | ZStack { |
| | | Color.black.opacity(0.10) |
| | | .ignoresSafeArea() |
| | | |
| | | VStack(spacing: 18) { |
| | | Image(systemName: notice.mode == .suggestionOnly ? "sparkles" : "checkmark.seal.fill") |
| | | .font(.system(size: 30, weight: .semibold)) |
| | | .foregroundStyle(Color.accentColor) |
| | | .frame(width: 58, height: 58) |
| | | .background( |
| | | Circle() |
| | | .fill(Color.accentColor.opacity(0.12)) |
| | | ) |
| | | |
| | | VStack(spacing: 8) { |
| | | Text(notice.title) |
| | | .font(.system(size: 21, weight: .semibold)) |
| | | .foregroundStyle(.primary) |
| | | .multilineTextAlignment(.center) |
| | | |
| | | Text(notice.message) |
| | | .font(.system(size: 15, weight: .regular)) |
| | | .foregroundStyle(.secondary) |
| | | .multilineTextAlignment(.center) |
| | | .lineLimit(4) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | } |
| | | |
| | | HStack(spacing: 12) { |
| | | switch notice.mode { |
| | | case .suggestionOnly: |
| | | Button(tr("smartstart.later")) { |
| | | dismissSmartStartNotice() |
| | | } |
| | | .buttonStyle(.bordered) |
| | | .controlSize(.large) |
| | | |
| | | Button(tr("smartstart.apply")) { |
| | | applySmartStartSuggestion() |
| | | } |
| | | .buttonStyle(.borderedProminent) |
| | | .controlSize(.large) |
| | | case .autoApplied, .manuallyApplied: |
| | | if notice.summary.backupPath != nil { |
| | | Button(tr("smartstart.undo")) { |
| | | undoSmartStart() |
| | | } |
| | | .buttonStyle(.bordered) |
| | | .controlSize(.large) |
| | | } |
| | | Button(tr("smartstart.ok")) { |
| | | dismissSmartStartNotice() |
| | | } |
| | | .buttonStyle(.borderedProminent) |
| | | .controlSize(.large) |
| | | } |
| | | } |
| | | } |
| | | .padding(.horizontal, 30) |
| | | .padding(.vertical, 26) |
| | | .frame(width: panelWidth) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 16) |
| | | .fill(.ultraThickMaterial) |
| | | .shadow(color: .black.opacity(0.26), radius: 28, y: 16) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 16) |
| | | .stroke(.white.opacity(0.16), lineWidth: 1) |
| | | ) |
| | | } |
| | | .frame(width: proxy.size.width, height: proxy.size.height) |
| | | .transition(.scale(scale: 0.96).combined(with: .opacity)) |
| | | .zIndex(650) |
| | | } |
| | | } |
| | | .ignoresSafeArea() |
| | | .allowsHitTesting(smartStartNotice != nil) |
| | | SmartStartNoticeOverlay( |
| | | notice: smartStartNotice, |
| | | onDismiss: dismissSmartStartNotice, |
| | | onApply: applySmartStartSuggestion, |
| | | onUndo: undoSmartStart |
| | | ) |
| | | } |
| | | // MARK: - Edit Tags View |
| | | |
| | |
| | | UncategorizedDropConfirmBubble( |
| | | title: tr("drop.uncategorizedConfirmTitle"), |
| | | message: uncategorizedConfirmMessage(for: pendingDrop), |
| | | doNotRemindTitle: tr("drop.removeTagDoNotAskAgain"), |
| | | doNotRemind: $appGridInteraction.uncategorizedDropSuppressFuturePrompt, |
| | | cancelTitle: tr("tag.cancel"), |
| | | confirmTitle: tr("edit.confirm"), |
| | | onCancel: dismissUncategorizedDropConfirm, |
| | |
| | | } |
| | | |
| | | private func handleAppGridScrollActivity() { |
| | | appGridInteraction.hoveredBubble = nil |
| | | if appGridInteraction.hoveredBubble != nil { |
| | | appGridInteraction.hoveredBubble = nil |
| | | } |
| | | } |
| | | |
| | | private func handleBubbleHover(app: AppInfo, frame: CGRect, event: AppBubbleHoverEvent) { |
| | |
| | | guard !removableTags.isEmpty else { return } |
| | | let assignedTags = assignedRegularDisplayTags(for: removableTags) |
| | | |
| | | if skipUncategorizedDropConfirm { |
| | | moveDroppedAppToUncategorized(path: app.path.path, tags: removableTags) |
| | | return |
| | | } |
| | | |
| | | clearAppBubbleState() |
| | | appGridInteraction.uncategorizedDropSuppressFuturePrompt = false |
| | | withAnimation(.spring(response: 0.24, dampingFraction: 0.84)) { |
| | | appGridInteraction.pendingUncategorizedDrop = PendingUncategorizedDrop( |
| | | app: app, |
| | |
| | | } |
| | | |
| | | private func dismissUncategorizedDropConfirm() { |
| | | appGridInteraction.uncategorizedDropSuppressFuturePrompt = false |
| | | resetTransientDragState(keepingPendingUncategorizedDrop: true) |
| | | withAnimation(.easeOut(duration: 0.18)) { |
| | | appGridInteraction.pendingUncategorizedDrop = nil |
| | |
| | | guard let pendingDrop = appGridInteraction.pendingUncategorizedDrop else { return } |
| | | let path = pendingDrop.app.path.path |
| | | let tags = pendingDrop.removableTags |
| | | let shouldSuppressFuturePrompt = appGridInteraction.uncategorizedDropSuppressFuturePrompt |
| | | if shouldSuppressFuturePrompt { |
| | | skipUncategorizedDropConfirm = true |
| | | } |
| | | appGridInteraction.uncategorizedDropSuppressFuturePrompt = false |
| | | resetTransientDragState(keepingPendingUncategorizedDrop: true) |
| | | withAnimation(.easeOut(duration: 0.16)) { |
| | | appGridInteraction.pendingUncategorizedDrop = nil |
| | | } |
| | | moveDroppedAppToUncategorized(path: path, tags: tags) |
| | | } |
| | | |
| | | private func moveDroppedAppToUncategorized(path: String, tags: [String]) { |
| | | guard !tags.isEmpty else { return } |
| | | TagEditor.removeTags(tags, from: [path]) |
| | | showDropRefresh() |