diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 8c4dfad9f..4a44dd98e 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -8451,6 +8451,74 @@ } } }, + "setting.service.unable_enable %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to enable translation service %@" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to enable translation service %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用%@翻译服务失败" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟用%@翻譯服務失敗" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nemôže aktivovať prekladovú službu %@" + } + } + } + }, + "setting.service.validate.error.empty_translate_result" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fail to validate service since test translation query returned invalid empty result. " + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fail to validate service since test translation query returned invalid empty result. " + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证翻译服务时,返回了无效的空结果" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "驗證翻譯服務時,返回了無效的空結果" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validácia prekladovej služby zlyhala, pretože testový prekladový dotaz vrátil neplatný prázdny výsledok." + } + } + } + }, "setting.tts_service.options.apple" : { "localizations" : { "en" : { diff --git a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift index 40a1b4030..79db2df8a 100644 --- a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift +++ b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift @@ -135,7 +135,7 @@ private struct ServiceItems: View { var body: some View { ForEach(servicesWithID, id: \.1) { service, _ in - ServiceItemView(service: service) + ServiceItemView(service: service, viewModel: viewModel) .tag(service) } .onMove(perform: viewModel.onServiceItemMove) @@ -154,13 +154,14 @@ private struct ServiceItems: View { // MARK: - ServiceItemViewModel +@MainActor private class ServiceItemViewModel: ObservableObject { // MARK: Lifecycle - init(_ service: QueryService) { + init(_ service: QueryService, viewModel: ServiceTabViewModel) { self.service = service - self.isEnable = service.enabled self.name = service.name() + self.viewModel = viewModel cancellables.append( serviceUpdatePublisher @@ -170,17 +171,86 @@ private class ServiceItemViewModel: ObservableObject { ) } + // MARK: Public + + public func enableService() { + // filter + if service.serviceType() == .appleDictionary || service.serviceType() == .apple { + service.enabled = true + service.enabledQuery = true + EZLocalStorage.shared().setService(service, windowType: viewModel.windowType) + viewModel.postUpdateServiceNotification() + return + } + + isValidating = true + + service.validate { [self] result, error in + // check into main thread + DispatchQueue.main.async { + defer { self.isValidating = false } + // Validate existence error + if let error = error { + logInfo("\(self.service.serviceType().rawValue) validate error: \(error)") + self.error = error + self.showErrorAlert = true + return + } + + // If error is nil but result text is also empty, we should report error. + guard let translatedText = result.translatedText, !translatedText.isEmpty else { + logInfo("\(self.service.serviceType().rawValue) validate translated text is empty") + self.showErrorAlert = true + self.error = EZError( + type: .API, + description: String(localized: "setting.service.validate.error.empty_translate_result") + ) + return + } + + // service enabel open the switch and toggle enable status + self.service.enabled = true + self.service.enabledQuery = true + EZLocalStorage.shared().setService(self.service, windowType: self.viewModel.windowType) + self.viewModel.postUpdateServiceNotification() + } + } + } + // MARK: Internal let service: QueryService - @Published var isEnable = false + @Published var isValidating = false @Published var name = "" + @Published var showErrorAlert = false + @Published var error: (any Error)? + + unowned var viewModel: ServiceTabViewModel + + var isEnable: Bool { + get { + service.enabled + } + set { + if newValue { + // validate service enabled + enableService() + } else { // close service + service.enabled = false + EZLocalStorage.shared().setService(service, windowType: viewModel.windowType) + viewModel.postUpdateServiceNotification() + } + } + } + // MARK: Private private var cancellables: [AnyCancellable] = [] + @EnvironmentObject private var serviceTabViewModel: ServiceTabViewModel + private var serviceUpdatePublisher: AnyPublisher { NotificationCenter.default .publisher(for: .serviceHasUpdated) @@ -200,9 +270,9 @@ private class ServiceItemViewModel: ObservableObject { private struct ServiceItemView: View { // MARK: Lifecycle - init(service: QueryService) { + init(service: QueryService, viewModel: ServiceTabViewModel) { self.service = service - self.serviceItemViewModel = ServiceItemViewModel(service) + self.serviceItemViewModel = ServiceItemViewModel(service, viewModel: viewModel) } // MARK: Internal @@ -210,31 +280,49 @@ private struct ServiceItemView: View { let service: QueryService var body: some View { - Toggle(isOn: $serviceItemViewModel.isEnable) { + Group { HStack { - Image(service.serviceType().rawValue) - .resizable() - .scaledToFit() - .frame(width: 20.0, height: 20.0) - Text(service.name()) - .lineLimit(1) - } - } - .onReceive(serviceItemViewModel.$isEnable) { newValue in - guard service.enabled != newValue else { return } - service.enabled = newValue - if newValue { - service.enabledQuery = newValue + HStack { + Image(service.serviceType().rawValue) + .resizable() + .scaledToFit() + .frame(width: 20.0, height: 20.0) + Text(service.name()) + .lineLimit(1) + } + Spacer() + // Use a fixed width container for both controls, to make sure they are center aligned. + ZStack { + if serviceItemViewModel.isValidating { + ProgressView() + .controlSize(.small) + } else { + Toggle( + serviceItemViewModel.service.name(), + isOn: $serviceItemViewModel.isEnable + ) + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.small) // size: 32*18 + } + } + .frame(width: 32) } - EZLocalStorage.shared().setService(service, windowType: viewModel.windowType) - viewModel.postUpdateServiceNotification() } - .toggleStyle(.switch) - .controlSize(.small) .listRowSeparator(.hidden) .listRowInsets(.init()) .padding(.horizontal, 8) .padding(.vertical, 12) + .alert( + "setting.service.unable_enable \(serviceItemViewModel.service.name())", + isPresented: $serviceItemViewModel.showErrorAlert + ) { + Button("ok") { + serviceItemViewModel.showErrorAlert = false + } + } message: { + Text(serviceItemViewModel.error?.localizedDescription ?? "error_unknown") + } } // MARK: Private