SheetsDesktopWidgetView.swift 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import AppKit
  2. import SwiftUI
  3. private let sheetsAccent = Color(red: 0.13, green: 0.59, blue: 0.38)
  4. private let sheetsPanelBg = Color(red: 0.06, green: 0.12, blue: 0.09)
  5. private enum SheetsCreateURL {
  6. static let newSheet = "https://sheets.new"
  7. static let templates = "https://docs.google.com/templates?type=spreadsheets"
  8. static let homeList = "https://docs.google.com/spreadsheets/u/0/"
  9. }
  10. /// Large Google Sheets dashboard: find + create (same pattern as Forms).
  11. struct SheetsDesktopWidgetView: View {
  12. let app: LauncherApp
  13. let mode: SheetsWidgetMode
  14. var isPreview: Bool = false
  15. @State private var findText: String = ""
  16. var body: some View {
  17. Group {
  18. switch mode {
  19. case .dashboard:
  20. dashboardBody
  21. }
  22. }
  23. .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
  24. }
  25. private var dashboardBody: some View {
  26. ZStack(alignment: .topLeading) {
  27. LinearGradient(
  28. colors: [
  29. Color(red: 0.08, green: 0.32, blue: 0.22),
  30. sheetsPanelBg,
  31. ],
  32. startPoint: .topLeading,
  33. endPoint: .bottomTrailing
  34. )
  35. .frame(maxWidth: .infinity, maxHeight: .infinity)
  36. VStack(alignment: .leading, spacing: 12) {
  37. headerRow
  38. findFieldRow
  39. createRow
  40. Spacer(minLength: 0)
  41. }
  42. .padding(14)
  43. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  44. }
  45. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  46. .disabled(isPreview)
  47. .opacity(isPreview ? 0.88 : 1)
  48. }
  49. private var headerRow: some View {
  50. HStack(spacing: 8) {
  51. AppIconView(app: app, size: 30, showAppBackground: false, iconPaddingFactor: 0.08)
  52. VStack(alignment: .leading, spacing: 2) {
  53. Text("Google Sheets")
  54. .font(.system(size: 14, weight: .bold))
  55. .foregroundStyle(.white.opacity(0.95))
  56. Text("Dashboard")
  57. .font(.system(size: 10, weight: .semibold))
  58. .foregroundStyle(.white.opacity(0.55))
  59. }
  60. Spacer(minLength: 0)
  61. Button(action: { openAbsolute(SheetsCreateURL.homeList) }) {
  62. Image(systemName: "arrow.up.right.square")
  63. .font(.system(size: 14, weight: .semibold))
  64. .foregroundStyle(.white.opacity(0.9))
  65. .padding(8)
  66. .background(Circle().fill(Color.white.opacity(0.12)))
  67. }
  68. .buttonStyle(.plain)
  69. .help("Open Google Sheets")
  70. }
  71. }
  72. private var findFieldRow: some View {
  73. HStack(spacing: 8) {
  74. Image(systemName: "magnifyingglass")
  75. .font(.system(size: 13, weight: .semibold))
  76. .foregroundStyle(.white.opacity(0.5))
  77. TextField("Find spreadsheets", text: $findText)
  78. .textFieldStyle(.plain)
  79. .font(.system(size: 13, weight: .medium))
  80. .foregroundStyle(.white.opacity(0.95))
  81. .onSubmit { submitFind() }
  82. Button("Go", action: submitFind)
  83. .font(.system(size: 11, weight: .bold))
  84. .buttonStyle(.borderedProminent)
  85. .tint(sheetsAccent)
  86. .controlSize(.small)
  87. }
  88. .padding(.horizontal, 10)
  89. .padding(.vertical, 8)
  90. .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.white.opacity(0.1)))
  91. }
  92. private var createRow: some View {
  93. VStack(alignment: .leading, spacing: 6) {
  94. sectionLabel("Create")
  95. HStack(spacing: 6) {
  96. createPill(title: "Sheet", systemImage: "tablecells", urlString: SheetsCreateURL.newSheet, help: "New spreadsheet (sheets.new)")
  97. createPill(title: "Templates", systemImage: "square.grid.2x2", urlString: SheetsCreateURL.templates, help: "Spreadsheet templates")
  98. createPill(title: "Browse", systemImage: "folder", urlString: SheetsCreateURL.homeList, help: "All spreadsheets")
  99. }
  100. }
  101. }
  102. private func createPill(title: String, systemImage: String, urlString: String, help: String) -> some View {
  103. Button {
  104. openAbsolute(urlString)
  105. } label: {
  106. VStack(spacing: 4) {
  107. Image(systemName: systemImage)
  108. .font(.system(size: 16, weight: .semibold))
  109. Text(title)
  110. .font(.system(size: 10, weight: .bold))
  111. }
  112. .foregroundStyle(.white.opacity(0.92))
  113. .frame(maxWidth: .infinity)
  114. .padding(.vertical, 10)
  115. .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.white.opacity(0.1)))
  116. .overlay(
  117. RoundedRectangle(cornerRadius: 12, style: .continuous)
  118. .strokeBorder(Color.white.opacity(0.08), lineWidth: 1)
  119. )
  120. }
  121. .buttonStyle(.plain)
  122. .help(help)
  123. }
  124. private func sectionLabel(_ text: String) -> some View {
  125. Text(text)
  126. .font(.system(size: 10, weight: .bold))
  127. .foregroundStyle(.white.opacity(0.45))
  128. .textCase(.uppercase)
  129. }
  130. private func submitFind() {
  131. NSApp.activate(ignoringOtherApps: true)
  132. let q = findText.trimmingCharacters(in: .whitespacesAndNewlines)
  133. var c = URLComponents(string: SheetsCreateURL.homeList)!
  134. if !q.isEmpty {
  135. c.queryItems = [URLQueryItem(name: "q", value: q)]
  136. }
  137. guard let url = c.url else { return }
  138. openInApp(url)
  139. }
  140. private func openAbsolute(_ urlString: String) {
  141. guard let url = URL(string: urlString) else { return }
  142. NSApp.activate(ignoringOtherApps: true)
  143. openInApp(url)
  144. }
  145. private func openInApp(_ url: URL) {
  146. InAppBrowserWindowManager.shared.open(url: url, title: app.name)
  147. }
  148. }