WordsCountView.swift 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import SwiftUI
  2. struct WordsCountView: View {
  3. @StateObject private var viewModel = WordsCountViewModel()
  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. VStack(alignment: .leading, spacing: 0) {
  9. header
  10. .padding(.bottom, 24)
  11. contentCard
  12. .frame(maxWidth: .infinity, maxHeight: .infinity)
  13. if let errorMessage = viewModel.errorMessage {
  14. Text(errorMessage)
  15. .font(.system(size: 14))
  16. .foregroundStyle(.red)
  17. .padding(.top, 12)
  18. }
  19. }
  20. .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
  21. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  22. .padding(.horizontal, dynamicHorizontalPadding)
  23. .padding(.top, AppTheme.brandLabelTopInset)
  24. .padding(.bottom, dynamicBottomPadding)
  25. }
  26. .onChange(of: viewModel.text) { _ in
  27. viewModel.handleTextChange()
  28. }
  29. }
  30. private var header: some View {
  31. HStack(alignment: .top) {
  32. VStack(alignment: .leading, spacing: 8) {
  33. Text("Words Count")
  34. .font(.system(size: 30, weight: .bold))
  35. .foregroundStyle(AppTheme.textPrimary)
  36. HStack(spacing: 0) {
  37. Text("Count ")
  38. .foregroundStyle(AppTheme.textSecondary)
  39. Text("every word")
  40. .fontWeight(.semibold)
  41. .foregroundStyle(AppTheme.teal)
  42. Text(". Write with ")
  43. .foregroundStyle(AppTheme.textSecondary)
  44. Text("precision")
  45. .fontWeight(.semibold)
  46. .foregroundStyle(AppTheme.teal)
  47. Text(".")
  48. .foregroundStyle(AppTheme.textSecondary)
  49. }
  50. .font(.system(size: 15))
  51. }
  52. Spacer()
  53. IconButton {
  54. SettingsGearIcon()
  55. } action: {}
  56. .accessibilityLabel("Settings")
  57. }
  58. }
  59. private var contentCard: some View {
  60. VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
  61. YourContentHeader()
  62. Text(viewModel.wordCountLabel)
  63. .font(.system(size: 18, weight: .semibold))
  64. .foregroundStyle(AppTheme.textPrimary)
  65. statsRow
  66. VStack(alignment: .leading, spacing: 6) {
  67. ThinCaretTextEditor(
  68. text: $viewModel.text,
  69. placeholder: "Type or paste your text here to count words...",
  70. maxWords: WordsCountViewModel.maxWords,
  71. onWordLimitReached: viewModel.notifyWordLimitReached
  72. )
  73. .font(.system(size: 14))
  74. .foregroundStyle(AppTheme.textPrimary)
  75. .frame(maxWidth: .infinity, maxHeight: .infinity)
  76. .frame(minHeight: 96)
  77. .background(Color.white)
  78. .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
  79. .overlay(
  80. RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
  81. .stroke(AppTheme.inputBorder, lineWidth: 1)
  82. )
  83. Text(viewModel.wordCountLimitLabel)
  84. .font(.system(size: 11))
  85. .foregroundStyle(AppTheme.textMuted)
  86. .frame(maxWidth: .infinity, alignment: .trailing)
  87. }
  88. .frame(maxHeight: .infinity)
  89. HStack(spacing: 8) {
  90. ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
  91. viewModel.pasteText()
  92. }
  93. ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {
  94. viewModel.uploadFile()
  95. }
  96. Spacer()
  97. ClearButton {
  98. viewModel.clearText()
  99. }
  100. }
  101. }
  102. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
  103. .inputContentCardPadding()
  104. .background(AppTheme.cardBackground)
  105. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
  106. .overlay(
  107. RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
  108. .stroke(AppTheme.border, lineWidth: 1)
  109. )
  110. .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
  111. }
  112. private var statsRow: some View {
  113. HStack(spacing: 12) {
  114. StatItem(title: "Words", value: "\(viewModel.wordCount)")
  115. StatItem(title: "Sentences", value: "\(viewModel.sentenceCount)")
  116. StatItem(title: "Paragraphs", value: "\(viewModel.paragraphCount)")
  117. StatItem(title: "Characters", value: "\(viewModel.characterCount)")
  118. Spacer(minLength: 0)
  119. }
  120. .padding(.vertical, 6)
  121. }
  122. }
  123. private struct StatItem: View {
  124. let title: String
  125. let value: String
  126. var body: some View {
  127. VStack(alignment: .leading, spacing: 4) {
  128. Text(value)
  129. .font(.system(size: 20, weight: .semibold))
  130. .foregroundStyle(AppTheme.teal)
  131. Text(title)
  132. .font(.system(size: 12))
  133. .foregroundStyle(AppTheme.textMuted)
  134. }
  135. .frame(minWidth: 96, alignment: .leading)
  136. .padding(.horizontal, 14)
  137. .padding(.vertical, 10)
  138. .background(AppTheme.tealLight)
  139. .clipShape(RoundedRectangle(cornerRadius: 10))
  140. .accessibilityElement(children: .combine)
  141. .accessibilityLabel("\(value) \(title)")
  142. }
  143. }