Selaa lähdekoodia

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 4 kuukautta sitten
vanhempi
sitoutus
b76461a308

+ 120 - 0
google_apps/AppTileView.swift

@@ -0,0 +1,120 @@
+import SwiftUI
+import AppKit
+
+struct AppTileView: View {
+    let app: LauncherApp
+    let onTap: () -> Void
+
+    @State private var isHovering = false
+
+    var body: some View {
+        Button(action: onTap) {
+            VStack(spacing: 10) {
+                AppIconView(app: app, size: 66)
+                    .shadow(color: .black.opacity(0.22), radius: 9, x: 0, y: 6)
+
+                Text(app.name)
+                    .font(.system(size: 13.5, weight: .medium))
+                    .foregroundStyle(.white.opacity(0.95))
+                    .lineLimit(1)
+                    .truncationMode(.tail)
+                    .frame(maxWidth: .infinity)
+            }
+            .padding(.horizontal, 8)
+            .padding(.vertical, 10)
+            .background(
+                RoundedRectangle(cornerRadius: 14, style: .continuous)
+                    .fill(isHovering ? Color.white.opacity(0.10) : Color.clear)
+            )
+            .overlay(
+                RoundedRectangle(cornerRadius: 14, style: .continuous)
+                    .stroke(isHovering ? Color.white.opacity(0.2) : Color.clear, lineWidth: 1)
+            )
+            .animation(.easeOut(duration: 0.15), value: isHovering)
+        }
+        .buttonStyle(.plain)
+        .onHover { hovering in
+            isHovering = hovering
+        }
+    }
+}
+
+private struct AppIconView: View {
+    let app: LauncherApp
+    let size: CGFloat
+
+    var body: some View {
+        ZStack {
+            if app.isCreateNew {
+                RoundedRectangle(cornerRadius: 18, style: .continuous)
+                    .stroke(Color.white.opacity(0.35), lineWidth: 2)
+                    .background(
+                        RoundedRectangle(cornerRadius: 18, style: .continuous)
+                            .fill(Color.white.opacity(0.03))
+                    )
+                Image(systemName: "plus")
+                    .font(.system(size: size * 0.34, weight: .regular))
+                    .foregroundStyle(.white.opacity(0.9))
+            } else {
+                RoundedRectangle(cornerRadius: 18, style: .continuous)
+                    .fill(LinearGradient(colors: gradientColors, startPoint: .topLeading, endPoint: .bottomTrailing))
+
+                if let iconImage = NSImage(named: app.assetIconName) {
+                    Image(nsImage: iconImage)
+                        .resizable()
+                        .scaledToFit()
+                        .padding(size * 0.12)
+                } else {
+                    Image(systemName: app.fallbackSymbolName)
+                        .font(.system(size: size * 0.32, weight: .semibold))
+                        .foregroundStyle(.white)
+                }
+            }
+        }
+        .frame(width: size, height: size)
+    }
+
+    private var gradientColors: [Color] {
+        switch app.name {
+        case let name where name.contains("Gmail"):
+            return [Color.red.opacity(0.85), Color.orange.opacity(0.8)]
+        case let name where name.contains("Docs"):
+            return [Color.blue.opacity(0.9), Color.indigo.opacity(0.85)]
+        case let name where name.contains("Drive"):
+            return [Color.green.opacity(0.9), Color.blue.opacity(0.75)]
+        case let name where name.contains("YouTube"):
+            return [Color.red.opacity(0.95), Color.pink.opacity(0.75)]
+        case let name where name.contains("Maps"):
+            return [Color.green.opacity(0.85), Color.teal.opacity(0.75)]
+        default:
+            return [Color.blue.opacity(0.85), Color.cyan.opacity(0.7)]
+        }
+    }
+}
+
+struct AppDetailView: View {
+    let app: LauncherApp
+    @Environment(\.dismiss) private var dismiss
+
+    var body: some View {
+        ZStack {
+            Color(red: 0.09, green: 0.09, blue: 0.11)
+                .ignoresSafeArea()
+
+            VStack(spacing: 14) {
+                AppIconView(app: app, size: 76)
+                Text(app.name)
+                    .font(.system(size: 22, weight: .semibold))
+                    .foregroundStyle(.white)
+                Text(app.description)
+                    .font(.system(size: 14))
+                    .foregroundStyle(.white.opacity(0.82))
+                Button("Close") {
+                    dismiss()
+                }
+                .buttonStyle(.borderedProminent)
+            }
+            .padding(24)
+        }
+    }
+}

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

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "icon_gmail.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

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


+ 34 - 0
google_apps/LauncherApp.swift

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

+ 126 - 0
google_apps/LauncherRootView.swift

