Ariver
2026-05-18 a5cba8fa3153098d95acb20b6060fac704ecc489
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import SwiftUI
import UniformTypeIdentifiers
 
// MARK: - Tag Group Section (with centered separator-line header)
 
struct TagGroupView: View {
    let group: TagGroup
    let onSelectApp: (AppInfo) -> Void
    let tagFontSize: CGFloat
    let iconSize: CGFloat
    var showNames: Bool = true
    var dragModeActive: Bool = false
    var onDragModeChange: ((Bool) -> Void)? = nil
    var onBubbleHover: ((AppInfo, CGRect, Bool) -> Void)? = nil
    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
    @Binding var hoveredAppItemID: String?
    var onDropApp: ((String, String, Bool) -> Void)? = nil
 
    /// Adaptive columns — auto-fit based on icon size and available width.
    private var columns: [GridItem] {
        let itemWidth = AppGridItem.stableWidth(iconSize: iconSize)
        return [GridItem(.adaptive(minimum: itemWidth, maximum: itemWidth + 36), spacing: 6)]
    }
 
    // Subtle separator line color — adapts to light/dark mode
    private var lineColor: Color {
        .secondary.opacity(0.25)
    }
 
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            // Centered separator line with tag name
            HStack(spacing: 0) {
                Rectangle()
                    .fill(lineColor)
                    .frame(height: 1)
 
                Text(group.name)
                    .font(.system(size: tagFontSize, weight: .semibold))
                    .foregroundStyle(.secondary)
                    .padding(.horizontal, 10)
 
                Rectangle()
                    .fill(lineColor)
                    .frame(height: 1)
            }
            .padding(.bottom, 6)
 
            // App icon grid — columns auto-adjust to icon size
            LazyVGrid(columns: columns, spacing: 2) {
                ForEach(group.apps) { app in
                    AppGridItem(
                        app: app,
                        iconSize: iconSize,
                        showName: showNames,
                        sourceTag: group.name,
                        dragModeActive: dragModeActive,
                        onDragModeChange: onDragModeChange,
                        onBubbleHover: onBubbleHover,
                        onEditNote: onEditNote,
                        itemID: "\(group.name)|\(app.path.path)",
                        hoveredAppItemID: $hoveredAppItemID,
                        onSelect: { onSelectApp(app) }
                    )
                }
            }
        }
        .contentShape(Rectangle())
        .overlay {
            AppDropTargetView(targetTag: group.name) { path, source, copy in
                onDropApp?(path, source, copy)
            }
            .allowsHitTesting(false)
        }
        .onDrop(of: [UTType.plainText], isTargeted: nil) { providers in
            handleDrop(providers)
        }
    }
 
    private func handleDrop(_ providers: [NSItemProvider]) -> Bool {
        guard let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) }) else {
            return false
        }
        provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in
            let text: String?
            if let data = item as? Data {
                text = String(data: data, encoding: .utf8)
            } else if let string = item as? String {
                text = string
            } else if let string = item as? NSString {
                text = string as String
            } else {
                text = nil
            }
            guard let text else { return }
            let parts = text.components(separatedBy: "\n")
            guard let path = parts.first, !path.isEmpty else { return }
            let source = parts.dropFirst().first ?? ""
            let copy = NSEvent.modifierFlags.contains(.option)
            DispatchQueue.main.async {
                onDropApp?(path, source, copy)
            }
        }
        return true
    }
}