From 667a89382078d7b38e6dc21c5627839fbde7f92c Mon Sep 17 00:00:00 2001
From: Ariver <ar@MacBook-Air.local>
Date: Mon, 11 May 2026 11:48:46 +0800
Subject: [PATCH] Archive TagLauncher 5.1.12

---
 Apptag/AppGridItem.swift |  236 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 210 insertions(+), 26 deletions(-)

diff --git a/Apptag/AppGridItem.swift b/Apptag/AppGridItem.swift
index b3b8141..f25f223 100644
--- a/Apptag/AppGridItem.swift
+++ b/Apptag/AppGridItem.swift
@@ -7,39 +7,223 @@
     let app: AppInfo
     let iconSize: CGFloat
     var showName: Bool = true
+    var sourceTag: String? = nil
+    var dragModeActive: Bool = false
+    var onDragModeChange: ((Bool) -> Void)? = nil
     let onSelect: () -> Void
 
     @State private var isHovered = false
+    @State private var wiggle = false
+
+    private let hoverScale: CGFloat = 1.22
 
     var body: some View {
-        Button(action: onSelect) {
-            VStack(spacing: 6) {
-                Image(nsImage: app.icon)
-                    .resizable()
-                    .aspectRatio(contentMode: .fit)
-                    .frame(width: iconSize, height: iconSize)
-                    .scaleEffect(isHovered ? 1.22 : 1.0)
-                    .shadow(
-                        color: .black.opacity(isHovered ? 0.35 : 0),
-                        radius: isHovered ? 14 : 0,
-                        y: isHovered ? 8 : 0
-                    )
-                    .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isHovered)
+        VStack(spacing: 6) {
+            DraggableAppIconView(
+                icon: app.icon,
+                iconSize: iconSize,
+                payload: "\(app.path.path)\n\(sourceTag ?? "")",
+                onHover: { isHovered = $0 },
+                onLongPress: { onDragModeChange?(true) },
+                onDragEnd: { onDragModeChange?(false) },
+                onClick: onSelect
+            )
+            .frame(width: iconSize, height: iconSize)
+            .scaleEffect(isHovered ? hoverScale : 1.0)
+            .shadow(
+                color: .black.opacity(isHovered ? 0.35 : 0),
+                radius: isHovered ? 14 : 0,
+                y: isHovered ? 8 : 0
+            )
+            .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isHovered)
 
-                Text(app.name)
-                    .font(.system(size: 11, weight: .medium))
-                    .lineLimit(1)
-                    .truncationMode(.tail)
-                    .frame(maxWidth: iconSize + 20)
-                    .opacity(showName ? 1 : (isHovered ? 0.85 : 0))
-            }
-            .padding(.vertical, 8)
-            .padding(.horizontal, 4)
-            .contentShape(RoundedRectangle(cornerRadius: 10))
+            Text(app.name)
+                .font(.system(size: 11, weight: .medium))
+                .lineLimit(1)
+                .truncationMode(.tail)
+                .frame(maxWidth: iconSize + 20)
+                .opacity(showName ? 1 : (isHovered ? 0.85 : 0))
         }
-        .buttonStyle(.plain)
-        .onHover { hovering in
-            isHovered = hovering
+        .padding(.vertical, 8)
+        .padding(.horizontal, 4)
+        .contentShape(RoundedRectangle(cornerRadius: 10))
+        .rotationEffect(.degrees(dragModeActive ? (wiggle ? 2.0 : -2.0) : 0))
+        .animation(
+            dragModeActive
+                ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true)
+                : .default,
+            value: wiggle
+        )
+        .onChange(of: dragModeActive) { _, active in
+            wiggle = active
         }
     }
 }
