diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index a0f56b776..476df1056 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -253,6 +253,8 @@ 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */; }; A0B65CA0F31AC8ECFB8347CC /* Pods_EasydictTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 378E73A7EA8FC8FB9C975A63 /* Pods_EasydictTests.framework */; }; B87AC7E36367075BA5D13234 /* Pods_Easydict.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */; }; + C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415C0AC2B450D4800A9D231 /* GeminiService.swift */; }; + C415C0AF2B450F9200A9D231 /* GeminiTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415C0AE2B450F9200A9D231 /* GeminiTranslateType.swift */; }; C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01E82B12B3C80025EE8E /* TencentService.swift */; }; C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */; }; C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */; }; @@ -728,6 +730,8 @@ 9672D7D02B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MASShortcutBinder+EZMASShortcutBinder.h"; sourceTree = ""; }; 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MASShortcutBinder+EZMASShortcutBinder.m"; sourceTree = ""; }; A230E9A2358C7FBC7FB26189 /* Pods-EasydictTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EasydictTests.debug.xcconfig"; path = "Target Support Files/Pods-EasydictTests/Pods-EasydictTests.debug.xcconfig"; sourceTree = ""; }; + C415C0AC2B450D4800A9D231 /* GeminiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiService.swift; sourceTree = ""; }; + C415C0AE2B450F9200A9D231 /* GeminiTranslateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GeminiTranslateType.swift; path = ../../../../../../../GeminiTranslateType.swift; sourceTree = ""; }; C4DD01E82B12B3C80025EE8E /* TencentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentService.swift; sourceTree = ""; }; C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentResponse.swift; sourceTree = ""; }; C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentTranslateType.swift; sourceTree = ""; }; @@ -1276,6 +1280,7 @@ isa = PBXGroup; children = ( 62E2BF462B4082BA00E42D38 /* Ali */, + C415C0AB2B450C4500A9D231 /* Gemini */, 17BCAEF22B0DFF9000A7D372 /* Niutrans */, 2746AEBF2AF95040005FE0A1 /* Caiyun */, C4DD01E72B12B3B00025EE8E /* Tencent */, @@ -2090,6 +2095,15 @@ path = Pods; sourceTree = ""; }; + C415C0AB2B450C4500A9D231 /* Gemini */ = { + isa = PBXGroup; + children = ( + C415C0AC2B450D4800A9D231 /* GeminiService.swift */, + C415C0AE2B450F9200A9D231 /* GeminiTranslateType.swift */, + ); + path = Gemini; + sourceTree = ""; + }; C4A40A9B2AC0168400B8E6EF /* Recovered References */ = { isa = PBXGroup; children = ( @@ -2536,6 +2550,7 @@ 03542A522937B69200C34C33 /* EZYoudaoTranslateResponse.m in Sources */, 03B0230129231FA6001C7E63 /* EZQueryView.m in Sources */, 03542A3D2937AF4F00C34C33 /* EZQueryResult.m in Sources */, + C415C0AF2B450F9200A9D231 /* GeminiTranslateType.swift in Sources */, 03262C1F29EF8EE500EFECA0 /* EZPrivacyViewController.m in Sources */, 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */, 03BDA7BF2A26DA280079D04F /* NSScanner+EscapedScanning.m in Sources */, @@ -2610,6 +2625,7 @@ 039CC90D292F664E0037B91E /* NSObject+EZWindowType.m in Sources */, 03B0232229231FA6001C7E63 /* NSImage+MM.m in Sources */, 03BB2DEF29F59C8A00447EDD /* EZSymbolImageButton.m in Sources */, + C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */, 62A2D03F2A82967F007EEB01 /* EZBingRequest.m in Sources */, 03BDA7BE2A26DA280079D04F /* XPMCountedArgument.m in Sources */, 03D35DAA2AA6C49B00B023FE /* NSString+EZRegex.m in Sources */, diff --git a/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json new file mode 100644 index 000000000..f0bbce672 --- /dev/null +++ b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Gemini.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png new file mode 100644 index 000000000..de0d56846 Binary files /dev/null and b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png differ diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 0ee5bae52..d119d72bf 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1243,6 +1243,23 @@ } } }, + "gemini_translate" : { + "comment" : "The name of Gemini Translate", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gemini Translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Gemini 翻译" + } + } + } + }, "GitHub:" : { "localizations" : { "zh-Hans" : { diff --git a/Easydict/Feature/Service/Gemini/GeminiService.swift b/Easydict/Feature/Service/Gemini/GeminiService.swift new file mode 100644 index 000000000..2acde01c2 --- /dev/null +++ b/Easydict/Feature/Service/Gemini/GeminiService.swift @@ -0,0 +1,94 @@ +// +// GeminiService.swift +// Easydict +// +// Created by Jerry on 2024-01-02. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import GoogleGenerativeAI + +@objc(EZGeminiService) +public final class GeminiService: QueryService { + override public func serviceType() -> ServiceType { + .gemini + } + + override public func link() -> String? { + "https://bard.google.com/chat" + } + + override public func name() -> String { + NSLocalizedString("gemini_translate", comment: "The name of Gemini Translate") + } + + override public func supportLanguagesDictionary() -> MMOrderedDictionary { + // TODO: Replace MMOrderedDictionary. + let orderedDict = MMOrderedDictionary() + GeminiTranslateType.supportLanguagesDictionary.forEach { key, value in + orderedDict.setObject(value as NSString, forKey: key.rawValue as NSString) + } + return orderedDict + } + + override public func ocr(_: EZQueryModel) async throws -> EZOCRResult { + NSLog("Gemini Translate does not support OCR") + throw QueryServiceError.notSupported + } + + override public func needPrivateAPIKey() -> Bool { + true + } + + override public func hasPrivateAPIKey() -> Bool { + if apiKey == defaultAPIKey { + return false + } + return true + } + + private let defaultAPIKey = "" /* .decryptAES() */ + + // easydict://writeKeyValue?EZGeminiAPIKey=xxx + private var apiKey: String { + let apiKey = UserDefaults.standard.string(forKey: EZGeminiAPIKey) + if let apiKey, !apiKey.isEmpty { + return apiKey + } else { + return defaultAPIKey + } + } + + override public func autoConvertTraditionalChinese() -> Bool { + true + } + + override public func translate(_ text: String, from: Language, to: Language, completion: @escaping (EZQueryResult, Error?) -> Void) { + Task { + // https://github.com/google/generative-ai-swift + do { + var resultString = "" + let prompt = "translate this \(from.rawValue) text into \(to.rawValue): \(text)" + print("gemini prompt: \(prompt)") + let model = GenerativeModel(name: "gemini-pro", apiKey: apiKey) + let outputContentStream = model.generateContentStream(prompt) + + // stream response + for try await outputContent in outputContentStream { + guard let line = outputContent.text else { + return + } + + print("gemini response: \(line)") + resultString += line + result.translatedResults = [resultString] + completion(result, nil) + } + } catch { + print(error.localizedDescription) + completion(result, error) + } + } + } +} diff --git a/Easydict/Feature/Service/Model/EZConstKey.h b/Easydict/Feature/Service/Model/EZConstKey.h index be00eeea2..cf422670b 100644 --- a/Easydict/Feature/Service/Model/EZConstKey.h +++ b/Easydict/Feature/Service/Model/EZConstKey.h @@ -35,6 +35,7 @@ static NSString *const EZNiuTransAPIKey = @"EZNiuTransAPIKey"; static NSString *const EZCaiyunToken = @"EZCaiyunToken"; static NSString *const EZTencentSecretId = @"EZTencentSecretId"; static NSString *const EZTencentSecretKey = @"EZTencentSecretKey"; +static NSString *const EZGeminiAPIKey = @"EZGeminiAPIKey"; static NSString *const EZAliAccessKeyId = @"EZAliAccessKeyId"; static NSString *const EZAliAccessKeySecret = @"EZAliAccessKeySecret"; diff --git a/Easydict/Feature/Service/Model/EZEnumTypes.h b/Easydict/Feature/Service/Model/EZEnumTypes.h index 28dbeea3a..cad41e525 100644 --- a/Easydict/Feature/Service/Model/EZEnumTypes.h +++ b/Easydict/Feature/Service/Model/EZEnumTypes.h @@ -43,6 +43,7 @@ FOUNDATION_EXPORT EZServiceType const EZServiceTypeNiuTrans; FOUNDATION_EXPORT EZServiceType const EZServiceTypeCaiyun; FOUNDATION_EXPORT EZServiceType const EZServiceTypeTencent; FOUNDATION_EXPORT EZServiceType const EZServiceTypeAli; +FOUNDATION_EXPORT EZServiceType const EZServiceTypeGemini; FOUNDATION_EXPORT NSString *const EZQueryTextTypeKey; FOUNDATION_EXPORT NSString *const EZIntelligentQueryTextTypeKey; diff --git a/Easydict/Feature/Service/Model/EZEnumTypes.m b/Easydict/Feature/Service/Model/EZEnumTypes.m index 76e778da2..879b9102b 100644 --- a/Easydict/Feature/Service/Model/EZEnumTypes.m +++ b/Easydict/Feature/Service/Model/EZEnumTypes.m @@ -22,6 +22,7 @@ NSString *const EZServiceTypeCaiyun = @"Caiyun"; NSString *const EZServiceTypeTencent = @"Tencent"; NSString *const EZServiceTypeAli = @"Alibaba"; +NSString *const EZServiceTypeGemini = @"Gemini"; NSString *const EZServiceTypeAppleDictionary = @"AppleDictionary"; diff --git a/Easydict/Feature/Service/Model/EZServiceTypes.m b/Easydict/Feature/Service/Model/EZServiceTypes.m index d63ed6649..c40d7ceba 100644 --- a/Easydict/Feature/Service/Model/EZServiceTypes.m +++ b/Easydict/Feature/Service/Model/EZServiceTypes.m @@ -63,6 +63,7 @@ + (instancetype)allocWithZone:(struct _NSZone *)zone { EZServiceTypeCaiyun, [EZCaiyunService class], EZServiceTypeTencent, [EZTencentService class], EZServiceTypeAli, [EZAliService class], + EZServiceTypeGemini, [EZGeminiService class], nil]; return allServiceDict; } diff --git a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m index cd8ba1630..2cf47dc60 100644 --- a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m +++ b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m @@ -219,6 +219,7 @@ - (NSArray *)allowedReadWriteKeys { EZAliAccessKeyId, EZAliAccessKeySecret, + EZGeminiAPIKey, EZIntelligentQueryModeKey, ];