Quellcode durchsuchen

Build Google-style SwiftUI launcher UI.

Replace the default AppKit content with a searchable launcher grid, app tile interactions, create-new tile support, and Gmail asset-catalog icon wiring so custom icons can be added by name.

Made-with: Cursor
huzaifahayat12 vor 3 Wochen
Ursprung
Commit
b76461a308

+ 120 - 0
google_apps/AppTileView.swift

@@ -0,0 +1,120 @@
1
+import SwiftUI
2
+import AppKit
3
+
4
+struct AppTileView: View {
5
+    let app: LauncherApp
6
+    let onTap: () -> Void
7
+
8
+    @State private var isHovering = false
9
+
10
+    var body: some View {
11
+        Button(action: onTap) {
12
+            VStack(spacing: 10) {
13
+                AppIconView(app: app, size: 66)
14
+                    .shadow(color: .black.opacity(0.22), radius: 9, x: 0, y: 6)
15
+
16
+                Text(app.name)
17
+                    .font(.system(size: 13.5, weight: .medium))
18
+                    .foregroundStyle(.white.opacity(0.95))
19
+                    .lineLimit(1)
20
+                    .truncationMode(.tail)
21
+                    .frame(maxWidth: .infinity)
22
+            }
23
+            .padding(.horizontal, 8)
24
+            .padding(.vertical, 10)
25
+            .background(
26
+                RoundedRectangle(cornerRadius: 14, style: .continuous)
27
+                    .fill(isHovering ? Color.white.opacity(0.10) : Color.clear)
28
+            )
29
+            .overlay(
30
+                RoundedRectangle(cornerRadius: 14, style: .continuous)
31
+                    .stroke(isHovering ? Color.white.opacity(0.2) : Color.clear, lineWidth: 1)
32
+            )
33
+            .animation(.easeOut(duration: 0.15), value: isHovering)
34
+        }
35
+        .buttonStyle(.plain)
36
+        .onHover { hovering in
37
+            isHovering = hovering
38
+        }
39
+    }
40
+}
41
+
42
+private struct AppIconView: View {
43
+    let app: LauncherApp
44
+    let size: CGFloat
45
+
46
+    var body: some View {
47
+        ZStack {
48
+            if app.isCreateNew {
49
+                RoundedRectangle(cornerRadius: 18, style: .continuous)
50
+                    .stroke(Color.white.opacity(0.35), lineWidth: 2)
51
+                    .background(
52
+                        RoundedRectangle(cornerRadius: 18, style: .continuous)
53
+                            .fill(Color.white.opacity(0.03))
54
+                    )
55
+                Image(systemName: "plus")
56
+                    .font(.system(size: size * 0.34, weight: .regular))
57
+                    .foregroundStyle(.white.opacity(0.9))
58
+            } else {
59
+                RoundedRectangle(cornerRadius: 18, style: .continuous)
60
+                    .fill(LinearGradient(colors: gradientColors, startPoint: .topLeading, endPoint: .bottomTrailing))
61
+
62
+                if let iconImage = NSImage(named: app.assetIconName) {
63
+                    Image(nsImage: iconImage)
64
+                        .resizable()
65
+                        .scaledToFit()
66
+                        .padding(size * 0.12)
67
+                } else {
68
+                    Image(systemName: app.fallbackSymbolName)
69
+                        .font(.system(size: size * 0.32, weight: .semibold))
70
+                        .foregroundStyle(.white)
71
+                }
72
+            }
73
+        }
74
+        .frame(width: size, height: size)
75
+    }
76
+
77
+    private var gradientColors: [Color] {
78
+        switch app.name {
79
+        case let name where name.contains("Gmail"):
80
+            return [Color.red.opacity(0.85), Color.orange.opacity(0.8)]
81
+        case let name where name.contains("Docs"):
82
+            return [Color.blue.opacity(0.9), Color.indigo.opacity(0.85)]
83
+        case let name where name.contains("Drive"):
84
+            return [Color.green.opacity(0.9), Color.blue.opacity(0.75)]
85
+        case let name where name.contains("YouTube"):
86
+            return [Color.red.opacity(0.95), Color.pink.opacity(0.75)]
87
+        case let name where name.contains("Maps"):
88
+            return [Color.green.opacity(0.85), Color.teal.opacity(0.75)]
89
+        default:
90
+            return [Color.blue.opacity(0.85), Color.cyan.opacity(0.7)]
91
+        }
92
+    }
93
+}
94
+
95
+struct AppDetailView: View {
96
+    let app: LauncherApp
97
+    @Environment(\.dismiss) private var dismiss
98
+
99
+    var body: some View {
100
+        ZStack {
101
+            Color(red: 0.09, green: 0.09, blue: 0.11)
102
+                .ignoresSafeArea()
103
+
104
+            VStack(spacing: 14) {
105
+                AppIconView(app: app, size: 76)
106
+                Text(app.name)
107
+                    .font(.system(size: 22, weight: .semibold))
108
+                    .foregroundStyle(.white)
109
+                Text(app.description)
110
+                    .font(.system(size: 14))
111
+                    .foregroundStyle(.white.opacity(0.82))
112
+                Button("Close") {
113
+                    dismiss()
114
+                }
115
+                .buttonStyle(.borderedProminent)
116
+            }
117
+            .padding(24)
118
+        }
119
+    }
120
+}

