From b48fd7b82dda51b044e68004135537e2d28d0563 Mon Sep 17 00:00:00 2001 From: Lava <34743145+CanglongCl@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:57:56 -0800 Subject: [PATCH] refactor: provide enable validation for each service --- Easydict/App/Localizable.xcstrings | 113 ++++++---- .../SettingView/Tabs/TabView/ServiceTab.swift | 193 ++++++++++-------- 2 files changed, 184 insertions(+), 122 deletions(-) diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index b6e2913f7..c8c25f77f 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -799,40 +799,6 @@ } } }, - "setting.service.back" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back" - } - }, - "en-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Späť" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "返回" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "返回" - } - } - } - }, "Baidu" : { "extractionState" : "manual", "localizations" : { @@ -1998,6 +1964,17 @@ } } }, + "fail_validate_service %@" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : " Fail to enable @%" + } + } + } + }, "Failed to allocate memory" : { "comment" : "Error reason", "localizations" : { @@ -8314,6 +8291,40 @@ } } }, + "setting.service.back" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Back" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Back" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Späť" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "返回" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "返回" + } + } + } + }, "setting.service.detail.no_configuration %@" : { "localizations" : { "en" : { @@ -8416,6 +8427,38 @@ } } }, + "setting.service.unable_enable %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to enable translation service %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用%@翻译服务失败" + } + } + } + }, + "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. " + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证翻译服务时,返回了无效的空结果" + } + } + } + }, "setting.tts_service.options.apple" : { "localizations" : { "en" : { @@ -10246,4 +10289,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift index fc7bb8ca0..870916e12 100644 --- a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift +++ b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift @@ -15,65 +15,52 @@ struct ServiceTab: View { // MARK: Internal var body: some View { - ZStack { - HStack(alignment: .top, spacing: 0) { - VStack { - WindowTypePicker(windowType: $viewModel.windowType) - .padding() - List(selection: $viewModel.selectedService) { - ServiceItems() - } - .listStyle(.plain) - .scrollIndicators(.never) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .padding(.bottom) - .padding(.horizontal) - .frame(minWidth: 260) - .onReceive(serviceHasUpdatedNotification) { _ in - viewModel.updateServices() - } + HStack(alignment: .top, spacing: 0) { + VStack { + WindowTypePicker(windowType: $viewModel.windowType) + .padding() + List(selection: $viewModel.selectedService) { + ServiceItems() } + .listStyle(.plain) + .scrollIndicators(.never) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding(.bottom) + .padding(.horizontal) + .frame(minWidth: 260) + .onReceive(serviceHasUpdatedNotification) { _ in + viewModel.updateServices() + } + } - Group { - if let service = viewModel.selectedService { - VStack(alignment: .leading) { - Button("setting.service.back") { - viewModel.selectedService = nil + Group { + if let service = viewModel.selectedService { + VStack(alignment: .leading) { + Button("setting.service.back") { + viewModel.selectedService = nil + } + .padding() + + if let view = service.configurationListItems() as? (any View) { + Form { + AnyView(view) } - .padding() - - if let view = service.configurationListItems() as? (any View) { - Form { - AnyView(view) - } - .formStyle(.grouped) - } else { + .formStyle(.grouped) + } else { + Spacer() + HStack { Spacer() - HStack { - Spacer() - Text("setting.service.detail.no_configuration \(service.name())") - Spacer() - } + Text("setting.service.detail.no_configuration \(service.name())") Spacer() } + Spacer() } - } else { - WindowConfigurationView(windowType: viewModel.windowType) } + } else { + WindowConfigurationView(windowType: viewModel.windowType) } - .layoutPriority(1) - } - - if viewModel.isValidating { - VStack { - ProgressView() - .controlSize(.small) - .progressViewStyle(.circular) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black.opacity(0.4)) - .zIndex(1) } + .layoutPriority(1) } .environmentObject(viewModel) } @@ -98,8 +85,6 @@ private class ServiceTabViewModel: ObservableObject { // MARK: Internal - @Published var isValidating: Bool = false - @Published var selectedService: QueryService? @Published private(set) var services: [QueryService] @@ -150,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) @@ -169,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 @@ -187,7 +173,7 @@ private class ServiceItemViewModel: ObservableObject { // MARK: Public - public func handleValidateServiceEnable(viewModel: ServiceTabViewModel) { + public func enableService() { // filter if service.serviceType() == .appleDictionary || service.serviceType() == .apple { @@ -198,32 +184,37 @@ private class ServiceItemViewModel: ObservableObject { return } - viewModel.isValidating = true + 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 { - self.isEnable = false logInfo("\(self.service.serviceType().rawValue) validate error: \(error)") - viewModel.isValidating = false + 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 { - self.isEnable = false logInfo("\(self.service.serviceType().rawValue) validate translated text is empty") - viewModel.isValidating = false + 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: viewModel.windowType) - viewModel.postUpdateServiceNotification() - viewModel.isValidating = false + EZLocalStorage.shared().setService(self.service, windowType: self.viewModel.windowType) + self.viewModel.postUpdateServiceNotification() + self.objectWillChange.send() } } } @@ -232,9 +223,29 @@ private class ServiceItemViewModel: ObservableObject { 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] = [] @@ -260,9 +271,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 @@ -270,34 +281,42 @@ 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 } - // open service - if newValue { - // validate service enabled - serviceItemViewModel.handleValidateServiceEnable(viewModel: viewModel) - } else { // close service - service.enabled = false - EZLocalStorage.shared().setService(service, windowType: viewModel.windowType) - viewModel.postUpdateServiceNotification() + HStack { + Image(service.serviceType().rawValue) + .resizable() + .scaledToFit() + .frame(width: 20.0, height: 20.0) + Text(service.name()) + .lineLimit(1) + } + Spacer() + if serviceItemViewModel.isValidating { + ProgressView() + .controlSize(.small) + } else { + Toggle(serviceItemViewModel.service.name(), isOn: $serviceItemViewModel.isEnable) + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.small) + } } } - .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