|
@@ -3,69 +3,108 @@ import SwiftUI
|
|
|
struct GrammarCheckerView: View {
|
|
struct GrammarCheckerView: View {
|
|
|
@StateObject private var viewModel = GrammarCheckerViewModel()
|
|
@StateObject private var viewModel = GrammarCheckerViewModel()
|
|
|
|
|
|
|
|
|
|
+ private let panelBackground = Color(red: 0.96, green: 0.97, blue: 0.98)
|
|
|
|
|
+
|
|
|
var body: some View {
|
|
var body: some View {
|
|
|
GeometryReader { proxy in
|
|
GeometryReader { proxy in
|
|
|
let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
|
|
let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
|
|
|
- let dynamicTopPadding = min(max(proxy.size.height * 0.05, 24), 44)
|
|
|
|
|
- let dynamicBottomPadding = min(max(proxy.size.height * 0.06, 28), 56)
|
|
|
|
|
|
|
+ let dynamicTopPadding = min(max(proxy.size.height * 0.015, 8), 16)
|
|
|
|
|
+ let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
|
|
|
let dynamicEditorHeight = min(max(proxy.size.height * 0.43, 260), 560)
|
|
let dynamicEditorHeight = min(max(proxy.size.height * 0.43, 260), 560)
|
|
|
|
|
|
|
|
- ScrollView(.vertical, showsIndicators: false) {
|
|
|
|
|
- VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
- header
|
|
|
|
|
- .padding(.bottom, 28)
|
|
|
|
|
|
|
+ Group {
|
|
|
|
|
+ if viewModel.hasResults {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
+ header
|
|
|
|
|
+ .padding(.bottom, 20)
|
|
|
|
|
+
|
|
|
|
|
+ resultsLayout
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ .padding(.horizontal, dynamicHorizontalPadding)
|
|
|
|
|
+ .padding(.top, dynamicTopPadding)
|
|
|
|
|
+ .padding(.bottom, dynamicBottomPadding)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ScrollView(.vertical, showsIndicators: false) {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
+ header
|
|
|
|
|
+ .padding(.bottom, 24)
|
|
|
|
|
+
|
|
|
|
|
+ contentCard(editorHeight: dynamicEditorHeight)
|
|
|
|
|
+ .padding(.bottom, 24)
|
|
|
|
|
|
|
|
- contentCard(editorHeight: dynamicEditorHeight)
|
|
|
|
|
- .padding(.bottom, 24)
|
|
|
|
|
|
|
+ if let errorMessage = viewModel.errorMessage {
|
|
|
|
|
+ Text(errorMessage)
|
|
|
|
|
+ .font(.system(size: 14))
|
|
|
|
|
+ .foregroundStyle(.red)
|
|
|
|
|
+ .padding(.bottom, 12)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- GradientButton(title: "Check Grammar") {
|
|
|
|
|
- viewModel.checkGrammar()
|
|
|
|
|
|
|
+ GradientButton(title: viewModel.isChecking ? "Checking..." : "Check Grammar") {
|
|
|
|
|
+ viewModel.checkGrammar()
|
|
|
|
|
+ }
|
|
|
|
|
+ .disabled(!viewModel.canCheck)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: 1200, alignment: .topLeading)
|
|
|
|
|
+ .frame(maxWidth: .infinity, alignment: .topLeading)
|
|
|
|
|
+ .padding(.horizontal, dynamicHorizontalPadding)
|
|
|
|
|
+ .padding(.top, dynamicTopPadding)
|
|
|
|
|
+ .padding(.bottom, dynamicBottomPadding)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- .frame(maxWidth: 1200, alignment: .topLeading)
|
|
|
|
|
- .frame(maxWidth: .infinity, alignment: .topLeading)
|
|
|
|
|
- .padding(.horizontal, dynamicHorizontalPadding)
|
|
|
|
|
- .padding(.top, dynamicTopPadding)
|
|
|
|
|
- .padding(.bottom, dynamicBottomPadding)
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.onChange(of: viewModel.text) { newValue in
|
|
.onChange(of: viewModel.text) { newValue in
|
|
|
if newValue.count > GrammarCheckerViewModel.maxCharacters {
|
|
if newValue.count > GrammarCheckerViewModel.maxCharacters {
|
|
|
viewModel.text = String(newValue.prefix(GrammarCheckerViewModel.maxCharacters))
|
|
viewModel.text = String(newValue.prefix(GrammarCheckerViewModel.maxCharacters))
|
|
|
}
|
|
}
|
|
|
|
|
+ if viewModel.hasResults {
|
|
|
|
|
+ viewModel.resetResults()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private var header: some View {
|
|
private var header: some View {
|
|
|
- HStack(alignment: .top) {
|
|
|
|
|
- VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
- Text("Grammar Checker")
|
|
|
|
|
- .font(.system(size: 30, weight: .bold))
|
|
|
|
|
- .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
-
|
|
|
|
|
- HStack(spacing: 0) {
|
|
|
|
|
- Text("Write ")
|
|
|
|
|
- .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
- Text("better")
|
|
|
|
|
- .fontWeight(.semibold)
|
|
|
|
|
- .foregroundStyle(AppTheme.teal)
|
|
|
|
|
- Text(". Sound ")
|
|
|
|
|
- .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
- Text("smarter")
|
|
|
|
|
- .fontWeight(.semibold)
|
|
|
|
|
- .foregroundStyle(AppTheme.teal)
|
|
|
|
|
- Text(".")
|
|
|
|
|
- .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 16) {
|
|
|
|
|
+ if viewModel.hasResults {
|
|
|
|
|
+ IconButton(iconName: "arrow.left") {
|
|
|
|
|
+ viewModel.resetResults()
|
|
|
}
|
|
}
|
|
|
- .font(.system(size: 15))
|
|
|
|
|
|
|
+ .accessibilityLabel("Go back")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- Spacer()
|
|
|
|
|
|
|
+ HStack(alignment: .top) {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
+ Text("Grammar Checker")
|
|
|
|
|
+ .font(.system(size: 30, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+
|
|
|
|
|
+ HStack(spacing: 0) {
|
|
|
|
|
+ Text("Write ")
|
|
|
|
|
+ .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
+ Text("better")
|
|
|
|
|
+ .fontWeight(.semibold)
|
|
|
|
|
+ .foregroundStyle(AppTheme.teal)
|
|
|
|
|
+ Text(". Sound ")
|
|
|
|
|
+ .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
+ Text("smarter")
|
|
|
|
|
+ .fontWeight(.semibold)
|
|
|
|
|
+ .foregroundStyle(AppTheme.teal)
|
|
|
|
|
+ Text(".")
|
|
|
|
|
+ .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
+ }
|
|
|
|
|
+ .font(.system(size: 15))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Spacer()
|
|
|
|
|
|
|
|
- IconButton {
|
|
|
|
|
- SettingsGearIcon()
|
|
|
|
|
- } action: {}
|
|
|
|
|
- .accessibilityLabel("Settings")
|
|
|
|
|
|
|
+ IconButton {
|
|
|
|
|
+ SettingsGearIcon()
|
|
|
|
|
+ } action: {}
|
|
|
|
|
+ .accessibilityLabel("Settings")
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -131,4 +170,116 @@ struct GrammarCheckerView: View {
|
|
|
)
|
|
)
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
|
|
.shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private var resultsLayout: some View {
|
|
|
|
|
+ HStack(alignment: .top, spacing: 20) {
|
|
|
|
|
+ correctedContentPanel
|
|
|
|
|
+ grammarReportPanel
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var correctedContentPanel: some View {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 14) {
|
|
|
|
|
+ Text("Your Content")
|
|
|
|
|
+ .font(.system(size: 18, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
+ Text(viewModel.correctedText)
|
|
|
|
|
+ .font(.system(size: 14))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ .padding(20)
|
|
|
|
|
+
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ ToolbarButton(title: "Copy Text", iconName: "doc.on.doc") {
|
|
|
|
|
+ viewModel.copyCorrectedText()
|
|
|
|
|
+ }
|
|
|
|
|
+ Spacer()
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.horizontal, 16)
|
|
|
|
|
+ .padding(.bottom, 16)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxHeight: .infinity)
|
|
|
|
|
+ .background(panelBackground)
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
|
|
|
|
|
+ .stroke(AppTheme.border, lineWidth: 1)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var grammarReportPanel: some View {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 14) {
|
|
|
|
|
+ Text("Grammar Report")
|
|
|
|
|
+ .font(.system(size: 18, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 14) {
|
|
|
|
|
+ Text(viewModel.issueCountLabel)
|
|
|
|
|
+ .font(.system(size: 15, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+
|
|
|
|
|
+ if viewModel.issues.isEmpty {
|
|
|
|
|
+ Text("No grammar issues found.")
|
|
|
|
|
+ .font(.system(size: 14))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ScrollView(.vertical, showsIndicators: false) {
|
|
|
|
|
+ VStack(spacing: 10) {
|
|
|
|
|
+ ForEach(viewModel.issues) { issue in
|
|
|
|
|
+ issueCard(issue)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxHeight: .infinity)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .frame(width: 300)
|
|
|
|
|
+ .frame(maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ .background(panelBackground)
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
|
|
|
|
|
+ .stroke(AppTheme.border, lineWidth: 1)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func issueCard(_ issue: GrammarIssue) -> some View {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
|
|
+ Text("Correct the grammar error")
|
|
|
|
|
+ .font(.system(size: 13, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+
|
|
|
|
|
+ HStack(spacing: 18) {
|
|
|
|
|
+ Text(issue.original)
|
|
|
|
|
+ .font(.system(size: 14, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+
|
|
|
|
|
+ Text(issue.suggestion)
|
|
|
|
|
+ .font(.system(size: 14, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.teal)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Text(issue.explanation)
|
|
|
|
|
+ .font(.system(size: 12))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
+ .padding(.horizontal, 14)
|
|
|
|
|
+ .padding(.vertical, 12)
|
|
|
|
|
+ .background(Color.white)
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 10))
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 10)
|
|
|
|
|
+ .stroke(AppTheme.border.opacity(0.6), lineWidth: 1)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|