diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 35f1a932d..c56ec8c9b 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -513,6 +513,7 @@ A1C1A10130ABCDEF00112233 /* ClaudeSSEParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C1A10030ABCDEF00112233 /* ClaudeSSEParserTests.swift */; }; A1C1A10430ABCDEF00112233 /* ClaudeSSEParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C1A10330ABCDEF00112233 /* ClaudeSSEParser.swift */; }; A1D1807B2F8D100100B1C0D1 /* ThrottleGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D1807A2F8D100100B1C0D1 /* ThrottleGate.swift */; }; + A1D1807F2F8D100100B1C0D1 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D1807F2F8D000100B1C0D1 /* NetworkSession.swift */; }; A1D1807E2F8D100100B1C0D1 /* ThrottleGateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D1807D2F8D100100B1C0D1 /* ThrottleGateTests.swift */; }; A2016EDA90ED45B5B66FA986 /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14921B68C1D645268F93668E /* AnalyticsService.swift */; }; A94F9CB9D704426DB91B25D3 /* DeviceSystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54300FF4F544CC2A180FE53 /* DeviceSystemInfo.swift */; }; @@ -1197,6 +1198,7 @@ A1C1A10030ABCDEF00112233 /* ClaudeSSEParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeSSEParserTests.swift; sourceTree = ""; }; A1C1A10330ABCDEF00112233 /* ClaudeSSEParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeSSEParser.swift; sourceTree = ""; }; A1D1807A2F8D100100B1C0D1 /* ThrottleGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThrottleGate.swift; sourceTree = ""; }; + A1D1807F2F8D000100B1C0D1 /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = ""; }; A1D1807D2F8D100100B1C0D1 /* ThrottleGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThrottleGateTests.swift; 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 = ""; }; A6C26D557A3746598059449DAD170FE1 /* OrderedDictionary+Variadic.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OrderedDictionary+Variadic.m"; sourceTree = ""; }; @@ -3436,6 +3438,7 @@ EA9943E62B534D7C00EE7B97 /* Extensions */, 03538DF12D25AAD1005E56A8 /* CookieManager.swift */, A1D1807A2F8D100100B1C0D1 /* ThrottleGate.swift */, + A1D1807F2F8D000100B1C0D1 /* NetworkSession.swift */, 03A3E1542BEBDB2000E7E210 /* Throttler.swift */, ); path = Utility; @@ -3996,6 +3999,7 @@ 039CBE872D9AFBF3009A04DC /* ChineseGenreAnalyzer.swift in Sources */, 03BDA7C12A26DA280079D04F /* XPMArgumentParser.m in Sources */, A1D1807B2F8D100100B1C0D1 /* ThrottleGate.swift in Sources */, + A1D1807F2F8D100100B1C0D1 /* NetworkSession.swift in Sources */, 03A3E1552BEBDB2000E7E210 /* Throttler.swift in Sources */, 03D9D6FF2D8C186F009E0CEC /* NSScreen+Extention.swift in Sources */, 03991166292A8A4400E1B06D /* EZTitleBarMoveView.m in Sources */, diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 6e351dd28..76d2f81ea 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -7175,6 +7175,118 @@ } } }, + "setting.advance.header.local_proxy" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Local Proxy" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lokálny proxy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "本地代理" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "本地代理" + } + } + } + }, + "setting.advance.http_proxy_url" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "HTTP Proxy URL" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "HTTP Proxy URL" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "HTTP 代理地址" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "HTTP 代理地址" + } + } + } + }, + "setting.advance.http_proxy_url_desc" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supports http:// and socks5:// schemes" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podporuje schémy http:// a socks5://" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "支持 http:// 和 socks5:// 格式" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "支持 http:// 和 socks5:// 格式" + } + } + } + }, + "setting.advance.http_proxy_url_footer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Routes all translation requests through the specified proxy. Leave empty to use system default networking." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Presmeruje všetky prekladové požiadavky cez zadaný proxy. Nechajte prázdne pre predvolené sieťové nastavenia." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有翻译服务请求将通过指定的代理发出。留空则使用系统默认网络设置。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有翻譯服務請求將透過指定的代理發出。留空則使用系統預設網路設定。" + } + } + } + }, "setting.advance.header.ocr_settings" : { "localizations" : { "en" : { diff --git a/Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift b/Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift index 67348939a..32e0c85c3 100644 --- a/Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift +++ b/Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift @@ -147,6 +147,11 @@ extension Defaults.Keys { static var enableHTTPServer = Key("enableHTTPServer", default: false) static var httpPort = Key("httpPort", default: "8080") + /// Local HTTP/SOCKS proxy URL used for all translation service requests. + /// Supports formats: `http://host:port`, `socks5://host:port`, or `host:port`. + /// Leave empty to use system default networking (no custom proxy). + static var httpProxyURL = Key("httpProxyURL", default: "") + static var enableAppleOfflineTranslation = Key( "enableAppleOfflineTranslation", default: false ) diff --git a/Easydict/Swift/Service/Ali/AliService.swift b/Easydict/Swift/Service/Ali/AliService.swift index b53b791bb..75c119dd1 100644 --- a/Easydict/Swift/Service/Ali/AliService.swift +++ b/Easydict/Swift/Service/Ali/AliService.swift @@ -204,7 +204,7 @@ class AliService: QueryService { result = currentResult } - let request = AF.request("https://mt.aliyuncs.com", method: .post, parameters: param) + let request = EAF.request("https://mt.aliyuncs.com", method: .post, parameters: param) queryModel.setStop({ request.cancel() diff --git a/Easydict/Swift/Service/Baidu/BaiduApiTranslate.swift b/Easydict/Swift/Service/Baidu/BaiduApiTranslate.swift index f17eba05f..e30c22078 100644 --- a/Easydict/Swift/Service/Baidu/BaiduApiTranslate.swift +++ b/Easydict/Swift/Service/Baidu/BaiduApiTranslate.swift @@ -61,7 +61,7 @@ class BaiduApiTranslate: NSObject { "sign": signMd5, ] - let request = AF.request( + let request = EAF.request( "https://fanyi-api.baidu.com/api/trans/vip/translate", method: .post, parameters: param, diff --git a/Easydict/Swift/Service/Baidu/BaiduService+OCR.swift b/Easydict/Swift/Service/Baidu/BaiduService+OCR.swift index f632965fa..16b345b8b 100644 --- a/Easydict/Swift/Service/Baidu/BaiduService+OCR.swift +++ b/Easydict/Swift/Service/Baidu/BaiduService+OCR.swift @@ -53,7 +53,7 @@ extension BaiduService { let url = "\(kBaiduTranslateURL)/getocr" do { - let response = try await AF.upload( + let response = try await EAF.upload( multipartFormData: { formData in formData.append(imageData, withName: "image", fileName: "blob", mimeType: "image/png") formData.append(Data((fromLang ?? "").utf8), withName: "from") diff --git a/Easydict/Swift/Service/Baidu/BaiduService.swift b/Easydict/Swift/Service/Baidu/BaiduService.swift index 9d75a2c9e..16d612583 100644 --- a/Easydict/Swift/Service/Baidu/BaiduService.swift +++ b/Easydict/Swift/Service/Baidu/BaiduService.swift @@ -314,7 +314,7 @@ final class BaiduService: QueryService { let url = "\(kBaiduTranslateURL)/langdetect" do { - let response = try await AF.request( + let response = try await EAF.request( url, method: .post, parameters: ["query": queryString], diff --git a/Easydict/Swift/Service/Bing/BingRequest.swift b/Easydict/Swift/Service/Bing/BingRequest.swift index 9b44625ef..71921c03e 100644 --- a/Easydict/Swift/Service/Bing/BingRequest.swift +++ b/Easydict/Swift/Service/Bing/BingRequest.swift @@ -490,7 +490,7 @@ class BingRequest { ) -> DataRequest { let encoding: ParameterEncoding = method == .get ? URLEncoding.default : URLEncoding.httpBody - let request = AF.request( + let request = EAF.request( url, method: method, parameters: parameters, diff --git a/Easydict/Swift/Service/Caiyun/CaiyunService.swift b/Easydict/Swift/Service/Caiyun/CaiyunService.swift index e862aef31..42b3330f7 100644 --- a/Easydict/Swift/Service/Caiyun/CaiyunService.swift +++ b/Easydict/Swift/Service/Caiyun/CaiyunService.swift @@ -79,7 +79,7 @@ public final class CaiyunService: QueryService { result = currentResult } - let request = AF.request( + let request = EAF.request( apiEndPoint, method: .post, parameters: parameters, diff --git a/Easydict/Swift/Service/Claude/ClaudeService.swift b/Easydict/Swift/Service/Claude/ClaudeService.swift index bff017cb4..5923e1520 100644 --- a/Easydict/Swift/Service/Claude/ClaudeService.swift +++ b/Easydict/Swift/Service/Claude/ClaudeService.swift @@ -90,7 +90,7 @@ public final class ClaudeService: StreamService { ) let urlRequest = try createURLRequest(body: requestBody) - let (asyncBytes, response) = try await URLSession.shared.bytes(for: urlRequest) + let (asyncBytes, response) = try await EURLSession.bytes(for: urlRequest) try await validateHTTPResponse(response, asyncBytes: asyncBytes) try await processStreamBytes(asyncBytes, continuation: continuation) diff --git a/Easydict/Swift/Service/DeepL/DeepLService+Translate.swift b/Easydict/Swift/Service/DeepL/DeepLService+Translate.swift index 7c23815f6..d4496893f 100644 --- a/Easydict/Swift/Service/DeepL/DeepLService+Translate.swift +++ b/Easydict/Swift/Service/DeepL/DeepLService+Translate.swift @@ -85,7 +85,7 @@ extension DeepLService { let startTime = CFAbsoluteTimeGetCurrent() - let dataRequest = AF.request(request) + let dataRequest = EAF.request(request) .validate(statusCode: 200 ..< 300) dataRequest.responseData { [weak self] response in @@ -196,7 +196,7 @@ extension DeepLService { let authorization = "DeepL-Auth-Key \(authKey)" let startTime = CFAbsoluteTimeGetCurrent() - let request = AF.request( + let request = EAF.request( url, method: .post, parameters: params, diff --git a/Easydict/Swift/Service/Doubao/DoubaoService.swift b/Easydict/Swift/Service/Doubao/DoubaoService.swift index be378bec8..da764778e 100644 --- a/Easydict/Swift/Service/Doubao/DoubaoService.swift +++ b/Easydict/Swift/Service/Doubao/DoubaoService.swift @@ -107,7 +107,7 @@ public final class DoubaoService: StreamService { let requestBody = buildRequestBody(text: text, transType: transType) let urlRequest = try createURLRequest(body: requestBody) - let (asyncBytes, response) = try await URLSession.shared.bytes(for: urlRequest) + let (asyncBytes, response) = try await EURLSession.bytes(for: urlRequest) try validateHTTPResponse(response) try await processStreamBytes(asyncBytes, continuation: continuation) diff --git a/Easydict/Swift/Service/Google/GoogleService+Translate.swift b/Easydict/Swift/Service/Google/GoogleService+Translate.swift index a246cd829..c643e9514 100644 --- a/Easydict/Swift/Service/Google/GoogleService+Translate.swift +++ b/Easydict/Swift/Service/Google/GoogleService+Translate.swift @@ -42,7 +42,7 @@ extension GoogleService { parameters: Parameters? = nil ) -> DataRequest { - AF.request( + EAF.request( url, method: .get, parameters: parameters, diff --git a/Easydict/Swift/Service/NiuTrans/NiuTransService.swift b/Easydict/Swift/Service/NiuTrans/NiuTransService.swift index e854d8452..000f607ca 100644 --- a/Easydict/Swift/Service/NiuTrans/NiuTransService.swift +++ b/Easydict/Swift/Service/NiuTrans/NiuTransService.swift @@ -182,7 +182,7 @@ extension NiuTransService { "source": "Easydict", ] - let request = AF.request( + let request = EAF.request( kNiuTransAPIURL, method: .post, parameters: params, diff --git a/Easydict/Swift/Service/Ollama/OllamaService.swift b/Easydict/Swift/Service/Ollama/OllamaService.swift index 5c4bdc082..8bc6d9f31 100644 --- a/Easydict/Swift/Service/Ollama/OllamaService.swift +++ b/Easydict/Swift/Service/Ollama/OllamaService.swift @@ -76,7 +76,7 @@ class OllamaService: BaseOpenAIService { } let modelsURL = trueBaseURL.appendingPathComponent("api/tags") - let dataTask = AF.request(modelsURL).serializingDecodable(OllamaModels.self) + let dataTask = EAF.request(modelsURL).serializingDecodable(OllamaModels.self) return try await dataTask.value } } diff --git a/Easydict/Swift/Service/Tencent/TencentService.swift b/Easydict/Swift/Service/Tencent/TencentService.swift index cf4fd0afd..e1bf6dac1 100644 --- a/Easydict/Swift/Service/Tencent/TencentService.swift +++ b/Easydict/Swift/Service/Tencent/TencentService.swift @@ -101,7 +101,7 @@ public final class TencentService: QueryService { result = currentResult } - let request = AF.request( + let request = EAF.request( endpoint, method: .post, parameters: parameters, diff --git a/Easydict/Swift/Service/Volcano/VolcanoService.swift b/Easydict/Swift/Service/Volcano/VolcanoService.swift index 610867aa5..5d8473b4a 100644 --- a/Easydict/Swift/Service/Volcano/VolcanoService.swift +++ b/Easydict/Swift/Service/Volcano/VolcanoService.swift @@ -100,7 +100,7 @@ public final class VolcanoService: QueryService { let afHost = host + uri + "?" + queryString - let request = AF.request( + let request = EAF.request( afHost, method: .post, parameters: parameters, diff --git a/Easydict/Swift/Service/Youdao/YoudaoService+Dict.swift b/Easydict/Swift/Service/Youdao/YoudaoService+Dict.swift index a2b734d20..b2b550c44 100644 --- a/Easydict/Swift/Service/Youdao/YoudaoService+Dict.swift +++ b/Easydict/Swift/Service/Youdao/YoudaoService+Dict.swift @@ -48,7 +48,7 @@ extension YoudaoService { do { // Get the raw data - let responseData = try await AF.request( + let responseData = try await EAF.request( url, method: .post, parameters: parameters @@ -128,7 +128,7 @@ extension YoudaoService { do { // Get the raw data - let responseData = try await AF.request( + let responseData = try await EAF.request( url, method: .get, parameters: parameters diff --git a/Easydict/Swift/Service/Youdao/YoudaoService+OCR.swift b/Easydict/Swift/Service/Youdao/YoudaoService+OCR.swift index 2782ffea4..18896dd07 100644 --- a/Easydict/Swift/Service/Youdao/YoudaoService+OCR.swift +++ b/Easydict/Swift/Service/Youdao/YoudaoService+OCR.swift @@ -37,7 +37,7 @@ extension YoudaoService { let parameters = ["imgBase": imageBase] do { - let response = try await AF.request( + let response = try await EAF.request( "https://aidemo.youdao.com/ocrtransapi1", method: .post, parameters: parameters, diff --git a/Easydict/Swift/Service/Youdao/YoudaoService+Translate.swift b/Easydict/Swift/Service/Youdao/YoudaoService+Translate.swift index 5c5c096fc..065d601a0 100644 --- a/Easydict/Swift/Service/Youdao/YoudaoService+Translate.swift +++ b/Easydict/Swift/Service/Youdao/YoudaoService+Translate.swift @@ -80,7 +80,7 @@ extension YoudaoService { ], uniquingKeysWith: { _, new in new } ) - let data = try await AF.request( + let data = try await EAF.request( "\(kYoudaoDictURL)/webtranslate", method: .post, parameters: parameters, @@ -121,7 +121,7 @@ extension YoudaoService { ], uniquingKeysWith: { _, new in new } ) - return try await AF.request( + return try await EAF.request( "\(kYoudaoDictURL)/webtranslate/key", method: .get, parameters: parameters, diff --git a/Easydict/Swift/Utility/NetworkSession.swift b/Easydict/Swift/Utility/NetworkSession.swift new file mode 100644 index 000000000..10d806b57 --- /dev/null +++ b/Easydict/Swift/Utility/NetworkSession.swift @@ -0,0 +1,135 @@ +// +// NetworkSession.swift +// Easydict +// +// Created by tisfeng on 2026/4/9. +// Copyright © 2026 izual. All rights reserved. +// + +import Alamofire +import Combine +import Defaults +import Foundation + +// MARK: - NetworkSession + +/// Manages the app-wide Alamofire Session, supporting an optional user-configured HTTP/SOCKS proxy. +/// +/// When `Defaults[.httpProxyURL]` is non-empty the session is configured with +/// the parsed proxy dictionary; otherwise the system default session is used. +final class NetworkSession { + // MARK: Lifecycle + + private init() { + setupSessionObserver() + } + + // MARK: Internal + + static let shared = NetworkSession() + + /// Current Alamofire Session. Updated automatically when the proxy setting changes. + private(set) var session: Alamofire.Session = .default + + /// Current URLSession. Updated automatically when the proxy setting changes. + private(set) var urlSession: URLSession = .shared + + // MARK: Private + + private var cancellables: Set = [] + + /// Builds a proxy-configured `URLSessionConfiguration`, or returns `nil` when the URL + /// is empty or cannot be parsed. + private static func makeProxyConfiguration(proxyURL: String) -> URLSessionConfiguration? { + let trimmed = proxyURL.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty, let proxyDict = proxyDictionary(for: trimmed) else { + return nil + } + let config = URLSessionConfiguration.default + config.connectionProxyDictionary = proxyDict + return config + } + + /// Creates an Alamofire Session configured with the given proxy URL string. + /// + /// Returns `Session.default` when the URL is empty or cannot be parsed. + private static func makeSession(proxyURL: String) -> Alamofire.Session { + guard let config = makeProxyConfiguration(proxyURL: proxyURL) else { + return .default + } + return Alamofire.Session(configuration: config) + } + + /// Creates a URLSession configured with the given proxy URL string. + /// + /// Returns `URLSession.shared` when the URL is empty or cannot be parsed. + private static func makeURLSession(proxyURL: String) -> URLSession { + guard let config = makeProxyConfiguration(proxyURL: proxyURL) else { + return .shared + } + return URLSession(configuration: config) + } + + /// Parses a proxy URL string into a `connectionProxyDictionary` for `URLSessionConfiguration`. + /// + /// Supported formats: + /// - `http://host:port` — HTTP proxy (also used for HTTPS via CONNECT tunnel) + /// - `socks5://host:port` — SOCKS5 proxy + /// - `host:port` — treated as HTTP proxy + private static func proxyDictionary(for proxyURL: String) -> [AnyHashable: Any]? { + var urlString = proxyURL + if !urlString.contains("://") { + urlString = "http://" + urlString + } + + guard let url = URL(string: urlString), + let host = url.host, + let port = url.port, + !host.isEmpty + else { + return nil + } + + var dict: [AnyHashable: Any] = [:] + let scheme = url.scheme ?? "http" + + if scheme == "socks5" || scheme == "socks" { + dict[kCFNetworkProxiesSOCKSEnable as String] = true + dict[kCFNetworkProxiesSOCKSProxy as String] = host + dict[kCFNetworkProxiesSOCKSPort as String] = port + } else { + // HTTP proxy — handles plain HTTP and HTTPS (via CONNECT tunnel) + dict[kCFNetworkProxiesHTTPEnable as String] = true + dict[kCFNetworkProxiesHTTPProxy as String] = host + dict[kCFNetworkProxiesHTTPPort as String] = port + dict[kCFNetworkProxiesHTTPSEnable as String] = true + dict[kCFNetworkProxiesHTTPSProxy as String] = host + dict[kCFNetworkProxiesHTTPSPort as String] = port + } + + return dict + } + + private func setupSessionObserver() { + Defaults.publisher(.httpProxyURL, options: [.initial]) + .sink { [weak self] change in + self?.session = NetworkSession.makeSession(proxyURL: change.newValue) + self?.urlSession = NetworkSession.makeURLSession(proxyURL: change.newValue) + } + .store(in: &cancellables) + } +} + +// MARK: - Global accessor + +/// A proxy-aware Alamofire Session for making HTTP requests throughout the app. +/// +/// Use `EAF` instead of Alamofire's global `AF` so that requests automatically +/// route through the user-configured local HTTP/SOCKS proxy when one is set. +var EAF: Alamofire.Session { NetworkSession.shared.session } + +/// A proxy-aware URLSession for making streaming HTTP requests throughout the app. +/// +/// Use `EURLSession` instead of `URLSession.shared` so that streaming requests +/// route through the user-configured local HTTP/SOCKS proxy when one is set. +var EURLSession: URLSession { NetworkSession.shared.urlSession } diff --git a/Easydict/Swift/View/SettingView/Tabs/TabView/AdvancedTab.swift b/Easydict/Swift/View/SettingView/Tabs/TabView/AdvancedTab.swift index 78696d82a..f89a2dcfa 100644 --- a/Easydict/Swift/View/SettingView/Tabs/TabView/AdvancedTab.swift +++ b/Easydict/Swift/View/SettingView/Tabs/TabView/AdvancedTab.swift @@ -417,6 +417,36 @@ struct AdvancedTab: View { } header: { Text("setting.advance.header.http_server") } + + // Local proxy + Section { + LabeledContent { + TextField( + "", + text: $httpProxyURL, + prompt: Text(verbatim: "http://127.0.0.1:7890") + ) + .frame(width: 200) + .fixedSize(horizontal: true, vertical: false) + } label: { + AdvancedTabItemView( + color: .blue, + icon: .link, + labelText: "setting.advance.http_proxy_url", + subtitleText: "setting.advance.http_proxy_url_desc" + ) + } + } header: { + Text("setting.advance.header.local_proxy") + } footer: { + HStack { + Text("setting.advance.http_proxy_url_footer") + .font(.footnote) + .foregroundColor(.secondary) + .padding(.leading, 10) + Spacer() + } + } } .formStyle(.grouped) } @@ -460,6 +490,7 @@ struct AdvancedTab: View { @Default(.enableHTTPServer) private var enableHTTPServer @Default(.httpPort) private var httpPort + @Default(.httpProxyURL) private var httpProxyURL @Default(.maxWindowHeightPercentage) private var maxWindowHeightPercentageValue