CreateAppSheetView.swift 5.9 KB

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