| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- import SwiftUI
- struct PunctuationCheckerView: View {
- @StateObject private var viewModel = PunctuationCheckerViewModel()
- @Environment(\.onSettingsTapped) private var onSettingsTapped
- @Environment(\.requirePremiumAccess) private var requirePremiumAccess
- let restoreEntry: HistoryEntry?
- let onRestoreHandled: () -> Void
- init(restoreEntry: HistoryEntry? = nil, onRestoreHandled: @escaping () -> Void = {}) {
- self.restoreEntry = restoreEntry
- self.onRestoreHandled = onRestoreHandled
- }
- var body: some View {
- GeometryReader { proxy in
- let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
- let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
- Group {
- if viewModel.isChecking {
- loadingLayout(
- horizontalPadding: dynamicHorizontalPadding,
- topPadding: AppTheme.brandLabelTopInset,
- bottomPadding: dynamicBottomPadding
- )
- } else if viewModel.hasResults {
- VStack(alignment: .leading, spacing: 0) {
- header
- .padding(.bottom, 12)
- 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, AppTheme.brandLabelTopInset)
- .padding(.bottom, dynamicBottomPadding)
- } else {
- VStack(alignment: .leading, spacing: 0) {
- header
- .padding(.bottom, 24)
- contentCard
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- if let errorMessage = viewModel.errorMessage {
- Text(errorMessage)
- .font(.system(size: 14))
- .foregroundStyle(.red)
- .padding(.top, 12)
- }
- GradientButton(title: viewModel.isChecking ? "Checking..." : "Check Punctuation") {
- guard requirePremiumAccess() else { return }
- viewModel.checkPunctuation()
- }
- .disabled(!viewModel.canCheck)
- .padding(.top, 16)
- }
- .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
- .padding(.horizontal, dynamicHorizontalPadding)
- .padding(.top, AppTheme.brandLabelTopInset)
- .padding(.bottom, dynamicBottomPadding)
- }
- }
- }
- .clearHostingBackground()
- .onChange(of: viewModel.text) { _ in
- viewModel.handleTextChange()
- }
- .onAppear(perform: restoreIfNeeded)
- .onChange(of: restoreEntry?.id) { _ in
- restoreIfNeeded()
- }
- }
- private func restoreIfNeeded() {
- guard let restoreEntry else { return }
- viewModel.restore(from: restoreEntry)
- onRestoreHandled()
- }
- private func loadingLayout(
- horizontalPadding: CGFloat,
- topPadding: CGFloat,
- bottomPadding: CGFloat
- ) -> some View {
- VStack(spacing: 14) {
- ProgressView()
- .controlSize(.regular)
- Text("Loading...")
- .font(.system(size: 16, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Text("Checking punctuation and applying corrections")
- .font(.system(size: 14))
- .foregroundStyle(AppTheme.textSecondary)
- }
- .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .center)
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
- .padding(.horizontal, horizontalPadding)
- .padding(.top, topPadding)
- .padding(.bottom, bottomPadding)
- }
- private var header: some View {
- ToolPageHeader(
- title: "Punctuation Checker",
- showsBackButton: viewModel.hasResults,
- onBack: { viewModel.resetResults() }
- ) {
- HStack(spacing: 0) {
- Text("Perfect your ")
- .foregroundStyle(AppTheme.textSecondary)
- Text("punctuation")
- .fontWeight(.semibold)
- .foregroundStyle(AppTheme.teal)
- Text(". Write with ")
- .foregroundStyle(AppTheme.textSecondary)
- Text("clarity")
- .fontWeight(.semibold)
- .foregroundStyle(AppTheme.teal)
- Text(".")
- .foregroundStyle(AppTheme.textSecondary)
- }
- .font(.system(size: 15))
- } trailing: {
- IconButton(icon: { SettingsGearIcon() }, action: onSettingsTapped)
- .accessibilityLabel("Settings")
- .padding(.top, 8)
- }
- }
- private var contentCard: some View {
- VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
- YourContentHeader()
- VStack(alignment: .leading, spacing: 6) {
- textEditor
- .background(AppTheme.surface)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
- .stroke(AppTheme.inputBorder, lineWidth: 1)
- )
- Text(viewModel.wordCountLimitLabel)
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.textMuted)
- .frame(maxWidth: .infinity, alignment: .trailing)
- }
- .scrollSafeEditorSectionFrame(compact: false)
- HStack(spacing: 8) {
- ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
- viewModel.pasteText()
- }
- ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {
- viewModel.uploadFile()
- }
- Spacer()
- ClearButton {
- viewModel.clearText()
- }
- }
- }
- .inputContentCardPadding()
- .scrollSafeContentCardFrame(compact: false)
- .background(AppTheme.cardBackground)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
- .stroke(AppTheme.border, lineWidth: 1)
- )
- .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
- }
- private var textEditor: some View {
- ThinCaretTextEditor(
- text: $viewModel.text,
- placeholder: "Type or paste your text here and press the check button...",
- maxWords: PunctuationCheckerViewModel.maxWords,
- onWordLimitReached: viewModel.notifyWordLimitReached
- )
- .font(.system(size: 14))
- .foregroundStyle(AppTheme.textPrimary)
- .scrollSafeTextEditorFrame(compact: false)
- }
- private var resultsLayout: some View {
- correctedTextCard
- }
- private var correctedTextCard: some View {
- VStack(alignment: .leading, spacing: 12) {
- HStack {
- Text("Corrected Text")
- .font(.system(size: 13, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Spacer()
- ToolbarButton(title: "Copy", iconName: "doc.on.doc") {
- viewModel.copyCorrectedText()
- }
- .disabled(viewModel.correctedText.isEmpty)
- .opacity(viewModel.correctedText.isEmpty ? 0.55 : 1)
- }
- ScrollView(.vertical, showsIndicators: true) {
- HighlightedCorrectedText(
- originalText: viewModel.text,
- correctedText: viewModel.correctedText,
- corrections: viewModel.issues.map(\.asTextCorrection),
- padding: 16
- )
- }
- .frame(maxHeight: .infinity, alignment: .top)
- .background(AppTheme.tealLight.opacity(0.35))
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
- .stroke(AppTheme.teal.opacity(0.2), lineWidth: 1)
- )
- }
- .padding(AppTheme.contentCardPadding)
- .background(AppTheme.cardBackground)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius)
- .stroke(AppTheme.border, lineWidth: 1)
- )
- .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
- }
- }
|