|
@@ -0,0 +1,614 @@
|
|
|
|
|
+import AppKit
|
|
|
|
|
+import SwiftUI
|
|
|
|
|
+import WebKit
|
|
|
|
|
+
|
|
|
|
|
+/// Tiered Google Drive desktop widget: recent files, starred preview, or full embedded Drive.
|
|
|
|
|
+struct DriveDesktopWidgetView: View {
|
|
|
|
|
+ let app: LauncherApp
|
|
|
|
|
+ let mode: DriveWidgetMode
|
|
|
|
|
+ var isPreview: Bool = false
|
|
|
|
|
+
|
|
|
|
|
+ private var baseURL: URL { app.webURL ?? URL(string: "https://drive.google.com")! }
|
|
|
|
|
+
|
|
|
|
|
+ var body: some View {
|
|
|
|
|
+ Group {
|
|
|
|
|
+ switch mode {
|
|
|
|
|
+ case .recentQuick:
|
|
|
|
|
+ recentQuickBody
|
|
|
|
|
+ case .filesPreview:
|
|
|
|
|
+ filesPreviewBody
|
|
|
|
|
+ case .interactive:
|
|
|
|
|
+ interactiveBody
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Small
|
|
|
|
|
+
|
|
|
|
|
+ private struct RecentEntry: Identifiable {
|
|
|
|
|
+ let id: String
|
|
|
|
|
+ let name: String
|
|
|
|
|
+ let systemImage: String
|
|
|
|
|
+ let time: String
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static let recentSamples: [RecentEntry] = [
|
|
|
|
|
+ RecentEntry(id: "1", name: "Q4 Budget.xlsx", systemImage: "tablecells", time: "2h ago"),
|
|
|
|
|
+ RecentEntry(id: "2", name: "Project Notes", systemImage: "doc.text", time: "Yesterday"),
|
|
|
|
|
+ RecentEntry(id: "3", name: "Design mockup.png", systemImage: "photo", time: "Mon"),
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ private var recentQuickBody: some View {
|
|
|
|
|
+ ZStack(alignment: .topLeading) {
|
|
|
|
|
+ driveGradientBackground
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
|
|
+ HStack(spacing: 10) {
|
|
|
|
|
+ AppIconView(app: app, size: 32, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
+ Text(app.name)
|
|
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ Text("Recent files")
|
|
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
|
|
+ }
|
|
|
|
|
+ Spacer(minLength: 0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Text("Sample")
|
|
|
|
|
+ .font(.system(size: 9, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.45))
|
|
|
|
|
+
|
|
|
|
|
+ VStack(spacing: 6) {
|
|
|
|
|
+ ForEach(Self.recentSamples) { item in
|
|
|
|
|
+ recentRow(name: item.name, systemImage: item.systemImage, time: item.time) {
|
|
|
|
|
+ openDriveSearch(query: item.name)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ drivePrimaryButton(title: "Open Drive", systemImage: "externaldrive.fill") {
|
|
|
|
|
+ openDrivePath("/drive/home")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(14)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ .disabled(isPreview)
|
|
|
|
|
+ .opacity(isPreview ? 0.85 : 1)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func recentRow(name: String, systemImage: String, time: String, action: @escaping () -> Void) -> some View {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ action()
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack(spacing: 10) {
|
|
|
|
|
+ Image(systemName: systemImage)
|
|
|
|
|
+ .font(.system(size: 14, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(Color(red: 0.75, green: 0.92, blue: 0.82))
|
|
|
|
|
+ .frame(width: 28, height: 28)
|
|
|
|
|
+ .background(RoundedRectangle(cornerRadius: 8).fill(Color.white.opacity(0.12)))
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
+ Text(name)
|
|
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ .lineLimit(1)
|
|
|
|
|
+ Text(time)
|
|
|
|
|
+ .font(.system(size: 10, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.5))
|
|
|
|
|
+ }
|
|
|
|
|
+ Spacer(minLength: 0)
|
|
|
|
|
+ Image(systemName: "chevron.right")
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.35))
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(10)
|
|
|
|
|
+ .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.22)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .disabled(isPreview)
|
|
|
|
|
+ .allowsHitTesting(!isPreview)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Medium
|
|
|
|
|
+
|
|
|
|
|
+ private struct StarredEntry: Identifiable {
|
|
|
|
|
+ let id: String
|
|
|
|
|
+ let name: String
|
|
|
|
|
+ let systemImage: String
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static let starredSamples: [StarredEntry] = [
|
|
|
|
|
+ StarredEntry(id: "s1", name: "Roadmap 2026", systemImage: "rectangle.on.rectangle"),
|
|
|
|
|
+ StarredEntry(id: "s2", name: "Team roster", systemImage: "doc.richtext"),
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ private var filesPreviewBody: some View {
|
|
|
|
|
+ ZStack(alignment: .topLeading) {
|
|
|
|
|
+ driveGradientBackground
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
|
+ ScrollView {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 12) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ AppIconView(app: app, size: 30, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ Text("Drive")
|
|
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ Spacer(minLength: 0)
|
|
|
|
|
+ Text("Sample")
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.5))
|
|
|
|
|
+ .padding(.horizontal, 8)
|
|
|
|
|
+ .padding(.vertical, 4)
|
|
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.12)))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ drivePrimaryButton(title: "Open Drive", systemImage: "externaldrive.fill") {
|
|
|
|
|
+ openDrivePath("/drive/home")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ sectionTitle("Recent")
|
|
|
|
|
+ VStack(spacing: 8) {
|
|
|
|
|
+ ForEach(Self.recentSamples) { item in
|
|
|
|
|
+ fileActionRow(
|
|
|
|
|
+ name: item.name,
|
|
|
|
|
+ systemImage: item.systemImage,
|
|
|
|
|
+ subtitle: item.time,
|
|
|
|
|
+ onOpen: { openDrivePath("/drive/recent") },
|
|
|
|
|
+ onShare: { openDriveSearch(query: item.name) }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ sectionTitle("Starred")
|
|
|
|
|
+ VStack(spacing: 8) {
|
|
|
|
|
+ ForEach(Self.starredSamples) { item in
|
|
|
|
|
+ fileActionRow(
|
|
|
|
|
+ name: item.name,
|
|
|
|
|
+ systemImage: item.systemImage,
|
|
|
|
|
+ subtitle: "Starred",
|
|
|
|
|
+ onOpen: { openDrivePath("/drive/starred") },
|
|
|
|
|
+ onShare: { openDriveSearch(query: item.name) }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
+ .padding(.bottom, 6)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(14)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ .disabled(isPreview)
|
|
|
|
|
+ .opacity(isPreview ? 0.85 : 1)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func sectionTitle(_ text: String) -> some View {
|
|
|
|
|
+ Text(text)
|
|
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.62))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func fileActionRow(
|
|
|
|
|
+ name: String,
|
|
|
|
|
+ systemImage: String,
|
|
|
|
|
+ subtitle: String,
|
|
|
|
|
+ onOpen: @escaping () -> Void,
|
|
|
|
|
+ onShare: @escaping () -> Void
|
|
|
|
|
+ ) -> some View {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
+ HStack(spacing: 10) {
|
|
|
|
|
+ Image(systemName: systemImage)
|
|
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(Color(red: 0.75, green: 0.92, blue: 0.82))
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
+ Text(name)
|
|
|
|
|
+ .font(.system(size: 12, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ .lineLimit(1)
|
|
|
|
|
+ Text(subtitle)
|
|
|
|
|
+ .font(.system(size: 10, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.5))
|
|
|
|
|
+ }
|
|
|
|
|
+ Spacer(minLength: 0)
|
|
|
|
|
+ }
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ smallAction(title: "Open", systemImage: "arrow.up.right.square", action: onOpen)
|
|
|
|
|
+ smallAction(title: "Share", systemImage: "square.and.arrow.up", action: onShare)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(10)
|
|
|
|
|
+ .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.24)))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func smallAction(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ action()
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack(spacing: 4) {
|
|
|
|
|
+ Image(systemName: systemImage)
|
|
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
|
|
+ Text(title)
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ }
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.9))
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 6)
|
|
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.14)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .disabled(isPreview)
|
|
|
|
|
+ .allowsHitTesting(!isPreview)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Large (interactive)
|
|
|
|
|
+
|
|
|
|
|
+ @State private var searchText = ""
|
|
|
|
|
+ @State private var webURL: URL
|
|
|
|
|
+
|
|
|
|
|
+ init(app: LauncherApp, mode: DriveWidgetMode, isPreview: Bool = false) {
|
|
|
|
|
+ self.app = app
|
|
|
|
|
+ self.mode = mode
|
|
|
|
|
+ self.isPreview = isPreview
|
|
|
|
|
+ let base = app.webURL ?? URL(string: "https://drive.google.com")!
|
|
|
|
|
+ _webURL = State(initialValue: Self.defaultDriveURL(base: base))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func defaultDriveURL(base: URL) -> URL {
|
|
|
|
|
+ WidgetDeepLinkURL.resolved(base: base, actionPath: "/drive/home", accountSlot: 0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var interactiveBody: some View {
|
|
|
|
|
+ Group {
|
|
|
|
|
+ if isPreview {
|
|
|
|
|
+ interactivePreviewChrome
|
|
|
|
|
+ } else {
|
|
|
|
|
+ interactiveLiveChrome
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var interactivePreviewChrome: some View {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
+ .fill(
|
|
|
|
|
+ LinearGradient(
|
|
|
|
|
+ colors: [Color(red: 0.1, green: 0.28, blue: 0.22), Color(red: 0.08, green: 0.14, blue: 0.22)],
|
|
|
|
|
+ startPoint: .topLeading,
|
|
|
|
|
+ endPoint: .bottomTrailing
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ VStack(spacing: 12) {
|
|
|
|
|
+ AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ Text("Google Drive")
|
|
|
|
|
+ .font(.system(size: 15, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
|
|
+ Text("Search, folders, previews, and create options load on your desktop.")
|
|
|
|
|
+ .font(.system(size: 11, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.65))
|
|
|
|
|
+ .multilineTextAlignment(.center)
|
|
|
|
|
+ .padding(.horizontal, 8)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var interactiveLiveChrome: some View {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
|
|
+ .fill(Color.black.opacity(0.32))
|
|
|
|
|
+
|
|
|
|
|
+ VStack(spacing: 0) {
|
|
|
|
|
+ VStack(spacing: 8) {
|
|
|
|
|
+ interactiveHeader
|
|
|
|
|
+ interactiveSearchBar
|
|
|
|
|
+ folderChipsRow
|
|
|
|
|
+ createAndUploadRow
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.horizontal, 6)
|
|
|
|
|
+ .padding(.vertical, 8)
|
|
|
|
|
+ .background(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
|
|
+ .fill(Color(red: 0.06, green: 0.1, blue: 0.09))
|
|
|
|
|
+ )
|
|
|
|
|
+ .zIndex(20)
|
|
|
|
|
+
|
|
|
|
|
+ DriveWebWidgetView(url: $webURL)
|
|
|
|
|
+ .frame(minHeight: 280)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
|
|
|
+ .zIndex(0)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(10)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var interactiveHeader: some View {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
|
|
+ Text("Drive")
|
|
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ Spacer(minLength: 0)
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ openDrivePath("/drive/home")
|
|
|
|
|
+ }) {
|
|
|
|
|
+ Image(systemName: "externaldrive.fill")
|
|
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.9))
|
|
|
|
|
+ .padding(8)
|
|
|
|
|
+ .background(Circle().fill(Color.white.opacity(0.12)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .help("Open Drive in browser")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var interactiveSearchBar: some View {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ Image(systemName: "magnifyingglass")
|
|
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
|
|
+ TextField("Search in Drive", text: $searchText)
|
|
|
|
|
+ .textFieldStyle(.plain)
|
|
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
|
|
+ .onSubmit { submitSearch() }
|
|
|
|
|
+ Button("Go") { submitSearch() }
|
|
|
|
|
+ .font(.system(size: 11, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(Color(red: 0.4, green: 0.85, blue: 0.65))
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 8)
|
|
|
|
|
+ .background(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
|
|
+ .fill(Color.white.opacity(0.08))
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var folderChipsRow: some View {
|
|
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ driveChip("My Drive", path: "/drive/my-drive")
|
|
|
|
|
+ driveChip("Recent", path: "/drive/recent")
|
|
|
|
|
+ driveChip("Starred", path: "/drive/starred")
|
|
|
|
|
+ driveChip("Shared", path: "/drive/shared-with-me")
|
|
|
|
|
+ driveChip("Shared drives", path: "/drive/shared-drives")
|
|
|
|
|
+ driveChip("Computers", path: "/drive/computers")
|
|
|
|
|
+ driveChip("Trash", path: "/drive/trash")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var createAndUploadRow: some View {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 6) {
|
|
|
|
|
+ Text("Create")
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
|
|
+ ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ createChip("Doc", systemImage: "doc.text", url: "https://docs.google.com/document/create")
|
|
|
|
|
+ createChip("Sheet", systemImage: "tablecells", url: "https://sheets.google.com/spreadsheets/create")
|
|
|
|
|
+ createChip("Slides", systemImage: "rectangle.on.rectangle", url: "https://slides.google.com/presentation/create")
|
|
|
|
|
+ createChip("Form", systemImage: "list.bullet.rectangle", url: "https://docs.google.com/forms/create")
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ openDrivePath("/drive/home")
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack(spacing: 4) {
|
|
|
|
|
+ Image(systemName: "arrow.up.doc")
|
|
|
|
|
+ Text("Upload")
|
|
|
|
|
+ }
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 6)
|
|
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.14)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .help("Open Drive to upload files")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func driveChip(_ title: String, path: String) -> some View {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ navigateWeb(to: path)
|
|
|
|
|
+ }) {
|
|
|
|
|
+ Text(title)
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 6)
|
|
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.12)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func createChip(_ title: String, systemImage: String, url: String) -> some View {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ guard let u = URL(string: url) else { return }
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: u, title: app.name)
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack(spacing: 4) {
|
|
|
|
|
+ Image(systemName: systemImage)
|
|
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
|
|
+ Text(title)
|
|
|
|
|
+ .font(.system(size: 10, weight: .bold))
|
|
|
|
|
+ }
|
|
|
|
|
+ .foregroundStyle(.white.opacity(0.92))
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 6)
|
|
|
|
|
+ .background(Capsule().fill(Color.white.opacity(0.12)))
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Shared
|
|
|
|
|
+
|
|
|
|
|
+ private var driveGradientBackground: some View {
|
|
|
|
|
+ LinearGradient(
|
|
|
|
|
+ colors: [
|
|
|
|
|
+ Color(red: 0.12, green: 0.36, blue: 0.28),
|
|
|
|
|
+ Color(red: 0.08, green: 0.16, blue: 0.28),
|
|
|
|
|
+ ],
|
|
|
|
|
+ startPoint: .topLeading,
|
|
|
|
|
+ endPoint: .bottomTrailing
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func drivePrimaryButton(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ action()
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack(spacing: 8) {
|
|
|
|
|
+ Image(systemName: systemImage)
|
|
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
|
|
+ Text(title)
|
|
|
|
|
+ .font(.system(size: 13, weight: .bold))
|
|
|
|
|
+ }
|
|
|
|
|
+ .foregroundStyle(Color(red: 0.06, green: 0.12, blue: 0.1))
|
|
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
|
|
+ .padding(.vertical, 11)
|
|
|
|
|
+ .background(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
|
|
+ .fill(Color.white.opacity(0.9))
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .disabled(isPreview)
|
|
|
|
|
+ .allowsHitTesting(!isPreview)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Navigation
|
|
|
|
|
+
|
|
|
|
|
+ private func openDrivePath(_ path: String) {
|
|
|
|
|
+ guard !isPreview else { return }
|
|
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
|
|
|
|
|
+ InAppBrowserWindowManager.shared.open(url: url, title: app.name)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func openDriveSearch(query: String) {
|
|
|
|
|
+ guard !isPreview else { return }
|
|
|
|
|
+ let encoded = query.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? query
|
|
|
|
|
+ let path = "/drive/search?q=\(encoded)"
|
|
|
|
|
+ openDrivePath(path)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func navigateWeb(to path: String) {
|
|
|
|
|
+ let url = WidgetDeepLinkURL.resolved(base: baseURL, actionPath: path, accountSlot: 0)
|
|
|
|
|
+ DispatchQueue.main.async {
|
|
|
|
|
+ webURL = url
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func submitSearch() {
|
|
|
|
|
+ let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ guard !q.isEmpty else { return }
|
|
|
|
|
+ let encoded = q.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? q
|
|
|
|
|
+ navigateWeb(to: "/drive/search?q=\(encoded)")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// MARK: - WKWebView
|
|
|
|
|
+
|
|
|
|
|
+private final class DriveWebViewClippingContainer: NSView {
|
|
|
|
|
+ override var isFlipped: Bool { true }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private struct DriveWebWidgetView: NSViewRepresentable {
|
|
|
|
|
+ @Binding var url: URL
|
|
|
|
|
+
|
|
|
|
|
+ func makeCoordinator() -> Coordinator {
|
|
|
|
|
+ Coordinator()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func makeNSView(context: Context) -> NSView {
|
|
|
|
|
+ let container = DriveWebViewClippingContainer()
|
|
|
|
|
+ container.wantsLayer = true
|
|
|
|
|
+ container.layer?.masksToBounds = true
|
|
|
|
|
+ container.layer?.cornerRadius = 12
|
|
|
|
|
+ container.layer?.borderWidth = 0
|
|
|
|
|
+
|
|
|
|
|
+ let config = WKWebViewConfiguration()
|
|
|
|
|
+ config.defaultWebpagePreferences.allowsContentJavaScript = true
|
|
|
|
|
+ let wv = WKWebView(frame: .zero, configuration: config)
|
|
|
|
|
+ wv.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ wv.focusRingType = .none
|
|
|
|
|
+ wv.customUserAgent =
|
|
|
|
|
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"
|
|
|
|
|
+ wv.navigationDelegate = context.coordinator
|
|
|
|
|
+
|
|
|
|
|
+ container.addSubview(wv)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ wv.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
|
|
|
+ wv.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
|
|
|
+ wv.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
|
|
+ wv.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ context.coordinator.webView = wv
|
|
|
|
|
+ context.coordinator.lastLoadedURLString = url.absoluteString
|
|
|
|
|
+ wv.load(URLRequest(url: url))
|
|
|
|
|
+ DispatchQueue.main.async {
|
|
|
|
|
+ Self.stripWebKitOuterChrome(wv)
|
|
|
|
|
+ }
|
|
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
|
|
|
|
+ Self.stripWebKitOuterChrome(wv)
|
|
|
|
|
+ }
|
|
|
|
|
+ return container
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func updateNSView(_ container: NSView, context: Context) {
|
|
|
|
|
+ guard let wv = context.coordinator.webView else { return }
|
|
|
|
|
+ let next = url.absoluteString
|
|
|
|
|
+ guard context.coordinator.lastLoadedURLString != next else { return }
|
|
|
|
|
+ context.coordinator.lastLoadedURLString = next
|
|
|
|
|
+ wv.load(URLRequest(url: url))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func stripWebKitOuterChrome(_ webView: WKWebView) {
|
|
|
|
|
+ webView.focusRingType = .none
|
|
|
|
|
+ func walk(_ view: NSView) {
|
|
|
|
|
+ view.focusRingType = .none
|
|
|
|
|
+ view.layer?.borderWidth = 0
|
|
|
|
|
+ view.layer?.borderColor = NSColor.clear.cgColor
|
|
|
|
|
+ if let scroll = view as? NSScrollView {
|
|
|
|
|
+ scroll.borderType = .noBorder
|
|
|
|
|
+ scroll.drawsBackground = false
|
|
|
|
|
+ scroll.scrollerStyle = .overlay
|
|
|
|
|
+ }
|
|
|
|
|
+ if let clip = view as? NSClipView {
|
|
|
|
|
+ clip.drawsBackground = false
|
|
|
|
|
+ }
|
|
|
|
|
+ for sub in view.subviews {
|
|
|
|
|
+ walk(sub)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ walk(webView)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ final class Coordinator: NSObject, WKNavigationDelegate {
|
|
|
|
|
+ weak var webView: WKWebView?
|
|
|
|
|
+ var lastLoadedURLString: String?
|
|
|
|
|
+
|
|
|
|
|
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
|
|
+ DriveWebWidgetView.stripWebKitOuterChrome(webView)
|
|
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
|
|
|
+ DriveWebWidgetView.stripWebKitOuterChrome(webView)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|