CreateAppSheetView.swift 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import SwiftUI
  2. struct CreateAppSheetView: View {
  3. struct NewAppInput {
  4. let name: String
  5. let url: URL
  6. }
  7. @Environment(\.dismiss) private var dismiss
  8. @State private var name = ""
  9. @State private var urlText = ""
  10. @State private var urlError: String?
  11. let onSave: (NewAppInput) -> Void
  12. var onClose: (() -> Void)? = nil
  13. var showsBackgroundLayer: Bool = true
  14. var body: some View {
  15. ZStack {
  16. if showsBackgroundLayer {
  17. backgroundLayer
  18. }
  19. VStack(spacing: 22) {
  20. header
  21. formContent
  22. footer
  23. }
  24. .padding(.horizontal, 20)
  25. .padding(.vertical, 16)
  26. }
  27. }
  28. private var backgroundLayer: some View {
  29. ZStack {
  30. LinearGradient(
  31. colors: [Color.black.opacity(0.95), Color(red: 0.05, green: 0.08, blue: 0.1)],
  32. startPoint: .topLeading,
  33. endPoint: .bottomTrailing
  34. )
  35. .ignoresSafeArea()
  36. Circle()
  37. .fill(Color.blue.opacity(0.18))
  38. .blur(radius: 90)
  39. .frame(width: 360, height: 360)
  40. .offset(x: -70, y: -120)
  41. Circle()
  42. .fill(Color.green.opacity(0.16))
  43. .blur(radius: 100)
  44. .frame(width: 380, height: 380)
  45. .offset(x: 210, y: 80)
  46. }
  47. }
  48. private var header: some View {
  49. HStack {
  50. Button(action: { closeView() }) {
  51. Image(systemName: "xmark")
  52. .font(.system(size: 17, weight: .medium))
  53. .foregroundStyle(.white.opacity(0.86))
  54. }
  55. .buttonStyle(.plain)
  56. Spacer()
  57. Button("Save") {
  58. saveApp()
  59. }
  60. .buttonStyle(.plain)
  61. .font(.system(size: 17, weight: .semibold))
  62. .foregroundStyle(canSave ? Color.white.opacity(0.9) : Color.white.opacity(0.4))
  63. .disabled(!canSave)
  64. }
  65. }
  66. private var formContent: some View {
  67. HStack(alignment: .top, spacing: 18) {
  68. RoundedRectangle(cornerRadius: 16, style: .continuous)
  69. .fill(Color.white.opacity(0.32))
  70. .frame(width: 160, height: 160)
  71. .overlay(
  72. Image(systemName: "globe")
  73. .font(.system(size: 38, weight: .regular))
  74. .foregroundStyle(.white.opacity(0.24))
  75. )
  76. VStack(spacing: 10) {
  77. inputField(title: "Name", text: $name)
  78. inputField(title: "URL", text: $urlText)
  79. if let urlError {
  80. Text(urlError)
  81. .font(.system(size: 12, weight: .medium))
  82. .foregroundStyle(.red.opacity(0.9))
  83. .frame(maxWidth: .infinity, alignment: .leading)
  84. }
  85. }
  86. }
  87. }
  88. private var footer: some View {
  89. HStack {
  90. VStack(alignment: .leading, spacing: 5) {
  91. Text("Open with")
  92. .font(.system(size: 41 / 2.2, weight: .semibold))
  93. .foregroundStyle(.white.opacity(0.92))
  94. Text("This app opens in your in-app browser window.")
  95. .font(.system(size: 13 / 1.35))
  96. .foregroundStyle(.white.opacity(0.65))
  97. }
  98. Spacer()
  99. HStack(spacing: 8) {
  100. RoundedRectangle(cornerRadius: 10, style: .continuous)
  101. .fill(Color.white.opacity(0.14))
  102. .overlay(
  103. Image(systemName: "app.badge")
  104. .font(.system(size: 17, weight: .semibold))
  105. .foregroundStyle(.white.opacity(0.9))
  106. )
  107. .frame(width: 42, height: 42)
  108. Text("Apps for Google App")
  109. .font(.system(size: 42 / 2.2, weight: .medium))
  110. Image(systemName: "chevron.down")
  111. .font(.system(size: 11, weight: .semibold))
  112. .offset(y: 1)
  113. }
  114. .minimumScaleFactor(0.6)
  115. .lineLimit(1)
  116. .foregroundStyle(.white.opacity(0.88))
  117. }
  118. .padding(.top, 2)
  119. }
  120. private var canSave: Bool {
  121. !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
  122. !urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
  123. }
  124. private func inputField(title: String, text: Binding<String>) -> some View {
  125. TextField(title, text: text)
  126. .textFieldStyle(.plain)
  127. .font(.system(size: 39 / 2.2, weight: .medium))
  128. .foregroundStyle(.white.opacity(0.95))
  129. .padding(.horizontal, 22)
  130. .padding(.vertical, 16)
  131. .background(
  132. RoundedRectangle(cornerRadius: 20, style: .continuous)
  133. .fill(Color.white.opacity(0.06))
  134. )
  135. .overlay(
  136. RoundedRectangle(cornerRadius: 20, style: .continuous)
  137. .stroke(Color.white.opacity(0.3), lineWidth: 1.7)
  138. )
  139. }
  140. private func saveApp() {
  141. let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
  142. let trimmedURL = urlText.trimmingCharacters(in: .whitespacesAndNewlines)
  143. guard !trimmedName.isEmpty, !trimmedURL.isEmpty else { return }
  144. let candidate = normalizedURLString(trimmedURL)
  145. guard let url = URL(string: candidate), let scheme = url.scheme, !scheme.isEmpty else {
  146. urlError = "Please enter a valid URL."
  147. return
  148. }
  149. urlError = nil
  150. onSave(NewAppInput(name: trimmedName, url: url))
  151. closeView()
  152. }
  153. private func normalizedURLString(_ value: String) -> String {
  154. if value.lowercased().hasPrefix("http://") || value.lowercased().hasPrefix("https://") {
  155. return value
  156. }
  157. return "https://\(value)"
  158. }
  159. private func closeView() {
  160. if let onClose {
  161. onClose()
  162. } else {
  163. dismiss()
  164. }
  165. }
  166. }
  167. #if DEBUG
  168. #Preview {
  169. CreateAppSheetView { _ in }
  170. .frame(width: 800, height: 390)
  171. }
  172. #endif