+ 21 - 0
google_apps/Assets.xcassets/icon_gmail.imageset/Contents.json

@@ -0,0 +1,21 @@
1
+{
2
+  "images" : [
3
+    {
4
+      "filename" : "icon_gmail.png",
5
+      "idiom" : "universal",
6
+      "scale" : "1x"
7
+    },
8
+    {
9
+      "idiom" : "universal",
10
+      "scale" : "2x"
11
+    },
12
+    {
13
+      "idiom" : "universal",
14
+      "scale" : "3x"
15
+    }
16
+  ],
17
+  "info" : {
18
+    "author" : "xcode",
19
+    "version" : 1
20
+  }
21
+}

BIN
google_apps/Assets.xcassets/icon_gmail.imageset/icon_gmail.png


+ 34 - 0
google_apps/LauncherApp.swift

@@ -0,0 +1,34 @@
1
+import Foundation
2
+
3
+struct LauncherApp: Identifiable, Hashable {
4
+    let id = UUID()
5
+    let name: String
6
+    let assetIconName: String
7
+    let fallbackSymbolName: String
8
+    let description: String
9
+    let isCreateNew: Bool
10
+}
11
+
12
+extension LauncherApp {
13
+    static let sampleApps: [LauncherApp] = [
14
+        LauncherApp(name: "Google Shopping", assetIconName: "icon_google_shopping", fallbackSymbolName: "bag.fill", description: "Explore products and offers.", isCreateNew: false),
15
+        LauncherApp(name: "Gmail", assetIconName: "icon_gmail", fallbackSymbolName: "envelope.fill", description: "Read and send emails quickly.", isCreateNew: false),
16
+        LauncherApp(name: "Google Docs", assetIconName: "icon_google_docs", fallbackSymbolName: "doc.text.fill", description: "Create and edit documents.", isCreateNew: false),
17
+        LauncherApp(name: "Google Drive", assetIconName: "icon_google_drive", fallbackSymbolName: "externaldrive.fill", description: "Store and sync your files.", isCreateNew: false),
18
+        LauncherApp(name: "Google Earth", assetIconName: "icon_google_earth", fallbackSymbolName: "globe.americas.fill", description: "Explore places worldwide.", isCreateNew: false),
19
+        LauncherApp(name: "Google Photos", assetIconName: "icon_google_photos", fallbackSymbolName: "photo.stack.fill", description: "View and organize photos.", isCreateNew: false),
20
+        LauncherApp(name: "Google Maps", assetIconName: "icon_google_maps", fallbackSymbolName: "map.fill", description: "Find routes and places.", isCreateNew: false),
21
+        LauncherApp(name: "Google Translate", assetIconName: "icon_google_translate", fallbackSymbolName: "character.bubble.fill", description: "Translate text and speech.", isCreateNew: false),
22
+        LauncherApp(name: "Google Sheets", assetIconName: "icon_google_sheets", fallbackSymbolName: "tablecells.fill", description: "Work with spreadsheets.", isCreateNew: false),
23
+        LauncherApp(name: "Google Search", assetIconName: "icon_google_search", fallbackSymbolName: "magnifyingglass.circle.fill", description: "Search the web quickly.", isCreateNew: false),
24
+        LauncherApp(name: "YouTube", assetIconName: "icon_youtube", fallbackSymbolName: "play.rectangle.fill", description: "Watch videos and channels.", isCreateNew: false),
25
+        LauncherApp(name: "Google Calendar", assetIconName: "icon_google_calendar", fallbackSymbolName: "calendar", description: "Manage events and schedules.", isCreateNew: false),
26
+        LauncherApp(name: "Google Keep", assetIconName: "icon_google_keep", fallbackSymbolName: "lightbulb.fill", description: "Capture notes and reminders.", isCreateNew: false),
27
+        LauncherApp(name: "Google Slides", assetIconName: "icon_google_slides", fallbackSymbolName: "rectangle.on.rectangle.fill", description: "Build and present slides.", isCreateNew: false),
28
+        LauncherApp(name: "Google Forms", assetIconName: "icon_google_forms", fallbackSymbolName: "list.bullet.rectangle.fill", description: "Create forms and surveys.", isCreateNew: false),
29
+        LauncherApp(name: "Google Books", assetIconName: "icon_google_books", fallbackSymbolName: "book.fill", description: "Browse and read books.", isCreateNew: false),
30
+        LauncherApp(name: "Google Travel", assetIconName: "icon_google_travel", fallbackSymbolName: "airplane", description: "Plan and organize trips.", isCreateNew: false),
31
+        LauncherApp(name: "Google Meet", assetIconName: "icon_google_meet", fallbackSymbolName: "video.fill", description: "Start and join video meetings.", isCreateNew: false),
32
+        LauncherApp(name: "Create New App", assetIconName: "icon_create_new_app", fallbackSymbolName: "plus", description: "Create and add a custom app shortcut.", isCreateNew: true),
33
+    ]
34
+}

