| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- import SwiftUI
- struct LanguageTranslatorView: View {
- @StateObject private var viewModel = LanguageTranslatorViewModel()
- var body: some View {
- GeometryReader { proxy in
- 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 dynamicPanelHeight = min(max(proxy.size.height * 0.22, 180), 280)
- ScrollView(.vertical, showsIndicators: false) {
- VStack(alignment: .leading, spacing: 0) {
- header
- .padding(.bottom, 28)
- contentCard(panelHeight: dynamicPanelHeight)
- .padding(.bottom, 24)
- GradientButton(title: "Translate") {
- viewModel.translate()
- }
- .disabled(!viewModel.canTranslate)
- .opacity(viewModel.canTranslate ? 1 : 0.55)
- }
- .frame(maxWidth: 1200, alignment: .topLeading)
- .frame(maxWidth: .infinity, alignment: .topLeading)
- .padding(.horizontal, dynamicHorizontalPadding)
- .padding(.top, dynamicTopPadding)
- .padding(.bottom, dynamicBottomPadding)
- }
- }
- .onChange(of: viewModel.sourceText) { newValue in
- if newValue.count > LanguageTranslatorViewModel.maxCharacters {
- viewModel.sourceText = String(newValue.prefix(LanguageTranslatorViewModel.maxCharacters))
- }
- }
- .onChange(of: viewModel.sourceLanguage) { _ in
- viewModel.syncTargetLanguageIfNeeded()
- }
- }
- private var header: some View {
- HStack(alignment: .top) {
- VStack(alignment: .leading, spacing: 8) {
- Text("Language Translator")
- .font(.system(size: 30, weight: .bold))
- .foregroundStyle(AppTheme.textPrimary)
- HStack(spacing: 0) {
- Text("Speak ")
- .foregroundStyle(AppTheme.textSecondary)
- Text("any language")
- .fontWeight(.semibold)
- .foregroundStyle(AppTheme.teal)
- Text(". Reach ")
- .foregroundStyle(AppTheme.textSecondary)
- Text("everyone")
- .fontWeight(.semibold)
- .foregroundStyle(AppTheme.teal)
- Text(".")
- .foregroundStyle(AppTheme.textSecondary)
- }
- .font(.system(size: 15))
- }
- Spacer()
- IconButton {
- SettingsGearIcon()
- } action: {}
- .accessibilityLabel("Settings")
- }
- }
- private func contentCard(panelHeight: CGFloat) -> some View {
- VStack(alignment: .leading, spacing: 16) {
- Image("YourContentLogo")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(height: 52, alignment: .topLeading)
- .frame(height: 46, alignment: .topLeading)
- .clipped()
- .accessibilityLabel("Your Content")
- sourcePanel(height: panelHeight)
- 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()
- swapLanguagesButton
- ClearButton {
- viewModel.clearText()
- }
- }
- targetPanel(height: panelHeight)
- Text("Press ⌘ + Return to translate")
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.textMuted)
- }
- .padding(22)
- .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 func sourcePanel(height: CGFloat) -> some View {
- VStack(alignment: .leading, spacing: 12) {
- LanguagePicker(
- title: "From",
- selection: $viewModel.sourceLanguage,
- languages: viewModel.sourceLanguageOptions
- )
- ZStack(alignment: .bottomTrailing) {
- ThinCaretTextEditor(text: $viewModel.sourceText, onSubmit: viewModel.translate)
- .font(.system(size: 14))
- .foregroundStyle(AppTheme.textPrimary)
- .frame(minHeight: height, idealHeight: height)
- .background(Color.white)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
- .stroke(AppTheme.inputBorder, lineWidth: 1)
- )
- .overlay(alignment: .topLeading) {
- if viewModel.sourceText.isEmpty {
- Text("Start writing...")
- .font(.system(size: 14))
- .foregroundStyle(AppTheme.textMuted)
- .padding(.leading, AppTheme.textEditorHorizontalInset)
- .padding(.top, AppTheme.textEditorVerticalInset)
- .allowsHitTesting(false)
- }
- }
- Text(viewModel.characterCountLabel)
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.textMuted)
- .padding(.trailing, 16)
- .padding(.bottom, 12)
- }
- }
- .padding(16)
- .background(AppTheme.background)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
- .stroke(AppTheme.border.opacity(0.6), lineWidth: 1)
- )
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
- }
- private func targetPanel(height: CGFloat) -> some View {
- VStack(alignment: .leading, spacing: 12) {
- LanguagePicker(
- title: "To",
- selection: $viewModel.targetLanguage,
- languages: viewModel.targetLanguageOptions
- )
- ZStack(alignment: .topLeading) {
- ThinCaretTextEditor(
- text: $viewModel.translatedText,
- isEditable: false
- )
- .font(.system(size: 14))
- .foregroundStyle(AppTheme.textPrimary)
- .frame(minHeight: height, idealHeight: height)
- .background(Color.white)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
- .stroke(AppTheme.inputBorder, lineWidth: 1)
- )
- if viewModel.translatedText.isEmpty {
- Text(viewModel.isTranslating ? "Translating..." : "Translation will appear here...")
- .font(.system(size: 14))
- .foregroundStyle(AppTheme.textMuted)
- .padding(.leading, AppTheme.textEditorHorizontalInset)
- .padding(.top, AppTheme.textEditorVerticalInset)
- .allowsHitTesting(false)
- }
- }
- }
- .padding(16)
- .background(AppTheme.background)
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
- .stroke(AppTheme.border.opacity(0.6), lineWidth: 1)
- )
- .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
- }
- private var swapLanguagesButton: some View {
- Button {
- viewModel.swapLanguages()
- } label: {
- Image(systemName: "arrow.up.arrow.down")
- .font(.system(size: 12, weight: .semibold))
- .foregroundStyle(AppTheme.textSecondary)
- .padding(.horizontal, 10)
- .padding(.vertical, 7)
- .background(Color.white)
- .clipShape(RoundedRectangle(cornerRadius: 8))
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(AppTheme.border, lineWidth: 1)
- )
- }
- .buttonStyle(.plain)
- .accessibilityLabel("Swap languages")
- }
- }
- private struct LanguagePicker: View {
- let title: String
- @Binding var selection: TranslationLanguage
- let languages: [TranslationLanguage]
- var body: some View {
- ToolbarMenuButton(
- title: "\(title): \(selection.name)",
- iconName: "globe"
- ) {
- ForEach(languages) { language in
- Button {
- selection = language
- } label: {
- HStack {
- Text(language.name)
- if selection == language {
- Spacer()
- Image(systemName: "checkmark")
- }
- }
- }
- }
- }
- .accessibilityLabel("\(title) language: \(selection.name)")
- }
- }
|