From a5cba8fa3153098d95acb20b6060fac704ecc489 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 18 May 2026 16:57:45 +0800
Subject: [PATCH] Use in-app confirmation for uncategorized drops
---
Apptag/ContentView.swift | 75 ++++++++++++++++++++-----
Apptag/EditModeViews.swift | 77 +++++++++++++++++++++++++
Apptag/Info.plist | 4
CHANGELOG.md | 6 ++
4 files changed, 145 insertions(+), 17 deletions(-)
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 045bb29..87ee89a 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -324,6 +324,12 @@
let message: String
}
+private struct PendingUncategorizedDrop: Identifiable {
+ let id = UUID()
+ let app: AppInfo
+ let assignedTags: [String]
+}
+
private enum SmartStartNoticeMode {
case autoApplied
case suggestionOnly
@@ -373,6 +379,7 @@
@State private var hoveredAppItemID: String? = nil
@State private var hoveredBubble: AppBubbleContext? = nil
@State private var editingBubble: AppBubbleContext? = nil
+ @State private var pendingUncategorizedDrop: PendingUncategorizedDrop? = nil
@State private var bubbleDraftNote = ""
@FocusState private var bubbleNoteFocused: Bool
@@ -435,6 +442,7 @@
uncommonAppBubbleOverlay
smartStartNoticeOverlay
editActionFeedbackOverlay
+ uncategorizedDropConfirmOverlay
if let message = dropWarningToast {
Text(message)
@@ -1562,6 +1570,32 @@
.allowsHitTesting(editActionFeedback != nil)
}
+ private var uncategorizedDropConfirmOverlay: some View {
+ GeometryReader { proxy in
+ if let pendingDrop = pendingUncategorizedDrop {
+ ZStack {
+ Color.black.opacity(0.14)
+ .ignoresSafeArea()
+
+ UncategorizedDropConfirmBubble(
+ title: tr("drop.uncategorizedConfirmTitle"),
+ message: uncategorizedConfirmMessage(for: pendingDrop),
+ cancelTitle: tr("tag.cancel"),
+ confirmTitle: tr("edit.confirm"),
+ onCancel: dismissUncategorizedDropConfirm,
+ onConfirm: confirmPendingUncategorizedDrop
+ )
+ .frame(width: min(540, max(340, proxy.size.width - 120)))
+ .position(x: proxy.size.width / 2, y: proxy.size.height / 2)
+ .transition(.scale(scale: 0.94).combined(with: .opacity))
+ }
+ .zIndex(710)
+ }
+ }
+ .ignoresSafeArea()
+ .allowsHitTesting(pendingUncategorizedDrop != nil)
+ }
+
private func buildEditActionFeedback(for selectedApps: [AppInfo], tags: [String]) -> EditActionFeedback {
let appNames = selectedApps.map(\.name)
.joined(separator: localizedListSeparator)
@@ -2054,11 +2088,10 @@
guard let app = allApps.first(where: { $0.path.path == path }) else { return }
let assignedTags = assignedRegularDisplayTags(for: app)
guard !assignedTags.isEmpty else { return }
- guard confirmUncategorizedDrop(appName: app.name, assignedTags: assignedTags) else { return }
- TagEditor.removeTags(app.tags, from: [path])
- showDropRefresh()
- refreshApps(forceLayoutRefresh: true)
+ withAnimation(.spring(response: 0.24, dampingFraction: 0.84)) {
+ pendingUncategorizedDrop = PendingUncategorizedDrop(app: app, assignedTags: assignedTags)
+ }
}
private func assignedRegularDisplayTags(for app: AppInfo) -> [String] {
@@ -2072,21 +2105,33 @@
return result
}
- private func confirmUncategorizedDrop(appName: String, assignedTags: [String]) -> Bool {
- let alert = NSAlert()
- alert.alertStyle = .warning
- alert.messageText = tr("drop.uncategorizedConfirmTitle")
- alert.informativeText = formattedFeedbackMessage(
+ private func uncategorizedConfirmMessage(for pendingDrop: PendingUncategorizedDrop) -> String {
+ formattedFeedbackMessage(
forKey: "drop.uncategorizedConfirmMessage",
replacements: [
- "%appName%": appName,
- "%tagCount%": "\(assignedTags.count)",
- "%tagNames%": assignedTags.joined(separator: localizedListSeparator)
+ "%appName%": pendingDrop.app.name,
+ "%tagCount%": "\(pendingDrop.assignedTags.count)",
+ "%tagNames%": pendingDrop.assignedTags.joined(separator: localizedListSeparator)
]
)
- alert.addButton(withTitle: tr("edit.confirm"))
- alert.addButton(withTitle: tr("tag.cancel"))
- return alert.runModal() == .alertFirstButtonReturn
+ }
+
+ private func dismissUncategorizedDropConfirm() {
+ withAnimation(.easeOut(duration: 0.18)) {
+ pendingUncategorizedDrop = nil
+ }
+ }
+
+ private func confirmPendingUncategorizedDrop() {
+ guard let pendingDrop = pendingUncategorizedDrop else { return }
+ let path = pendingDrop.app.path.path
+ let tags = pendingDrop.app.tags
+ withAnimation(.easeOut(duration: 0.16)) {
+ pendingUncategorizedDrop = nil
+ }
+ TagEditor.removeTags(tags, from: [path])
+ showDropRefresh()
+ refreshApps(forceLayoutRefresh: true)
}
private func isUncategorizedDropTarget(_ targetTag: String) -> Bool {
diff --git a/Apptag/EditModeViews.swift b/Apptag/EditModeViews.swift
index 87b7f0d..b64956d 100644
--- a/Apptag/EditModeViews.swift
+++ b/Apptag/EditModeViews.swift
@@ -310,3 +310,80 @@
)
}
}
+
+struct UncategorizedDropConfirmBubble: View {
+ let title: String
+ let message: String
+ let cancelTitle: String
+ let confirmTitle: String
+ let onCancel: () -> Void
+ let onConfirm: () -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 18) {
+ HStack(alignment: .top, spacing: 12) {
+ Text(title)
+ .font(.system(size: 22, weight: .bold))
+ .foregroundStyle(.white)
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ Button(action: onCancel) {
+ Image(systemName: "xmark")
+ .font(.system(size: 12, weight: .bold))
+ .foregroundStyle(.white.opacity(0.82))
+ .frame(width: 26, height: 26)
+ .background(
+ Circle()
+ .fill(Color.white.opacity(0.12))
+ )
+ }
+ .buttonStyle(.plain)
+ }
+
+ Text(message)
+ .font(.system(size: 15, weight: .medium))
+ .lineSpacing(4)
+ .foregroundStyle(.white.opacity(0.84))
+ .multilineTextAlignment(.leading)
+ .fixedSize(horizontal: false, vertical: true)
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ HStack(spacing: 12) {
+ Spacer(minLength: 0)
+
+ Button(action: onCancel) {
+ Text(cancelTitle)
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(.white.opacity(0.86))
+ .frame(width: 92, height: 32)
+ .background(
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
+ .fill(Color.white.opacity(0.13))
+ )
+ }
+ .buttonStyle(.plain)
+
+ Button(action: onConfirm) {
+ Text(confirmTitle)
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(.white)
+ .frame(width: 92, height: 32)
+ .background(
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
+ .fill(Color.accentColor)
+ )
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(.horizontal, 24)
+ .padding(.vertical, 20)
+ .background(
+ RoundedRectangle(cornerRadius: 18, style: .continuous)
+ .fill(Color.black.opacity(0.92))
+ .shadow(color: .black.opacity(0.34), radius: 24, y: 16)
+ .shadow(color: .black.opacity(0.18), radius: 8, y: 3)
+ )
+ .onExitCommand(perform: onCancel)
+ }
+}
diff --git a/Apptag/Info.plist b/Apptag/Info.plist
index 206e0a3..22223b0 100644
--- a/Apptag/Info.plist
+++ b/Apptag/Info.plist
@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>7.3.0</string>
+ <string>7.3.1</string>
<key>CFBundleVersion</key>
- <string>739</string>
+ <string>740</string>
<key>LSMinimumSystemVersion</key>
<string>15.0</string>
<key>NSHighResolutionCapable</key>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57c95aa..25f762d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Apptag Changelog
+## [7.3.1] — 2026-05-18
+
+- 将拖动 App 到「未分类」时的确认提示从系统弹窗改为应用列表内部浮层,视觉样式与现有确认反馈保持一致
+- 未分类确认浮层现在只覆盖 TagLauncher 应用列表窗口,提供「取消 / 确认」两个明确操作,并支持右上角关闭与 Esc 取消
+- 版本号更新为 `7.3.1`,Build 更新为 `740`
+
## [7.3.0] — 2026-05-18
- 修复 Apple 系统自带 App 被拖入其他标签后从「Mac自带」分组消失的问题,「Mac自带」现在作为客观属性始终保留
--
Gitblit v1.9.3