+
+private struct DraggableAppIconView: NSViewRepresentable {
+    let icon: NSImage
+    let iconSize: CGFloat
+    let payload: String
+    let onHover: (Bool) -> Void
+    let onLongPress: () -> Void
+    let onDragEnd: () -> Void
+    let onClick: () -> Void
+
+    func makeNSView(context: Context) -> DragIconNSView {
+        let view = DragIconNSView()
+        view.image = icon
+        view.iconSize = iconSize
+        view.payload = payload
+        view.onHover = onHover
+        view.onLongPress = onLongPress
+        view.onDragEnd = onDragEnd
+        view.onClick = onClick
+        return view
+    }
+
+    func updateNSView(_ view: DragIconNSView, context: Context) {
+        view.image = icon
+        view.iconSize = iconSize
+        view.payload = payload
+        view.onHover = onHover
+        view.onLongPress = onLongPress
+        view.onDragEnd = onDragEnd
+        view.onClick = onClick
+        view.needsDisplay = true
+    }
+}
+
+private final class DragIconNSView: NSView {
+    var image: NSImage = NSImage()
+    var iconSize: CGFloat = 56
+    var payload: String = ""
+    var onHover: ((Bool) -> Void)?
+    var onLongPress: (() -> Void)?
+    var onDragEnd: (() -> Void)?
+    var onClick: (() -> Void)?
+
+    private var mouseDownEvent: NSEvent?
+    private var didStartDrag = false
+    private var isLongPressActive = false
+    private var longPressWorkItem: DispatchWorkItem?
+    private var trackingAreaRef: NSTrackingArea?
+
+    override var isFlipped: Bool { true }
+
+    override func updateTrackingAreas() {
+        super.updateTrackingAreas()
+        if let trackingAreaRef {
+            removeTrackingArea(trackingAreaRef)
+        }
+        let area = NSTrackingArea(
+            rect: bounds,
+            options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect],
+            owner: self,
+            userInfo: nil
+        )
+        trackingAreaRef = area
+        addTrackingArea(area)
+    }
+
+    override func draw(_ dirtyRect: NSRect) {
+        super.draw(dirtyRect)
+        let rect = NSRect(
+            x: (bounds.width - iconSize) / 2,
+            y: (bounds.height - iconSize) / 2,
+            width: iconSize,
+            height: iconSize
+        )
+        image.draw(in: rect)
+    }
+
+    override func mouseEntered(with event: NSEvent) {
+        onHover?(true)
+    }
+
+    override func mouseDown(with event: NSEvent) {
+        mouseDownEvent = event
+        didStartDrag = false
+        isLongPressActive = false
+        let workItem = DispatchWorkItem { [weak self] in
+            guard let self, self.mouseDownEvent != nil else { return }
+            self.isLongPressActive = true
+            self.onLongPress?()
+        }
+        longPressWorkItem = workItem
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)
+    }
+
+    override func mouseDragged(with event: NSEvent) {
+        if didStartDrag {
+            AppDragCoordinator.shared.updateDragLocation(
+                screenPoint(for: event),
+                copy: event.modifierFlags.contains(.option)
+            )
+            return
+        }
+
+        guard let mouseDownEvent, isLongPressActive else { return }
+        let dx = event.locationInWindow.x - mouseDownEvent.locationInWindow.x
+        let dy = event.locationInWindow.y - mouseDownEvent.locationInWindow.y
+        guard hypot(dx, dy) > 3 else { return }
+
+        didStartDrag = true
+        longPressWorkItem?.cancel()
+        AppDragCoordinator.shared.beginDrag(
+            image: makeDragImage(),
+            payload: payload,
+            at: screenPoint(for: event),
+            copy: event.modifierFlags.contains(.option),
+            in: window
+        )
+    }
+
+    override func mouseUp(with event: NSEvent) {
+        longPressWorkItem?.cancel()
+        if didStartDrag {
+            AppDragCoordinator.shared.finishDrag(
+                at: screenPoint(for: event),
+                copy: event.modifierFlags.contains(.option)
+            )
+            onDragEnd?()
+        } else if !isLongPressActive {
+            onClick?()
+        } else {
+            onDragEnd?()
+        }
+        didStartDrag = false
+        isLongPressActive = false
+        mouseDownEvent = nil
+    }
+
+    override func mouseExited(with event: NSEvent) {
+        if !didStartDrag {
+            onHover?(false)
+        }
+    }
+
+    private func makeDragImage() -> NSImage {
+        let scale: CGFloat = 1.22 * 1.5
+        let imageSize = iconSize * scale
+        let padding = iconSize * 0.45
+        let canvasSize = NSSize(width: imageSize + padding * 2, height: imageSize + padding * 2)
+        let dragImage = NSImage(size: canvasSize)
+
+        dragImage.lockFocus()
+        NSGraphicsContext.current?.imageInterpolation = .high
+
+        let shadow = NSShadow()
+        shadow.shadowColor = NSColor.black.withAlphaComponent(0.48)
+        shadow.shadowBlurRadius = 24
+        shadow.shadowOffset = NSSize(width: 0, height: -14)
+        shadow.set()
+
+        image.draw(in: NSRect(x: padding, y: padding, width: imageSize, height: imageSize))
+        dragImage.unlockFocus()
+        return dragImage
+    }
+
+    private func screenPoint(for event: NSEvent) -> NSPoint {
+        window?.convertPoint(toScreen: event.locationInWindow) ?? NSEvent.mouseLocation
+    }
+}

--
Gitblit v1.9.3