diff --git a/mobile/Heygo.xcodeproj/project.xcworkspace/xcuserdata/junv.xcuserdatad/UserInterfaceState.xcuserstate b/mobile/Heygo.xcodeproj/project.xcworkspace/xcuserdata/junv.xcuserdatad/UserInterfaceState.xcuserstate index f9bd4c5..f5a375a 100644 Binary files a/mobile/Heygo.xcodeproj/project.xcworkspace/xcuserdata/junv.xcuserdatad/UserInterfaceState.xcuserstate and b/mobile/Heygo.xcodeproj/project.xcworkspace/xcuserdata/junv.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/mobile/Heygo/Resources/en.lproj/Localizable.strings b/mobile/Heygo/Resources/en.lproj/Localizable.strings index cbd3ee8..4f61241 100644 --- a/mobile/Heygo/Resources/en.lproj/Localizable.strings +++ b/mobile/Heygo/Resources/en.lproj/Localizable.strings @@ -120,6 +120,7 @@ /* Validation Messages */ "alias_already_exists" = "Alias already exists"; "alias_invalid_characters" = "Alias contains invalid characters"; +"invalid_url_format" = "Please enter a valid URL (e.g., https://example.com)"; "tag_already_exists" = "Tag already exists"; /* Error Messages */ diff --git a/mobile/Heygo/Resources/zh-Hans.lproj/Localizable.strings b/mobile/Heygo/Resources/zh-Hans.lproj/Localizable.strings index 0f475a7..99469d7 100644 --- a/mobile/Heygo/Resources/zh-Hans.lproj/Localizable.strings +++ b/mobile/Heygo/Resources/zh-Hans.lproj/Localizable.strings @@ -140,3 +140,9 @@ /* Basic Information */ "basic_information" = "基本信息"; "actions" = "操作"; + +/* Validation Messages */ +"alias_already_exists" = "别名已存在"; +"alias_invalid_characters" = "别名包含无效字符"; +"invalid_url_format" = "请输入有效的网址(例如:https://example.com)"; +"tag_already_exists" = "标签已存在"; diff --git a/mobile/Heygo/Views/LinkFormView.swift b/mobile/Heygo/Views/LinkFormView.swift index fa6d620..440a07e 100644 --- a/mobile/Heygo/Views/LinkFormView.swift +++ b/mobile/Heygo/Views/LinkFormView.swift @@ -16,7 +16,10 @@ struct LinkFormView: View { @State private var tagSearchText: String = "" @State private var isValidatingAlias: Bool = false @State private var aliasValidationMessage: String = "" + @State private var urlValidationMessage: String = "" @State private var showingTagPicker: Bool = false + @State private var saveErrorMessage: String = "" + @State private var showingSaveError: Bool = false @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Tag.name, ascending: true)], @@ -38,7 +41,8 @@ struct LinkFormView: View { var canSave: Bool { return !alias.isEmpty && (selectedLinkType == .custom ? !text.isEmpty : !originalURL.isEmpty) && - aliasValidationMessage.isEmpty + aliasValidationMessage.isEmpty && + urlValidationMessage.isEmpty } var body: some View { @@ -68,6 +72,14 @@ struct LinkFormView: View { } } .pickerStyle(SegmentedPickerStyle()) + .onChange(of: selectedLinkType) { _, newValue in + // Re-validate URL when link type changes + if newValue != .custom { + validateURL(originalURL) + } else { + urlValidationMessage = "" + } + } // URL or Text field based on type if selectedLinkType == .custom { @@ -84,6 +96,24 @@ struct LinkFormView: View { .autocapitalization(.none) .autocorrectionDisabled() .keyboardType(.URL) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(!urlValidationMessage.isEmpty ? Color.orange : Color.clear, lineWidth: 2) + ) + .onChange(of: originalURL) { _, newValue in + validateURL(newValue) + } + + if !urlValidationMessage.isEmpty { + HStack { + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + Text(urlValidationMessage) + .font(.caption) + .foregroundColor(.orange) + } + } if selectedLinkType == .template { Text("template_help") @@ -169,6 +199,14 @@ struct LinkFormView: View { .onAppear { setupForm() } + .alert("Error", isPresented: $showingSaveError) { + Button("OK") { + showingSaveError = false + saveErrorMessage = "" + } + } message: { + Text(saveErrorMessage) + } } private func setupForm() { @@ -179,6 +217,10 @@ struct LinkFormView: View { linkDescription = link.linkDescription ?? "" selectedLinkType = link.linkTypeEnum selectedTags = link.sortedTags + + // Validate existing data + validateAlias(alias) + validateURL(originalURL) } } @@ -214,6 +256,62 @@ struct LinkFormView: View { isValidatingAlias = false } + private func validateURL(_ url: String) { + // Don't validate empty URLs (allow empty for templates with placeholders) + if url.isEmpty { + urlValidationMessage = "" + return + } + + // For templates, allow URLs with placeholders like {query} + if selectedLinkType == .template { + // Check if it's a valid URL pattern with or without placeholders + let urlWithoutPlaceholders = url.replacingOccurrences(of: #"\{[^}]+\}"#, with: "placeholder", options: .regularExpression) + if isValidURL(urlWithoutPlaceholders) { + urlValidationMessage = "" + return + } + } + + // For regular links, validate the URL strictly + if selectedLinkType == .link { + if isValidURL(url) { + urlValidationMessage = "" + return + } + } + + // If we reach here, the URL is invalid + urlValidationMessage = NSLocalizedString("invalid_url_format", comment: "Invalid URL format") + } + + private func isValidURL(_ urlString: String) -> Bool { + // First check if it's a valid URL + guard let url = URL(string: urlString) else { + return false + } + + // Check if it has a scheme + guard let scheme = url.scheme else { + return false + } + + // Allow http, https, and other common schemes + let allowedSchemes = ["http", "https", "ftp", "mailto", "tel", "sms"] + if !allowedSchemes.contains(scheme.lowercased()) { + return false + } + + // Check if it has a host for http/https URLs + if ["http", "https"].contains(scheme.lowercased()) { + guard let host = url.host, !host.isEmpty else { + return false + } + } + + return true + } + private func toggleTag(_ tag: Tag) { if selectedTags.contains(where: { $0.id == tag.id }) { selectedTags.removeAll { $0.id == tag.id } @@ -250,6 +348,23 @@ struct LinkFormView: View { } private func saveLink() { + // Clear any previous error + saveErrorMessage = "" + showingSaveError = false + + // Validate before saving + if !aliasValidationMessage.isEmpty { + saveErrorMessage = aliasValidationMessage + showingSaveError = true + return + } + + if !urlValidationMessage.isEmpty && selectedLinkType != .custom { + saveErrorMessage = urlValidationMessage + showingSaveError = true + return + } + let success: Bool if isEditing, let link = link { @@ -275,6 +390,13 @@ struct LinkFormView: View { if success { presentationMode.wrappedValue.dismiss() + } else { + // Show error from view model + if let errorMessage = linkViewModel.errorMessage { + saveErrorMessage = errorMessage + showingSaveError = true + linkViewModel.errorMessage = nil // Clear the error + } } } }