@@ -0,0 +1,126 @@
+import SwiftUI
+
+struct LauncherRootView: View {
+    @State private var query = ""
+    @State private var selectedApp: LauncherApp?
+
+    private let apps = LauncherApp.sampleApps
+    private let columns = [GridItem(.adaptive(minimum: 122, maximum: 150), spacing: 24)]
+
+    private var filteredApps: [LauncherApp] {
+        let q = query.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !q.isEmpty else { return apps }
+        return apps.filter { $0.name.localizedCaseInsensitiveContains(q) }
+    }
+
+    var body: some View {
+        ZStack {
+            Color(red: 0.05, green: 0.06, blue: 0.09)
+                .ignoresSafeArea()
+
+            VStack(spacing: 16) {
+                SearchHeader(query: $query)
+                    .padding(.top, 12)
+
+                PromoBanner()
+
+                if filteredApps.isEmpty {
+                    ContentUnavailableView(
+                        "No apps found",
+                        systemImage: "magnifyingglass",
+                        description: Text("Try a different search term.")
+                    )
+                    .foregroundStyle(.white.opacity(0.85))
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                } else {
+                    ScrollView {
+                        LazyVGrid(columns: columns, spacing: 20) {
+                            ForEach(filteredApps) { app in
+                                AppTileView(app: app) {
+                                    selectedApp = app
+                                }
+                            }
+                        }
+                        .padding(.horizontal, 14)
+                        .padding(.bottom, 18)
+                    }
+                }
+            }
+            .padding(.horizontal, 12)
+            .padding(.bottom, 8)
+        }
+        .sheet(item: $selectedApp) { app in
+            AppDetailView(app: app)
+                .frame(minWidth: 360, minHeight: 220)
+        }
+    }
+}
+
+private struct SearchHeader: View {
+    @Binding var query: String
+
+    var body: some View {
+        HStack {
+            HStack(spacing: 10) {
+                Image(systemName: "magnifyingglass")
+                    .foregroundStyle(.white.opacity(0.82))
+                TextField("Search", text: $query)
+                    .textFieldStyle(.plain)
+                    .foregroundStyle(.white.opacity(0.94))
+            }
+            .padding(.horizontal, 12)
+            .padding(.vertical, 9)
+            .background(
+                RoundedRectangle(cornerRadius: 16, style: .continuous)
+                    .fill(Color.white.opacity(0.08))
+            )
+
+            Spacer(minLength: 8)
+
+            HStack(spacing: 12) {
+                Image(systemName: "list.bullet")
+                Image(systemName: "ellipsis")
+            }
+            .font(.system(size: 15, weight: .semibold))
+            .foregroundStyle(.white.opacity(0.82))
+            .padding(.trailing, 4)
+        }
+    }
+}
+
+private struct PromoBanner: View {
+    var body: some View {
+        HStack(spacing: 10) {
+            Image(systemName: "xmark")
+                .foregroundStyle(.blue.opacity(0.95))
+                .font(.system(size: 14, weight: .semibold))
+
+            Text("Update to Premium to unlock all features and remove ads")
+                .font(.system(size: 13, weight: .medium))
+                .foregroundStyle(.blue.opacity(0.95))
+                .lineLimit(1)
+
+            Spacer()
+
+            Text("Upgrade")
+                .font(.system(size: 13, weight: .semibold))
+                .foregroundStyle(.white)
+                .padding(.horizontal, 12)
+                .padding(.vertical, 6)
+                .background(
+                    Capsule(style: .continuous)
+                        .fill(Color.blue.opacity(0.92))
+                )
+        }
+        .padding(.horizontal, 12)
+        .padding(.vertical, 8)
+        .background(
+            RoundedRectangle(cornerRadius: 12, style: .continuous)
+                .fill(Color.white.opacity(0.04))
+        )
+    }
+}
+
+#Preview {
+    LauncherRootView()
+}

+ 16 - 5
google_apps/ViewController.swift

@@ -6,21 +6,32 @@
 //
 
 import Cocoa
+import SwiftUI
 
 class ViewController: NSViewController {
+    private var hostingView: NSHostingView<LauncherRootView>?
 
     override func viewDidLoad() {
         super.viewDidLoad()
-
-        // Do any additional setup after loading the view.
+        let launcher = LauncherRootView()
+        let hostingView = NSHostingView(rootView: launcher)
+        hostingView.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(hostingView)
+
+        NSLayoutConstraint.activate([
+            hostingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            hostingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            hostingView.topAnchor.constraint(equalTo: view.topAnchor),
+            hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+        ])
+
+        self.hostingView = hostingView
     }
 
     override var representedObject: Any? {
         didSet {
-        // Update the view, if already loaded.
+            // Intentionally left blank.
         }
     }
-
-
 }