+ 126 - 0
google_apps/LauncherRootView.swift

@@ -0,0 +1,126 @@
1
+import SwiftUI
2
+
3
+struct LauncherRootView: View {
4
+    @State private var query = ""
5
+    @State private var selectedApp: LauncherApp?
6
+
7
+    private let apps = LauncherApp.sampleApps
8
+    private let columns = [GridItem(.adaptive(minimum: 122, maximum: 150), spacing: 24)]
9
+
10
+    private var filteredApps: [LauncherApp] {
11
+        let q = query.trimmingCharacters(in: .whitespacesAndNewlines)
12
+        guard !q.isEmpty else { return apps }
13
+        return apps.filter { $0.name.localizedCaseInsensitiveContains(q) }
14
+    }
15
+
16
+    var body: some View {
17
+        ZStack {
18
+            Color(red: 0.05, green: 0.06, blue: 0.09)
19
+                .ignoresSafeArea()
20
+
21
+            VStack(spacing: 16) {
22
+                SearchHeader(query: $query)
23
+                    .padding(.top, 12)
24
+
25
+                PromoBanner()
26
+
27
+                if filteredApps.isEmpty {
28
+                    ContentUnavailableView(
29
+                        "No apps found",
30
+                        systemImage: "magnifyingglass",
31
+                        description: Text("Try a different search term.")
32
+                    )
33
+                    .foregroundStyle(.white.opacity(0.85))
34
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
35
+                } else {
36
+                    ScrollView {
37
+                        LazyVGrid(columns: columns, spacing: 20) {
38
+                            ForEach(filteredApps) { app in
39
+                                AppTileView(app: app) {
40
+                                    selectedApp = app
41
+                                }
42
+                            }
43
+                        }
44
+                        .padding(.horizontal, 14)
45
+                        .padding(.bottom, 18)
46
+                    }
47
+                }
48
+            }
49
+            .padding(.horizontal, 12)
50
+            .padding(.bottom, 8)
51
+        }
52
+        .sheet(item: $selectedApp) { app in
53
+            AppDetailView(app: app)
54
+                .frame(minWidth: 360, minHeight: 220)
55
+        }
56
+    }
57
+}
58
+
59
+private struct SearchHeader: View {
60
+    @Binding var query: String
61
+
62
+    var body: some View {
63
+        HStack {
64
+            HStack(spacing: 10) {
65
+                Image(systemName: "magnifyingglass")
66
+                    .foregroundStyle(.white.opacity(0.82))
67
+                TextField("Search", text: $query)
68
+                    .textFieldStyle(.plain)
69
+                    .foregroundStyle(.white.opacity(0.94))
70
+            }
71
+            .padding(.horizontal, 12)
72
+            .padding(.vertical, 9)
73
+            .background(
74
+                RoundedRectangle(cornerRadius: 16, style: .continuous)
75
+                    .fill(Color.white.opacity(0.08))
76
+            )
77
+
78
+            Spacer(minLength: 8)
79
+
80
+            HStack(spacing: 12) {
81
+                Image(systemName: "list.bullet")
82
+                Image(systemName: "ellipsis")
83
+            }
84
+            .font(.system(size: 15, weight: .semibold))
85
+            .foregroundStyle(.white.opacity(0.82))
86
+            .padding(.trailing, 4)
87
+        }
88
+    }
89
+}
90
+
91
+private struct PromoBanner: View {
92
+    var body: some View {
93
+        HStack(spacing: 10) {
94
+            Image(systemName: "xmark")
95
+                .foregroundStyle(.blue.opacity(0.95))
96
+                .font(.system(size: 14, weight: .semibold))
97
+
98
+            Text("Update to Premium to unlock all features and remove ads")
99
+                .font(.system(size: 13, weight: .medium))
100
+                .foregroundStyle(.blue.opacity(0.95))
101
+                .lineLimit(1)
102
+
103
+            Spacer()
104
+
105
+            Text("Upgrade")
106
+                .font(.system(size: 13, weight: .semibold))
107
+                .foregroundStyle(.white)
108
+                .padding(.horizontal, 12)
109
+                .padding(.vertical, 6)
110
+                .background(
111
+                    Capsule(style: .continuous)
112
+                        .fill(Color.blue.opacity(0.92))
113
+                )
114
+        }
115
+        .padding(.horizontal, 12)
116
+        .padding(.vertical, 8)
117
+        .background(
118
+            RoundedRectangle(cornerRadius: 12, style: .continuous)
119
+                .fill(Color.white.opacity(0.04))
120
+        )
121
+    }
122
+}
123
+
124
+#Preview {
125
+    LauncherRootView()
126
+}

+ 16 - 5
google_apps/ViewController.swift

@@ -6,21 +6,32 @@
6 6
 //
7 7
 
8 8
 import Cocoa
9
+import SwiftUI
9 10
 
10 11
 class ViewController: NSViewController {
12
+    private var hostingView: NSHostingView<LauncherRootView>?
11 13
 
12 14
     override func viewDidLoad() {
13 15
         super.viewDidLoad()
14
-
15
-        // Do any additional setup after loading the view.
16
+        let launcher = LauncherRootView()
17
+        let hostingView = NSHostingView(rootView: launcher)
18
+        hostingView.translatesAutoresizingMaskIntoConstraints = false
19
+        view.addSubview(hostingView)
20
+
21
+        NSLayoutConstraint.activate([
22
+            hostingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
23
+            hostingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
24
+            hostingView.topAnchor.constraint(equalTo: view.topAnchor),
25
+            hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
26
+        ])
27
+
28
+        self.hostingView = hostingView
16 29
     }
17 30
 
18 31
     override var representedObject: Any? {
19 32
         didSet {
20
-        // Update the view, if already loaded.
33
+            // Intentionally left blank.
21 34
         }
22 35
     }
23
-
24
-
25 36
 }
26 37