Procházet zdrojové kódy

Add panel-style icon layout toggle for launcher.

Implement an alternate panel view from the top layout button, including left-side app rows, right-side circular action icons, and quick actions menu controls in the search header.

Made-with: Cursor
huzaifahayat12 před 4 měsíci
rodič
revize
0f1118976e
1 změnil soubory, kde provedl 153 přidání a 11 odebrání
  1. 153 11
      google_apps/LauncherRootView.swift

+ 153 - 11
google_apps/LauncherRootView.swift

@@ -2,12 +2,20 @@ import SwiftUI
 import AppKit
 
 struct LauncherRootView: View {
+    enum LayoutMode {
+        case grid
+        case panel
+    }
+
     @State private var query = ""
     @State private var selectedApp: LauncherApp?
     @State private var showingPremiumScreen = false
+    @State private var layoutMode: LayoutMode = .grid
 
     private let apps = LauncherApp.sampleApps
-    private let columns = [GridItem(.adaptive(minimum: 120, maximum: 145), spacing: 22)]
+    private var columns: [GridItem] {
+        [GridItem(.adaptive(minimum: 120, maximum: 145), spacing: 22)]
+    }
 
     private var filteredApps: [LauncherApp] {
         let q = query.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -24,7 +32,21 @@ struct LauncherRootView: View {
             .ignoresSafeArea()
 
             VStack(spacing: 16) {
-                SearchHeader(query: $query)
+                SearchHeader(
+                    query: $query,
+                    isPanelMode: layoutMode == .panel,
+                    onToggleLayout: {
+                        layoutMode = layoutMode == .grid ? .panel : .grid
+                    },
+                    onOpenPremium: {
+                        showingPremiumScreen = true
+                    },
+                    onOpenGoogle: {
+                        if let url = URL(string: "https://www.google.com") {
+                            NSWorkspace.shared.open(url)
+                        }
+                    }
+                )
                     .padding(.top, 12)
 
                 PromoBanner(
@@ -41,16 +63,22 @@ struct LauncherRootView: View {
                     .foregroundStyle(.white.opacity(0.85))
                     .frame(maxWidth: .infinity, maxHeight: .infinity)
                 } else {
-                    ScrollView {
-                        LazyVGrid(columns: columns, spacing: 20) {
-                            ForEach(filteredApps) { app in
-                                AppTileView(app: app) {
-                                    handleAppTap(app)
+                    if layoutMode == .panel {
+                        IconPanelView(apps: filteredApps) { app in
+                            handleAppTap(app)
+                        }
+                    } else {
+                        ScrollView {
+                            LazyVGrid(columns: columns, spacing: 20) {
+                                ForEach(filteredApps) { app in
+                                    AppTileView(app: app) {
+                                        handleAppTap(app)
+                                    }
                                 }
                             }
+                            .padding(.horizontal, 14)
+                            .padding(.bottom, 18)
                         }
-                        .padding(.horizontal, 14)
-                        .padding(.bottom, 18)
                     }
                 }
             }
@@ -79,6 +107,10 @@ struct LauncherRootView: View {
 
 private struct SearchHeader: View {
     @Binding var query: String
+    let isPanelMode: Bool
+    let onToggleLayout: () -> Void
+    let onOpenPremium: () -> Void
+    let onOpenGoogle: () -> Void
 
     var body: some View {
         HStack {
@@ -103,8 +135,25 @@ private struct SearchHeader: View {
             Spacer(minLength: 8)
 
             HStack(spacing: 12) {
-                Image(systemName: "list.bullet")
-                Image(systemName: "ellipsis")
+                Button(action: onToggleLayout) {
+                    Image(systemName: isPanelMode ? "square.grid.2x2" : "list.bullet")
+                }
+                .buttonStyle(.plain)
+
+                Menu {
+                    Button("Clear Search") {
+                        query = ""
+                    }
+                    Button("Open Premium") {
+                        onOpenPremium()
+                    }
+                    Button("Open Google") {
+                        onOpenGoogle()
+                    }
+                } label: {
+                    Image(systemName: "ellipsis")
+                }
+                .menuStyle(.borderlessButton)
             }
             .font(.system(size: 15, weight: .semibold))
             .foregroundStyle(.white.opacity(0.82))
@@ -113,6 +162,99 @@ private struct SearchHeader: View {
     }
 }
 
+private struct IconPanelView: View {
+    let apps: [LauncherApp]
+    let onAppTap: (LauncherApp) -> Void
+
+    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(Array(apps.prefix(10))) { app in
+                        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)
+                            }
+                            .padding(.vertical, 3)
+                        }
+                        .buttonStyle(.plain)
+                    }
+                    Spacer(minLength: 0)
+                }
+                .frame(width: 320, alignment: .topLeading)
+
+                VStack(alignment: .trailing, spacing: 14) {
+                    ForEach(Array(apps.prefix(10).enumerated()), id: \.offset) { index, _ in
+                        HStack(spacing: 12) {
+                            ForEach(0..<8, id: \.self) { col in
+                                let symbol = actions[(index * 3 + col) % actions.count]
+                                ActionBubble(symbol: symbol, highlighted: col == 0 && index % 3 == 0)
+                            }
+                            Image(systemName: "info.circle")
+                                .foregroundStyle(.white.opacity(0.6))
+                                .font(.system(size: 17))
+                                .frame(width: 20)
+                        }
+                    }
+                }
+                .frame(maxWidth: .infinity, alignment: .topTrailing)
+            }
+            .padding(.horizontal, 10)
+            .padding(.bottom, 18)
+        }
+    }
+}
+
+private struct AppIconGlyph: View {
+    let app: LauncherApp
+
+    var body: some View {
+        ZStack {
+            RoundedRectangle(cornerRadius: 12, style: .continuous)
+                .fill(Color.white.opacity(0.08))
+                .frame(width: 48, height: 48)
+
+            if let icon = NSImage(named: app.assetIconName) {
+                Image(nsImage: icon)
+                    .resizable()
+                    .scaledToFit()
+                    .frame(width: 32, height: 32)
+            } else {
+                Image(systemName: app.fallbackSymbolName)
+                    .font(.system(size: 20, weight: .semibold))
+                    .foregroundStyle(.white)
+            }
+        }
+    }
+}
+
+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: 15, weight: .medium))
+            )
+            .frame(width: 42, height: 42)
+    }
+}
+
 private struct PromoBanner: View {
     let onCloseTap: () -> Void
     let onUpgradeTap: () -> Void