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