PunctuationCheckerView.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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.correctedText.isEmpty {
  10. VStack(alignment: .leading, spacing: 0) {
  11. header
  12. .padding(.bottom, 24)
  13. contentCard
  14. .frame(maxWidth: .infinity, maxHeight: .infinity)
  15. if let errorMessage = viewModel.errorMessage {
  16. Text(errorMessage)
  17. .font(.system(size: 14))
  18. .foregroundStyle(.red)
  19. .padding(.top, 12)
  20. }
  21. GradientButton(title: viewModel.isChecking ? "Checking..." : "Check Punctuation") {
  22. viewModel.checkPunctuation()
  23. }
  24. .disabled(!viewModel.canCheck)
  25. .padding(.top, 16)
  26. }
  27. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
  28. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  29. .padding(.horizontal, dynamicHorizontalPadding)
  30. .padding(.top, AppTheme.brandLabelTopInset)
  31. .padding(.bottom, dynamicBottomPadding)
  32. } else {
  33. ScrollView(.vertical, showsIndicators: false) {
  34. VStack(alignment: .leading, spacing: 0) {
  35. header
  36. .padding(.bottom, 24)
  37. contentCard
  38. .padding(.bottom, 24)
  39. outputCard
  40. .padding(.bottom, 24)
  41. if let errorMessage = viewModel.errorMessage {
  42. Text(errorMessage)
  43. .font(.system(size: 14))
  44. .foregroundStyle(.red)
  45. .padding(.bottom, 12)
  46. }
  47. GradientButton(title: viewModel.isChecking ? "Checking..." : "Check Punctuation") {
  48. viewModel.checkPunctuation()
  49. }
  50. .disabled(!viewModel.canCheck)
  51. }
  52. .frame(maxWidth: 1200, alignment: .topLeading)
  53. .frame(maxWidth: .infinity, alignment: .topLeading)
  54. .padding(.horizontal, dynamicHorizontalPadding)
  55. .padding(.top, AppTheme.brandLabelTopInset)
  56. .padding(.bottom, dynamicBottomPadding)
  57. }
  58. }
  59. }
  60. }
  61. .onChange(of: viewModel.text) { newValue in
  62. if newValue.wordCount > PunctuationCheckerViewModel.maxWords {
  63. viewModel.text = newValue.truncated(toMaxWords: PunctuationCheckerViewModel.maxWords)
  64. }
  65. }
  66. }
  67. private var header: some View {
  68. HStack(alignment: .top) {
  69. VStack(alignment: .leading, spacing: 8) {
  70. Text("Punctuation Checker")
  71. .font(.system(size: 30, weight: .bold))
  72. .foregroundStyle(AppTheme.textPrimary)
  73. HStack(spacing: 0) {
  74. Text("Perfect your ")
  75. .foregroundStyle(AppTheme.textSecondary)
  76. Text("punctuation")
  77. .fontWeight(.semibold)
  78. .foregroundStyle(AppTheme.teal)
  79. Text(". Write with ")
  80. .foregroundStyle(AppTheme.textSecondary)
  81. Text("clarity")
  82. .fontWeight(.semibold)
  83. .foregroundStyle(AppTheme.teal)
  84. Text(".")
  85. .foregroundStyle(AppTheme.textSecondary)
  86. }
  87. .font(.system(size: 15))
  88. }
  89. Spacer()
  90. IconButton {
  91. SettingsGearIcon()
  92. } action: {}
  93. .accessibilityLabel("Settings")
  94. }
  95. }
  96. private var contentCard: some View {
  97. VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
  98. YourContentHeader()
  99. VStack(alignment: .leading, spacing: 6) {
  100. ThinCaretTextEditor(
  101. text: $viewModel.text,
  102. placeholder: "Type or paste your text here and press the check button..."
  103. )
  104. .font(.system(size: 14))
  105. .foregroundStyle(AppTheme.textPrimary)
  106. .frame(maxWidth: .infinity, maxHeight: .infinity)
  107. .background(Color.white)
  108. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  109. .overlay(
  110. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  111. .stroke(AppTheme.inputBorder, lineWidth: 1)
  112. )
  113. Text(viewModel.wordCountLimitLabel)
  114. .font(.system(size: 11))
  115. .foregroundStyle(AppTheme.textMuted)
  116. .frame(maxWidth: .infinity, alignment: .trailing)
  117. }
  118. .frame(maxWidth: .infinity, maxHeight: .infinity)
  119. HStack(spacing: 8) {
  120. ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
  121. viewModel.pasteText()
  122. }
  123. ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {
  124. viewModel.uploadFile()
  125. }
  126. Spacer()
  127. ClearButton {
  128. viewModel.clearText()
  129. }
  130. }
  131. }
  132. .inputContentCardPadding()
  133. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  134. .background(AppTheme.cardBackground)
  135. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  136. .overlay(
  137. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  138. .stroke(AppTheme.border, lineWidth: 1)
  139. )
  140. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  141. }
  142. private var outputCard: some View {
  143. VStack(alignment: .leading, spacing: 12) {
  144. HStack {
  145. Text("Corrected Text")
  146. .font(.system(size: 13, weight: .semibold))
  147. .foregroundStyle(AppTheme.textPrimary)
  148. Spacer()
  149. ToolbarButton(title: "Copy", iconName: "doc.on.doc") {
  150. viewModel.copyCorrectedText()
  151. }
  152. }
  153. Text(viewModel.correctedText)
  154. .font(.system(size: 14))
  155. .foregroundStyle(AppTheme.textPrimary)
  156. .frame(maxWidth: .infinity, alignment: .leading)
  157. .padding(16)
  158. .background(AppTheme.tealLight.opacity(0.35))
  159. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  160. .overlay(
  161. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  162. .stroke(AppTheme.teal.opacity(0.2), lineWidth: 1)
  163. )
  164. }
  165. .padding(AppTheme.contentCardPadding)
  166. .background(AppTheme.cardBackground)
  167. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  168. .overlay(
  169. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  170. .stroke(AppTheme.border, lineWidth: 1)
  171. )
  172. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  173. }
  174. }