Ver código fonte

Align panel-mode app actions with grid tiles

Panel mode now uses the same 4 actions (Pin/Unpin, Add to StatusBar, Add to desktop, Add Widget) instead of the extra bubble icon cluster.

Made-with: Cursor
huzaifahayat12 4 meses atrás
pai
commit
73019935a3
1 arquivos alterados com 179 adições e 93 exclusões
  1. 179 93
      google_apps/LauncherRootView.swift

+ 179 - 93
google_apps/LauncherRootView.swift

@@ -416,97 +416,134 @@ private struct IconPanelView: View {
     @Binding var draggedAppID: UUID?
     @Binding var lastDropDestinationAppID: UUID?
     let onMove: (UUID, UUID) -> Void
-    @AppStorage("pinnedTileIDsData") private var pinnedTileIDsData = ""
-
-    private let actions = [
-        "house", "magnifyingglass", "target", "map", "heart", "person.2", "clock",
-        "star", "exclamationmark.circle", "trash", "paperplane", "doc", "lock",
-    ]
 
     var body: some View {
         ScrollView {
-            HStack(alignment: .top, spacing: 20) {
-                VStack(alignment: .leading, spacing: 12) {
-                    ForEach(apps) { app in
-                        HStack(spacing: 14) {
-                            // Tap-to-open area (kept separate from the pin menu so
-                            // clicking "Pin/Unpin" doesn't also trigger open).
-                            HStack(spacing: 14) {
-                                AppIconGlyph(app: app)
-                                Text(app.name)
-                                    .font(.system(size: 18, weight: .medium))
-                                    .foregroundStyle(.white.opacity(0.94))
-                                    .lineLimit(1)
-                                    .truncationMode(.tail)
-                            }
-                            .contentShape(Rectangle())
-                            .onTapGesture { onAppTap(app) }
-
-                            Spacer(minLength: 0)
-                        }
-                        .padding(.vertical, 3)
-                        .onDrag {
-                            // Used by the reorder drop delegate.
-                            draggedAppID = app.id
-                            lastDropDestinationAppID = nil
-                            return NSItemProvider(object: app.id.uuidString as NSString)
-                        }
-                        .onDrop(
-                            of: [UTType.text],
-                            delegate: TileReorderDropDelegate(
-                                destinationAppID: app.id,
-                                draggedAppID: $draggedAppID,
-                                lastDropDestinationAppID: $lastDropDestinationAppID,
-                                onMove: onMove
-                            )
-                        )
-                    }
-                    Spacer(minLength: 0)
-                }
-                .frame(width: 320, alignment: .topLeading)
-
-                VStack(alignment: .trailing, spacing: 14) {
-                    ForEach(Array(apps.enumerated()), id: \.element.id) { index, _ in
-                        HStack(spacing: 10) {
-                            // Reduce the number of action icons and add a working pin/unpin button.
-                            // Layout: [pin/unpin] + 6 action bubbles
-                            ForEach(0..<7, id: \.self) { col in
-                                let app = apps[index]
-                                if col == 0 {
-                                    let isPinned = pinnedIDs.contains(app.id)
-                                    Button(action: { togglePin(app) }) {
-                                        ActionBubble(
-                                            symbol: isPinned ? "pin.fill" : "pin",
-                                            highlighted: isPinned
-                                        )
-                                    }
-                                    .buttonStyle(.plain)
-                                } else {
-                                    let actionCol = col - 1
-                                    let symbol = actions[(index * 3 + actionCol) % actions.count]
-                                    let highlighted = actionCol == 0 && index % 3 == 0
-                                    ActionBubble(symbol: symbol, highlighted: highlighted)
-                                }
-                            }
-                            Image(systemName: "info.circle")
-                                .foregroundStyle(.white.opacity(0.6))
-                                .font(.system(size: 14))
-                                .frame(width: 20)
-                        }
-                    }
+            VStack(alignment: .leading, spacing: 12) {
+                ForEach(apps) { app in
+                    PanelAppRowView(
+                        app: app,
+                        onAppTap: onAppTap,
+                        draggedAppID: $draggedAppID,
+                        lastDropDestinationAppID: $lastDropDestinationAppID,
+                        onMove: onMove
+                    )
                 }
-                .frame(maxWidth: .infinity, alignment: .topTrailing)
             }
             .padding(.horizontal, 10)
             .padding(.bottom, 18)
         }
     }
+}
+
+private struct PanelAppRowView: View {
+    let app: LauncherApp
+    let onAppTap: (LauncherApp) -> Void
+    @Binding var draggedAppID: UUID?
+    @Binding var lastDropDestinationAppID: UUID?
+    let onMove: (UUID, UUID) -> Void
+
+    @AppStorage("pinnedTileIDsData") private var pinnedTileIDsData = ""
+    @AppStorage("statusBarAppIDsData") private var statusBarAppIDsData = ""
+    @AppStorage("widgetAppIDsData") private var widgetAppIDsData = ""
+
+    @State private var isHovering = false
 
     private var pinnedIDs: Set<UUID> {
         decodeUUIDSetGlobal(from: pinnedTileIDsData)
     }
 
-    private func togglePin(_ app: LauncherApp) {
+    private var isPinned: Bool {
+        pinnedIDs.contains(app.id)
+    }
+
+    var body: some View {
+        ZStack(alignment: .topTrailing) {
+            Button(action: { onAppTap(app) }) {
+                HStack(spacing: 14) {
+                    AppIconGlyph(app: app)
+                    Text(app.name)
+                        .font(.system(size: 18, weight: .medium))
+                        .foregroundStyle(.white.opacity(0.94))
+                        .lineLimit(1)
+                        .truncationMode(.tail)
+                    Spacer(minLength: 0)
+                }
+                .frame(maxWidth: .infinity, alignment: .leading)
+                .contentShape(Rectangle())
+            }
+            .buttonStyle(.plain)
+
+            if isPinned && !app.isCreateNew {
+                Image(systemName: "pin.fill")
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.92))
+                    .frame(width: 26, height: 26)
+                    .background(
+                        Circle()
+                            .fill(Color.black.opacity(isHovering ? 0.35 : 0.2))
+                    )
+                    .overlay(
+                        Circle()
+                            .stroke(Color.white.opacity(isHovering ? 0.18 : 0.12), lineWidth: 1)
+                    )
+                    .shadow(color: .black.opacity(0.22), radius: 10, x: 0, y: 6)
+                    .padding(8)
+                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+                    .allowsHitTesting(false)
+            }
+
+            if !app.isCreateNew {
+                Menu {
+                    Button(isPinned ? "Unpin" : "Pin") { togglePin() }
+                    Divider()
+                    Button("Add to StatusBar") { addToStatusBar() }
+                    Button("Add to desktop") { addToDesktop() }
+                    Button("Add Widget") { addWidget() }
+                } label: {
+                    Image(systemName: "ellipsis")
+                        .font(.system(size: 13, weight: .semibold))
+                        .rotationEffect(.degrees(90))
+                        .foregroundStyle(.white.opacity(isHovering ? 0.92 : 0.75))
+                        .frame(width: 26, height: 26)
+                        .background(
+                            Circle()
+                                .fill(Color.black.opacity(isHovering ? 0.35 : 0.2))
+                        )
+                        .overlay(
+                            Circle()
+                                .stroke(Color.white.opacity(isHovering ? 0.18 : 0.12), lineWidth: 1)
+                        )
+                        .shadow(color: .black.opacity(0.25), radius: 10, x: 0, y: 6)
+                }
+                .menuStyle(.borderlessButton)
+                .modifier(HideMenuIndicatorIfAvailable())
+                .padding(8)
+                .opacity(isHovering ? 1 : 0.0)
+                .animation(.easeOut(duration: 0.12), value: isHovering)
+            }
+        }
+        .onHover { hovering in
+            isHovering = hovering
+        }
+        .padding(.vertical, 3)
+        .onDrag {
+            draggedAppID = app.id
+            lastDropDestinationAppID = nil
+            return NSItemProvider(object: app.id.uuidString as NSString)
+        }
+        .onDrop(
+            of: [UTType.text],
+            delegate: TileReorderDropDelegate(
+                destinationAppID: app.id,
+                draggedAppID: $draggedAppID,
+                lastDropDestinationAppID: $lastDropDestinationAppID,
+                onMove: onMove
+            )
+        )
+    }
+
+    private func togglePin() {
         var updated = pinnedIDs
         if updated.contains(app.id) {
             updated.remove(app.id)
@@ -515,6 +552,61 @@ private struct IconPanelView: View {
         }
         pinnedTileIDsData = encodeUUIDSetGlobal(updated)
     }
+
+    private func addToStatusBar() {
+        var updated = decodeUUIDSetGlobal(from: statusBarAppIDsData)
+        updated.insert(app.id)
+        statusBarAppIDsData = encodeUUIDSetGlobal(updated)
+        showAlert(title: "Added", message: "Added “\(app.name)” to Status Bar shortcuts.")
+    }
+
+    private func addWidget() {
+        var updated = decodeUUIDSetGlobal(from: widgetAppIDsData)
+        updated.insert(app.id)
+        widgetAppIDsData = encodeUUIDSetGlobal(updated)
+        showAlert(title: "Added", message: "Added “\(app.name)” to Widgets.")
+    }
+
+    private func addToDesktop() {
+        guard let url = app.webURL else {
+            showAlert(title: "Unavailable", message: "This tile doesn’t have a web link to create a desktop shortcut.")
+            return
+        }
+
+        guard let desktopURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first else {
+            showAlert(title: "Error", message: "Couldn’t find your Desktop folder.")
+            return
+        }
+
+        let fileName = sanitizedFileName(app.name).isEmpty ? "Shortcut" : sanitizedFileName(app.name)
+        let targetURL = desktopURL.appendingPathComponent("\(fileName).webloc")
+
+        let payload: [String: Any] = ["URL": url.absoluteString]
+        do {
+            let data = try PropertyListSerialization.data(fromPropertyList: payload, format: .xml, options: 0)
+            try data.write(to: targetURL, options: .atomic)
+            showAlert(title: "Created", message: "Desktop shortcut created: \(targetURL.lastPathComponent)")
+        } catch {
+            showAlert(title: "Error", message: "Couldn’t create the desktop shortcut.")
+        }
+    }
+
+    private func sanitizedFileName(_ input: String) -> String {
+        let invalid = CharacterSet(charactersIn: "/:\\?%*|\"<>")
+        let cleaned = input.components(separatedBy: invalid).joined(separator: " ")
+        return cleaned
+            .trimmingCharacters(in: .whitespacesAndNewlines)
+            .replacingOccurrences(of: "  +", with: " ", options: .regularExpression)
+    }
+
+    private func showAlert(title: String, message: String) {
+        let alert = NSAlert()
+        alert.messageText = title
+        alert.informativeText = message
+        alert.addButton(withTitle: "OK")
+        alert.alertStyle = .informational
+        alert.runModal()
+    }
 }
 
 private struct AppIconGlyph: View {
@@ -548,22 +640,6 @@ private struct AppIconGlyph: View {
     }
 }
 
-private struct ActionBubble: View {
-    let symbol: String
-    let highlighted: Bool
-
-    var body: some View {
-        Circle()
-            .fill(highlighted ? Color.cyan.opacity(0.22) : Color.white.opacity(0.08))
-            .overlay(
-                Image(systemName: symbol)
-                    .foregroundStyle(.white.opacity(0.9))
-                    .font(.system(size: 13, weight: .medium))
-            )
-            .frame(width: 34, height: 34)
-    }
-}
-
 // MARK: - Pinned app persistence helpers (panel mode)
 private func decodeUUIDSetGlobal(from dataString: String) -> Set<UUID> {
     guard !dataString.isEmpty, let data = dataString.data(using: .utf8) else { return [] }
@@ -631,6 +707,16 @@ private struct PromoBanner: View {
     }
 }
 
+private struct HideMenuIndicatorIfAvailable: ViewModifier {
+    func body(content: Content) -> some View {
+        if #available(macOS 13.0, *) {
+            content.menuIndicator(.hidden)
+        } else {
+            content
+        }
+    }
+}
+
 #Preview {
     LauncherRootView()
 }