From ce804d58ae308981453c3ecdc9c3c2f14687d598 Mon Sep 17 00:00:00 2001
From: Ariver <ar@MacBook-Air.local>
Date: Mon, 11 May 2026 13:14:12 +0800
Subject: [PATCH] Prepare App Store sandbox build

---
 Apptag/ApptagApp.swift          |   27 ++++++--
 Apptag/ContentView.swift        |   89 +++++++++++++++++++----------
 Apptag/TagLauncher.entitlements |    8 ++
 build.sh                        |    4 +
 4 files changed, 91 insertions(+), 37 deletions(-)

diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index ff4466d..9698519 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -94,6 +94,9 @@
     // MARK: - Launch at Login (LaunchAgent, zero permissions)
 
     private static let launchAgentLabel = "com.apptag.launcher"
+    static var supportsLaunchAtLogin: Bool {
+        ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] == nil
+    }
 
     private static var launchAgentURL: URL {
         FileManager.default.homeDirectoryForCurrentUser
@@ -101,6 +104,7 @@
     }
 
     static func enableLaunchAtLogin() {
+        guard supportsLaunchAtLogin else { return }
         let plist: [String: Any] = [
             "Label": Self.launchAgentLabel,
             "ProgramArguments": ["open", Bundle.main.bundlePath, "--hide"],
@@ -118,6 +122,7 @@
     }
 
     static func disableLaunchAtLogin() {
+        guard supportsLaunchAtLogin else { return }
         let uid = getuid()
         let task = Process()
         task.launchPath = "/bin/launchctl"
@@ -129,6 +134,12 @@
     /// On first launch, enable login item by default via LaunchAgent.
     /// Does NOT require App Management permission.
     private func setupLaunchAtLogin() {
+        guard Self.supportsLaunchAtLogin else {
+            if UserDefaults.standard.object(forKey: "launchAtLogin") == nil {
+                UserDefaults.standard.set(false, forKey: "launchAtLogin")
+            }
+            return
+        }
         let key = "launchAtLogin"
         if UserDefaults.standard.object(forKey: key) == nil {
             UserDefaults.standard.set(true, forKey: key)
@@ -635,14 +646,16 @@
             VStack(alignment: .leading, spacing: 0) {
                 // Toggle row — centered as a rectangular block
                 HStack(spacing: 20) {
-                    Toggle(tr("settings.launchAtLogin"), isOn: $launchAtLogin)
-                        .onChange(of: launchAtLogin) { _, enabled in
-                            if enabled {
-                                AppDelegate.enableLaunchAtLogin()
-                            } else {
-                                AppDelegate.disableLaunchAtLogin()
+                    if AppDelegate.supportsLaunchAtLogin {
+                        Toggle(tr("settings.launchAtLogin"), isOn: $launchAtLogin)
+                            .onChange(of: launchAtLogin) { _, enabled in
+                                if enabled {
+                                    AppDelegate.enableLaunchAtLogin()
+                                } else {
+                                    AppDelegate.disableLaunchAtLogin()
+                                }
                             }
-                        }
+                    }
                     Toggle(tr("settings.showInDock"), isOn: $showDockIcon)
                     Toggle(tr("settings.hideAppNames"), isOn: $hideAppNames)
                 }
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index c3eb159..843f892 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -204,6 +204,7 @@
     @State private var successToast: String? = nil
     @State private var draggedTagNames: [String] = []  // live drag order
     @State private var dragItem: String? = nil          // currently dragged tag
+    @State private var tagReorderFrames: [String: CGRect] = [:]
     @State private var hoveredContainer: String? = nil  // colored container lift
     // Fixed interaction for "Colorless Container": hover fills persistently; click clears.
     @State private var filledColorlessContainer: String? = nil
@@ -931,27 +932,27 @@
                         VStack(spacing: 4) {
                             ForEach(sortedTagNames, id: \.self) { tagName in
                                 selectableTagItem(tagName)
-                                    .onDrag {
-                                        dragItem = tagName
-                                        return NSItemProvider(object: tagName as NSString)
-                                    }
-                                    .onDrop(of: [.text], isTargeted: nil) { providers, _ in
-                                        guard let fromName = dragItem,
-                                              var names = draggedTagNames as [String]?,
-                                              let fromIdx = names.firstIndex(of: fromName),
-                                              let toIdx = names.firstIndex(of: tagName),
-                                              fromIdx != toIdx
-                                        else { return false }
-                                        let toOffset = toIdx > fromIdx ? toIdx + 1 : toIdx
-                                        names.move(fromOffsets: [fromIdx], toOffset: toOffset)
-                                        draggedTagNames = names
-                                        TagEditor.reorderTags(names)
-                                        dragItem = nil
-                                        return true
-                                    }
+                                    .opacity(dragItem == tagName ? 0.72 : 1.0)
+                                    .scaleEffect(dragItem == tagName ? 1.03 : 1.0)
+                                    .background(
+                                        GeometryReader { proxy in
+                                            Color.clear.preference(
+                                                key: TagReorderFramePreferenceKey.self,
+                                                value: [tagName: proxy.frame(in: .named("tagReorderList"))]
+                                            )
+                                        }
+                                    )
+                                    .zIndex(dragItem == tagName ? 1 : 0)
+                                    .highPriorityGesture(tagReorderGesture(for: tagName))
                             }
-                        }.padding(12)
-                    }.frame(width: 155)
+                        }
+                        .padding(12)
+                        .coordinateSpace(name: "tagReorderList")
+                        .onPreferenceChange(TagReorderFramePreferenceKey.self) { frames in
+                            tagReorderFrames = frames
+                        }
+                    }
+                    .frame(width: 155)
                 }
                 Rectangle().fill(.secondary.opacity(0.12)).frame(width: 1)
 
@@ -1033,6 +1034,36 @@
         .onTapGesture {
             if isSelected { selectedTagNames.remove(tagName) } else { selectedTagNames.insert(tagName) }
         }
+    }
+
+    private func tagReorderGesture(for tagName: String) -> some Gesture {
+        DragGesture(minimumDistance: 4, coordinateSpace: .named("tagReorderList"))
+            .onChanged { value in
+                if dragItem == nil {
+                    dragItem = tagName
+                }
+                guard dragItem == tagName else { return }
+                reorderDraggedTag(at: value.location)
+            }
+            .onEnded { _ in
+                dragItem = nil
+                TagEditor.reorderTags(draggedTagNames)
+            }
+    }
+
+    private func reorderDraggedTag(at location: CGPoint) {
+        guard let fromName = dragItem,
+              let targetName = tagReorderFrames.first(where: { $0.value.contains(location) })?.key,
+              fromName != targetName,
+              let fromIndex = draggedTagNames.firstIndex(of: fromName),
+              let toIndex = draggedTagNames.firstIndex(of: targetName)
+        else { return }
+
+        withAnimation(.easeInOut(duration: 0.14)) {
+            let destination = toIndex > fromIndex ? toIndex + 1 : toIndex
+            draggedTagNames.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: destination)
+        }
+        TagEditor.reorderTags(draggedTagNames)
     }
 
     private func confirmAssign() {
@@ -1269,16 +1300,6 @@
     func openApp(_ app: AppInfo) {
         appDragModeActive = false
         hideOverlay()
-        if let bundleIdentifier = app.bundleIdentifier {
-            NSWorkspace.shared.launchApplication(
-                withBundleIdentifier: bundleIdentifier,
-                options: [],
-                additionalEventParamDescriptor: nil,
-                launchIdentifier: nil
-            )
-            return
-        }
-
         let configuration = NSWorkspace.OpenConfiguration()
         NSWorkspace.shared.openApplication(at: app.path, configuration: configuration)
     }
@@ -1286,6 +1307,14 @@
 
 // MARK: - Tag Label
 
+private struct TagReorderFramePreferenceKey: PreferenceKey {
+    static var defaultValue: [String: CGRect] = [:]
+
+    static func reduce(value: inout [String: CGRect], nextValue: () -> [String: CGRect]) {
+        value.merge(nextValue(), uniquingKeysWith: { _, new in new })
+    }
+}
+
 private struct TagLabel: Identifiable {
     var id: String { name }
     let name: String
diff --git a/Apptag/TagLauncher.entitlements b/Apptag/TagLauncher.entitlements
new file mode 100644
index 0000000..852fa1a
--- /dev/null
+++ b/Apptag/TagLauncher.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.app-sandbox</key>
+	<true/>
+</dict>
+</plist>
diff --git a/build.sh b/build.sh
index 3764c3c..9ea9bdf 100644
--- a/build.sh
+++ b/build.sh
@@ -24,6 +24,10 @@
     if [ -z "$CODESIGN_IDENTITY" ]; then
         echo "⚠️  APP_STORE=1 but CODESIGN_IDENTITY not set. Will ad-hoc sign with entitlements."
     fi
+    if [ ! -f "$ENTITLEMENTS" ]; then
+        echo "❌ APP_STORE=1 requires entitlements at: $ENTITLEMENTS"
+        exit 1
+    fi
 fi
 
 echo "==> Cleaning..."

--
Gitblit v1.9.3