PunctuationCheckerView.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import SwiftUI
  2. struct PunctuationCheckerView: View {
  3. @StateObject private var viewModel = PunctuationCheckerViewModel()
  4. var body: some View {
  5. GeometryReader { proxy in
  6. let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
  7. let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
  8. Group {
  9. if viewModel.isChecking {
  10. loadingLayout(
  11. horizontalPadding: dynamicHorizontalPadding,
  12. topPadding: AppTheme.brandLabelTopInset,
  13. bottomPadding: dynamicBottomPadding
  14. )
  15. } else if viewModel.hasResults {
  16. VStack(alignment: .leading, spacing: 0) {
  17. header
  18. .padding(.bottom, 20)
  19. resultsLayout
  20. .frame(maxWidth: .infinity, maxHeight: .infinity)
  21. }
  22. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
  23. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  24. .padding(.horizontal, dynamicHorizontalPadding)
  25. .padding(.top, AppTheme.brandLabelTopInset)
  26. .padding(.bottom, dynamicBottomPadding)
  27. } else {
  28. VStack(alignment: .leading, spacing: 0) {
  29. header
  30. .padding(.bottom, 24)
  31. contentCard
  32. .frame(maxWidth: .infinity, maxHeight: .infinity)
  33. if let errorMessage = viewModel.errorMessage {
  34. Text(errorMessage)
  35. .font(.system(size: 14))
  36. .foregroundStyle(.red)
  37. .padding(.top, 12)
  38. }
  39. GradientButton(title: viewModel.isChecking ? "Checking..." : "Check Punctuation") {
  40. viewModel.checkPunctuation()
  41. }
  42. .disabled(!viewModel.canCheck)
  43. .padding(.top, 16)
  44. }
  45. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
  46. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  47. .padding(.horizontal, dynamicHorizontalPadding)
  48. .padding(.top, AppTheme.brandLabelTopInset)
  49. .padding(.bottom, dynamicBottomPadding)
  50. }
  51. }
  52. }
  53. .onChange(of: viewModel.text) { _ in
  54. viewModel.handleTextChange()
  55. }
  56. }
  57. private func loadingLayout(
  58. horizontalPadding: CGFloat,
  59. topPadding: CGFloat,
  60. bottomPadding: CGFloat
  61. ) -> some View {
  62. VStack(spacing: 14) {
  63. ProgressView()
  64. .controlSize(.regular)
  65. Text("Loading...")
  66. .font(.system(size: 16, weight: .semibold))
  67. .foregroundStyle(AppTheme.textPrimary)
  68. Text("Checking punctuation and applying corrections")
  69. .font(.system(size: 14))
  70. .foregroundStyle(AppTheme.textSecondary)
  71. }
  72. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .center)
  73. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
  74. .padding(.horizontal, horizontalPadding)
  75. .padding(.top, topPadding)
  76. .padding(.bottom, bottomPadding)
  77. }
  78. private var header: some View {
  79. VStack(alignment: .leading, spacing: 16) {
  80. if viewModel.hasResults {
  81. IconButton(iconName: "arrow.left") {
  82. viewModel.resetResults()
  83. }
  84. .accessibilityLabel("Go back")
  85. }
  86. HStack(alignment: .top) {
  87. VStack(alignment: .leading, spacing: 8) {
  88. Text("Punctuation Checker")
  89. .font(.system(size: 30, weight: .bold))
  90. .foregroundStyle(AppTheme.textPrimary)
  91. HStack(spacing: 0) {
  92. Text("Perfect your ")
  93. .foregroundStyle(AppTheme.textSecondary)
  94. Text("punctuation")
  95. .fontWeight(.semibold)
  96. .foregroundStyle(AppTheme.teal)
  97. Text(". Write with ")
  98. .foregroundStyle(AppTheme.textSecondary)
  99. Text("clarity")
  100. .fontWeight(.semibold)
  101. .foregroundStyle(AppTheme.teal)
  102. Text(".")
  103. .foregroundStyle(AppTheme.textSecondary)
  104. }
  105. .font(.system(size: 15))
  106. }
  107. Spacer()
  108. IconButton {
  109. SettingsGearIcon()
  110. } action: {}
  111. .accessibilityLabel("Settings")
  112. }
  113. }
  114. }
  115. private var contentCard: some View {
  116. VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
  117. YourContentHeader()
  118. VStack(alignment: .leading, spacing: 6) {
  119. textEditor
  120. .background(Color.white)
  121. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  122. .overlay(
  123. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  124. .stroke(AppTheme.inputBorder, lineWidth: 1)
  125. )
  126. Text(viewModel.wordCountLimitLabel)
  127. .font(.system(size: 11))
  128. .foregroundStyle(AppTheme.textMuted)
  129. .frame(maxWidth: .infinity, alignment: .trailing)
  130. }
  131. .scrollSafeEditorSectionFrame(compact: false)
  132. HStack(spacing: 8) {
  133. ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
  134. viewModel.pasteText()
  135. }
  136. ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {
  137. viewModel.uploadFile()
  138. }
  139. Spacer()
  140. ClearButton {
  141. viewModel.clearText()
  142. }
  143. }
  144. }
  145. .inputContentCardPadding()
  146. .scrollSafeContentCardFrame(compact: false)
  147. .background(AppTheme.cardBackground)
  148. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  149. .overlay(
  150. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  151. .stroke(AppTheme.border, lineWidth: 1)
  152. )
  153. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  154. }
  155. private var textEditor: some View {
  156. ThinCaretTextEditor(
  157. text: $viewModel.text,
  158. placeholder: "Type or paste your text here and press the check button...",
  159. maxWords: PunctuationCheckerViewModel.maxWords,
  160. onWordLimitReached: viewModel.notifyWordLimitReached
  161. )
  162. .font(.system(size: 14))
  163. .foregroundStyle(AppTheme.textPrimary)
  164. .scrollSafeTextEditorFrame(compact: false)
  165. }
  166. private var resultsLayout: some View {
  167. correctedTextCard
  168. }
  169. private var correctedTextCard: some View {
  170. VStack(alignment: .leading, spacing: 12) {
  171. HStack {
  172. Text("Corrected Text")
  173. .font(.system(size: 13, weight: .semibold))
  174. .foregroundStyle(AppTheme.textPrimary)
  175. Spacer()
  176. ToolbarButton(title: "Copy", iconName: "doc.on.doc") {
  177. viewModel.copyCorrectedText()
  178. }
  179. .disabled(viewModel.correctedText.isEmpty)
  180. .opacity(viewModel.correctedText.isEmpty ? 0.55 : 1)
  181. }
  182. ScrollView(.vertical, showsIndicators: true) {
  183. Text(viewModel.correctedText)
  184. .font(.system(size: 14))
  185. .foregroundStyle(AppTheme.textPrimary)
  186. .frame(maxWidth: .infinity, alignment: .leading)
  187. .textSelection(.enabled)
  188. .padding(16)
  189. }
  190. .frame(maxHeight: .infinity, alignment: .top)
  191. .background(AppTheme.tealLight.opacity(0.35))
  192. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  193. .overlay(
  194. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  195. .stroke(AppTheme.teal.opacity(0.2), lineWidth: 1)
  196. )
  197. }
  198. .padding(AppTheme.contentCardPadding)
  199. .background(AppTheme.cardBackground)
  200. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  201. .overlay(
  202. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  203. .stroke(AppTheme.border, lineWidth: 1)
  204. )
  205. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  206. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  207. }
  208. }