diff --git a/.bartycrouch.toml b/.bartycrouch.toml index c4fc663ae..4d58d51c8 100644 --- a/.bartycrouch.toml +++ b/.bartycrouch.toml @@ -22,7 +22,6 @@ translateMethodName = "translate" [update.normalize] path = "./ClashX" -sourceLocale = "zh-Hans" harmonizeWithSource = true sortByKeys = true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f77dcc653..a5380bc06 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,34 +1,51 @@ name: ClashX -on: [push] +on: [ push, workflow_dispatch ] env: FASTLANE_SKIP_UPDATE_CHECK: true - DEVELOPER_DIR: /Applications/Xcode_13.1.app/Contents/Developer jobs: build: - runs-on: macos-11 + runs-on: macos-13 steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: import certs run: | echo `/usr/bin/xcodebuild -version` openssl aes-256-cbc -k "${{ secrets.ENCRYPTION_SECRET }}" -in ".github/certs/dist.p12.enc" -d -a -out ".github/certs/dist.p12" -md md5 - name: setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 + with: + go-version: 1.21.x + + - uses: maxim-lobanov/setup-xcode@v1 with: - go-version: 1.18.x - + xcode-version: latest-stable + - name: install deps run: | bash install_dependency.sh - - - name: update beta build version - if: contains(github.event.head_commit.message, '[beta]') && !startsWith(github.ref, 'refs/tags/') + + - name: update dev build version + if: ${{!startsWith(github.ref, 'refs/tags/')}} + run: | + tag=`git describe --abbrev=0`.`date '+%m%d%H%M%S'` + bundle exec fastlane run increment_build_number_in_plist build_number:"${tag}" scheme:"ClashX" + bundle exec fastlane run increment_version_number_in_plist version_number:"${tag}" scheme:"ClashX" + bundle exec fastlane run set_info_plist_value path:ClashX/Info.plist key:BETA value:true + + - name: update tag build version + if: startsWith(github.ref, 'refs/tags/') run: | - bundle exec fastlane beta - bundle exec fastlane run set_info_plist_value path:ClashX/Info.plist key:BETA value:YES + tag=${GITHUB_REF##*/} + bundle exec fastlane run set_info_plist_value path:ClashX/Info.plist key:BETA value:false + bundle exec fastlane run increment_build_number_in_plist build_number:"${tag}" scheme:"ClashX" + bundle exec fastlane run increment_version_number_in_plist version_number:"${tag}" scheme:"ClashX" - name: build env: @@ -39,24 +56,24 @@ jobs: cd .. bundle exec fastlane build echo "Checking SMJobBless Vailded" - python SMJobBlessUtil.py check ClashX.app + python3 SMJobBlessUtil.py check ClashX.app echo "Check done" - name: setup node - if: startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[appcenter]') - uses: actions/setup-node@v1 + if: startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[beta]') + uses: actions/setup-node@v3 with: - node-version: '10.x' + node-version: '18.x' - name: create dmg - if: startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[appcenter]') + if: startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[beta]') run: | npm install --global create-dmg create-dmg ClashX.app mv ClashX*.dmg ClashX.dmg - - name: notarize - if: startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[notarize]') + - name: notarize + if: startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[beta]') env: FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} FASTLANE_USER: ${{ secrets.FASTLANE_USER }} @@ -64,8 +81,8 @@ jobs: run: | bundle exec fastlane run notarize package:"./ClashX.dmg" bundle_id:"com.west2online.ClashX" asc_provider:MEWHFZ92DY - - name: upload to appcenter - if: startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[appcenter]') + - name: upload to appcenter + if: startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[beta]') env: APPCENTER_DISTRIBUTE_UPLOAD_BUILD_ONLY: true APPCENTER_DISTRIBUTE_DESTINATIONS: Public @@ -73,6 +90,8 @@ jobs: APPCENTER_API_TOKEN: ${{ secrets.APPCENTER_API_TOKEN }} APPCENTER_DISTRIBUTE_FILE: ClashX.dmg APPCENTER_OWNER_NAME: ${{ secrets.APPCENTER_OWNER_NAME }} + APPCENTER_DISTRIBUTE_DSYM: "ClashX.app.dSYM.zip" + APPCENTER_DISTRIBUTE_RELEASE_NOTES: ${{ github.event.head_commit.message }} run: | appversion=`defaults read \`pwd\`/ClashX.app/Contents/Info.plist CFBundleShortVersionString` buildVersion=`defaults read \`pwd\`/ClashX.app/Contents/Info.plist CFBundleVersion` @@ -86,7 +105,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - files: ClashX.dmg + files: | + ClashX.app.dSYM.zip + ClashX.dmg draft: true prerelease: true diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml new file mode 100644 index 000000000..71df35e82 --- /dev/null +++ b/.github/workflows/pull_request.yaml @@ -0,0 +1,29 @@ +name: ClashX + +on: [ pull_request ] + +env: + FASTLANE_SKIP_UPDATE_CHECK: true + +jobs: + build: + runs-on: macos-13 + steps: + - uses: actions/checkout@v3 + + - name: setup Go + uses: actions/setup-go@v3 + with: + go-version: 1.20.x + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: install deps + run: | + bash install_dependency.sh + + - name: check + run: | + bundle exec fastlane check diff --git a/.gitignore b/.gitignore index 96a0611cc..d67b4bde6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ ClashX/goClash/goClash.a fastlane/report.xml ClashX/Resources/Country.mmdb.gz .bundle/config +*.app/** +ClashX.app.dSYM.zip +build_derived_data diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 000000000..c9c456e03 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,25 @@ +# By default, SwiftLint uses a set of sensible default rules you can adjust: +disabled_rules: # rule identifiers turned on by default to exclude from running + - colon + - identifier_name + - force_cast + - closure_parameter_position + - file_length + - large_tuple + - type_body_length + - cyclomatic_complexity + - function_body_length + - nesting +opt_in_rules: # some rules are turned off by default, so you need to opt-in + - empty_count + - empty_string +included: # paths to include during linting. `--path` is ignored if present. + - ClashX +excluded: # paths to ignore during linting. Takes precedence over `included`. + - ClashX/Vendor + - Pods +analyzer_rules: # Rules run by `swiftlint analyze` + - explicit_self +# implicitly +line_length: 300 +# reporter: "xcode" diff --git a/ClashX.xcodeproj/project.pbxproj b/ClashX.xcodeproj/project.pbxproj index 946d2c005..b0be7d541 100644 --- a/ClashX.xcodeproj/project.pbxproj +++ b/ClashX.xcodeproj/project.pbxproj @@ -3,13 +3,22 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 0AD7506B2A5B9A04001FFBBD /* ConnectionLeftPannelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD7506A2A5B9A04001FFBBD /* ConnectionLeftPannelViewModel.swift */; }; + 0AD7506D2A5B9B26001FFBBD /* ConnectionLeftTextCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD7506C2A5B9B26001FFBBD /* ConnectionLeftTextCellView.swift */; }; + 0AEF6EAF2A5F961B00EFEE23 /* SectionedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEF6EAE2A5F961B00EFEE23 /* SectionedTableView.swift */; }; + 4905A2C52A2058B000AEDA2E /* GlobalShortCutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4905A2C42A2058B000AEDA2E /* GlobalShortCutViewController.swift */; }; + 4905A2C82A2058D400AEDA2E /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 4905A2C72A2058D400AEDA2E /* KeyboardShortcuts */; }; + 4905A2CA2A20841B00AEDA2E /* NSView+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4905A2C92A20841B00AEDA2E /* NSView+Layout.swift */; }; + 4908087B29F8F405007A4944 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4908087A29F8F3FF007A4944 /* libresolv.tbd */; }; + 490C8A102AC26E67007140F2 /* JSBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490C8A0F2AC26E67007140F2 /* JSBridge.swift */; }; 4913C82321157D0200F6B87C /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4913C82221157D0200F6B87C /* Notification.swift */; }; 491E6203258A424D00313AEF /* CommonUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 491E61FD258A424500313AEF /* CommonUtils.m */; }; 49228457270AADE20027A4B6 /* RemoteConfigUpdateIntervalSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49228456270AADE20027A4B6 /* RemoteConfigUpdateIntervalSettingView.swift */; }; + 49281C802A1F01FA00F60935 /* DebugSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49281C7F2A1F01FA00F60935 /* DebugSettingViewController.swift */; }; 4929F610258CD22E00A435F6 /* menu_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4929F60F258CD22E00A435F6 /* menu_icon@2x.png */; }; 4929F677258CD89B00A435F6 /* Country.mmdb.gz in Resources */ = {isa = PBXBuildFile; fileRef = 4929F676258CD89B00A435F6 /* Country.mmdb.gz */; }; 4929F67F258CE04700A435F6 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4929F67E258CE04700A435F6 /* Settings.swift */; }; @@ -27,8 +36,9 @@ 495A44D320D267D000888A0A /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495A44D220D267D000888A0A /* LaunchAtLogin.swift */; }; 495BFB8821919B9800C8779D /* RemoteConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495BFB8721919B9800C8779D /* RemoteConfigManager.swift */; }; 4960A6DB2136529200B940C9 /* JSBridgeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4960A6DA2136529200B940C9 /* JSBridgeHandler.swift */; }; + 496322222AA5D89E00854231 /* UpdateExternalResourceAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496322212AA5D89E00854231 /* UpdateExternalResourceAction.swift */; }; 4966E9E32118153A00A391FB /* NSUserNotificationCenter+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4966E9E22118153A00A391FB /* NSUserNotificationCenter+Extension.swift */; }; - 4966E9E6211824F300A391FB /* NSImage+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4966E9E5211824F300A391FB /* NSImage+extension.swift */; }; + 4969E73D2A5E3CB20012E005 /* ConnectionDetailInfoGeneralView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4969E73F2A5E3CB20012E005 /* ConnectionDetailInfoGeneralView.xib */; }; 496BDEE021196F1E00C5207F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496BDEDF21196F1E00C5207F /* Logger.swift */; }; 49722FEF211F338B00650A41 /* FileEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49722FEA211F338B00650A41 /* FileEvent.swift */; }; 49722FF0211F338B00650A41 /* EventStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49722FEB211F338B00650A41 /* EventStream.swift */; }; @@ -38,7 +48,20 @@ 4981C88B216BAE4A008CC14A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4981C88D216BAE4A008CC14A /* Localizable.strings */; }; 4982F51F2344A216008804B0 /* Cgo+Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4982F51E2344A216008804B0 /* Cgo+Convert.swift */; }; 49862FA0218418C600A1D5EC /* ClashRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49862F9F218418C600A1D5EC /* ClashRule.swift */; }; + 49870ADB2AA75DC7002B106B /* TerminalCleanUpAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49870ADA2AA75DC7002B106B /* TerminalCleanUpAction.swift */; }; + 49870ADD2AA76602002B106B /* UpdateConfigAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49870ADC2AA76602002B106B /* UpdateConfigAction.swift */; }; 4989F98E20D0AE990001E564 /* sampleConfig.yaml in Resources */ = {isa = PBXBuildFile; fileRef = 4989F98D20D0AE990001E564 /* sampleConfig.yaml */; }; + 498BC2532929CC2A00CA8084 /* SettingTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498BC2522929CC2A00CA8084 /* SettingTabViewController.swift */; }; + 498BC2552929CCAE00CA8084 /* GeneralSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498BC2542929CCAE00CA8084 /* GeneralSettingViewController.swift */; }; + 498D87D82A617D2200CD456F /* DashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498D87D72A617D2200CD456F /* DashboardViewController.swift */; }; + 498D87DA2A617E9900CD456F /* DashboardSubViewControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498D87D92A617E9900CD456F /* DashboardSubViewControllerProtocol.swift */; }; + 4991D2282A5646D200978143 /* ConnectionProxyClientCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4991D2272A5646D200978143 /* ConnectionProxyClientCellView.swift */; }; + 4991D22A2A56472300978143 /* ConnectionCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4991D2292A56472300978143 /* ConnectionCellProtocol.swift */; }; + 4991D22C2A56478400978143 /* ConnectionColume.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4991D22B2A56478400978143 /* ConnectionColume.swift */; }; + 4991D22E2A564A4200978143 /* ConnectionTextCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4991D22D2A564A4200978143 /* ConnectionTextCellView.swift */; }; + 4991D2302A564DDC00978143 /* SpeedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4991D22F2A564DDC00978143 /* SpeedUtils.swift */; }; + 4991D2322A565E6A00978143 /* Combine+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4991D2312A565E6A00978143 /* Combine+Ext.swift */; }; + 4994B5542A47C4FF00E595B9 /* NormalMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4994B5532A47C4FF00E595B9 /* NormalMenuItemView.swift */; }; 499976C821359F0400E7BF83 /* ClashWebViewContoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499976C721359F0400E7BF83 /* ClashWebViewContoller.swift */; }; 499A485522ED707300F6C675 /* RemoteConfigViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499A485322ED707300F6C675 /* RemoteConfigViewController.swift */; }; 499A485822ED715200F6C675 /* RemoteConfigModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499A485722ED715200F6C675 /* RemoteConfigModel.swift */; }; @@ -53,9 +76,18 @@ 49B445162457CDF000B27E3E /* ClashStatusTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B445152457CDF000B27E3E /* ClashStatusTool.swift */; }; 49B4575D244F4A2A00463C39 /* PrivilegedHelperManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B4575C244F4A2A00463C39 /* PrivilegedHelperManager.swift */; }; 49B4575F244FD4D100463C39 /* PrivilegedHelperManager+Legacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B4575E244FD4D100463C39 /* PrivilegedHelperManager+Legacy.swift */; }; - 49BB31E7246853EA008A4CB0 /* iCloudManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49BB31E6246853EA008A4CB0 /* iCloudManager.swift */; }; + 49BB31E7246853EA008A4CB0 /* ICloudManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49BB31E6246853EA008A4CB0 /* ICloudManager.swift */; }; 49BC061C212931F4005A0FE7 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49BC061B212931F4005A0FE7 /* AboutViewController.swift */; }; + 49C9AFEC2A592C5D00178BB4 /* ConnectionDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C9AFEB2A592C5D00178BB4 /* ConnectionDetailViewModel.swift */; }; + 49C9AFF22A59366200178BB4 /* ConnectionDetailInfoGeneralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C9AFF12A59366200178BB4 /* ConnectionDetailInfoGeneralView.swift */; }; + 49C9AFF42A593B8800178BB4 /* ConnectionTopListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C9AFF32A593B8800178BB4 /* ConnectionTopListViewModel.swift */; }; 49C9EF64223E78F5005D8B6A /* ClashProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C9EF63223E78F5005D8B6A /* ClashProxy.swift */; }; + 49CCDA282A54F8DA00FF1E13 /* ConnectionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CCDA272A54F8DA00FF1E13 /* ConnectionsViewController.swift */; }; + 49CCDA2A2A54F9AC00FF1E13 /* ClashWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CCDA292A54F9AC00FF1E13 /* ClashWindowController.swift */; }; + 49CCDA2D2A54FBA600FF1E13 /* ConnectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CCDA2C2A54FBA600FF1E13 /* ConnectionsViewModel.swift */; }; + 49CCDA302A54FC3300FF1E13 /* ConnectionLeftPannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CCDA2F2A54FC3300FF1E13 /* ConnectionLeftPannelView.swift */; }; + 49CCDA322A5506D100FF1E13 /* ConnectionTopListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CCDA312A5506D100FF1E13 /* ConnectionTopListView.swift */; }; + 49CCDA362A55126A00FF1E13 /* ConnectionDetailInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CCDA352A55126A00FF1E13 /* ConnectionDetailInfoView.swift */; }; 49CF3B2120CD7463001EBF94 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CF3B2020CD7463001EBF94 /* AppDelegate.swift */; }; 49CF3B2820CD7465001EBF94 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49CF3B2620CD7465001EBF94 /* Main.storyboard */; }; 49CF3B5C20CE8068001EBF94 /* ClashResourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49CF3B5B20CE8068001EBF94 /* ClashResourceManager.swift */; }; @@ -63,6 +95,13 @@ 49D176A72355FE680093DD7B /* NetworkChangeNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176A62355FE680093DD7B /* NetworkChangeNotifier.swift */; }; 49D176A9235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176A8235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift */; }; 49D176AB23575BB20093DD7B /* ProxyGroupMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176AA23575BB20093DD7B /* ProxyGroupMenuItemView.swift */; }; + 49D223392A1DA5F10002FFCB /* SSIDSuspendTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D223382A1DA5F10002FFCB /* SSIDSuspendTool.swift */; }; + 49D6A45229AEEC15006487EF /* StatusItemTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A45129AEEC15006487EF /* StatusItemTool.swift */; }; + 49D6A45629AEEC55006487EF /* StatusItemViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A45529AEEC55006487EF /* StatusItemViewProtocol.swift */; }; + 49D767742A6195C800830333 /* ConnectionsReq.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D767732A6195C800830333 /* ConnectionsReq.swift */; }; + 49D767762A6195E600830333 /* StructedLogReq.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D767752A6195E600830333 /* StructedLogReq.swift */; }; + 49D84AD32A56E9760074CCDB /* ConnectionStatusIconCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D84AD22A56E9760074CCDB /* ConnectionStatusIconCellView.swift */; }; + 49FEC6692AD9369C00BAD9F5 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FEC6682AD9369C00BAD9F5 /* Command.swift */; }; 8A2BBEA727A03ACB0081EBEF /* ProxySetting.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 8A2BBEA627A03ACB0081EBEF /* ProxySetting.sdef */; }; 8ACD21BB27A04C7800BC4632 /* ProxySettingCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ACD21BA27A04C7800BC4632 /* ProxySettingCommand.swift */; }; 8ACD21BD27A04ED500BC4632 /* ProxyModeChangeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ACD21BC27A04ED500BC4632 /* ProxyModeChangeCommand.swift */; }; @@ -126,10 +165,18 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0AD7506A2A5B9A04001FFBBD /* ConnectionLeftPannelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionLeftPannelViewModel.swift; sourceTree = ""; }; + 0AD7506C2A5B9B26001FFBBD /* ConnectionLeftTextCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionLeftTextCellView.swift; sourceTree = ""; }; + 0AEF6EAE2A5F961B00EFEE23 /* SectionedTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionedTableView.swift; sourceTree = ""; }; + 4905A2C42A2058B000AEDA2E /* GlobalShortCutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalShortCutViewController.swift; sourceTree = ""; }; + 4905A2C92A20841B00AEDA2E /* NSView+Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSView+Layout.swift"; sourceTree = ""; }; + 4908087A29F8F3FF007A4944 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + 490C8A0F2AC26E67007140F2 /* JSBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSBridge.swift; sourceTree = ""; }; 4913C82221157D0200F6B87C /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 491E61FC258A424500313AEF /* CommonUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CommonUtils.h; sourceTree = ""; }; 491E61FD258A424500313AEF /* CommonUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommonUtils.m; sourceTree = ""; }; 49228456270AADE20027A4B6 /* RemoteConfigUpdateIntervalSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigUpdateIntervalSettingView.swift; sourceTree = ""; }; + 49281C7F2A1F01FA00F60935 /* DebugSettingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugSettingViewController.swift; sourceTree = ""; }; 4929F60F258CD22E00A435F6 /* menu_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icon@2x.png"; sourceTree = ""; }; 4929F676258CD89B00A435F6 /* Country.mmdb.gz */ = {isa = PBXFileReference; lastKnownFileType = archive.gzip; path = Country.mmdb.gz; sourceTree = ""; }; 4929F67E258CE04700A435F6 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; @@ -142,14 +189,19 @@ 4949D153213242F600EF85E6 /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = ""; }; 4952C3BE2115C7CA004A4FA8 /* MenuItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemFactory.swift; sourceTree = ""; }; 4952C3CF2117027C004A4FA8 /* ConfigFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigFileManager.swift; sourceTree = ""; }; - 495340AF20DE5F7200B0D3FF /* StatusItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusItemView.xib; sourceTree = ""; }; + 495340AF20DE5F7200B0D3FF /* StatusItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = StatusItemView.xib; path = ClashX/Views/StatusItem/StatusItemView.xib; sourceTree = SOURCE_ROOT; }; 495340B220DE68C300B0D3FF /* StatusItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemView.swift; sourceTree = ""; }; 495A44D220D267D000888A0A /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = ""; }; 495BFB8721919B9800C8779D /* RemoteConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigManager.swift; sourceTree = ""; }; 4960A6DA2136529200B940C9 /* JSBridgeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSBridgeHandler.swift; sourceTree = ""; }; + 496322212AA5D89E00854231 /* UpdateExternalResourceAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExternalResourceAction.swift; sourceTree = ""; }; 4966E9E22118153A00A391FB /* NSUserNotificationCenter+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserNotificationCenter+Extension.swift"; sourceTree = ""; }; - 4966E9E5211824F300A391FB /* NSImage+extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+extension.swift"; sourceTree = ""; }; + 4969E73E2A5E3CB20012E005 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ConnectionDetailInfoGeneralView.xib; sourceTree = ""; }; + 4969E7412A5E3CB80012E005 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ConnectionDetailInfoGeneralView.strings"; sourceTree = ""; }; + 4969E7432A5E3CB90012E005 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/ConnectionDetailInfoGeneralView.strings"; sourceTree = ""; }; 496BDEDF21196F1E00C5207F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 496C16462A3418C80052064A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; + 496C16472A3418C80052064A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 49722FEA211F338B00650A41 /* FileEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileEvent.swift; sourceTree = ""; }; 49722FEB211F338B00650A41 /* EventStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventStream.swift; sourceTree = ""; }; 49722FEC211F338B00650A41 /* Witness.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Witness.swift; sourceTree = ""; }; @@ -160,8 +212,21 @@ 4981C88E216BAE4D008CC14A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 4982F51E2344A216008804B0 /* Cgo+Convert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cgo+Convert.swift"; sourceTree = ""; }; 49862F9F218418C600A1D5EC /* ClashRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashRule.swift; sourceTree = ""; }; + 49870ADA2AA75DC7002B106B /* TerminalCleanUpAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCleanUpAction.swift; sourceTree = ""; }; + 49870ADC2AA76602002B106B /* UpdateConfigAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateConfigAction.swift; sourceTree = ""; }; 498960722340F21C00AFB7EC /* com.west2online.ClashX.ProxyConfigHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = com.west2online.ClashX.ProxyConfigHelper.entitlements; sourceTree = ""; }; 4989F98D20D0AE990001E564 /* sampleConfig.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = sampleConfig.yaml; sourceTree = ""; }; + 498BC2522929CC2A00CA8084 /* SettingTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingTabViewController.swift; sourceTree = ""; }; + 498BC2542929CCAE00CA8084 /* GeneralSettingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingViewController.swift; sourceTree = ""; }; + 498D87D72A617D2200CD456F /* DashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewController.swift; sourceTree = ""; }; + 498D87D92A617E9900CD456F /* DashboardSubViewControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardSubViewControllerProtocol.swift; sourceTree = ""; }; + 4991D2272A5646D200978143 /* ConnectionProxyClientCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionProxyClientCellView.swift; sourceTree = ""; }; + 4991D2292A56472300978143 /* ConnectionCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionCellProtocol.swift; sourceTree = ""; }; + 4991D22B2A56478400978143 /* ConnectionColume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionColume.swift; sourceTree = ""; }; + 4991D22D2A564A4200978143 /* ConnectionTextCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTextCellView.swift; sourceTree = ""; }; + 4991D22F2A564DDC00978143 /* SpeedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedUtils.swift; sourceTree = ""; }; + 4991D2312A565E6A00978143 /* Combine+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Combine+Ext.swift"; sourceTree = ""; }; + 4994B5532A47C4FF00E595B9 /* NormalMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NormalMenuItemView.swift; sourceTree = ""; }; 499976C721359F0400E7BF83 /* ClashWebViewContoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashWebViewContoller.swift; sourceTree = ""; }; 499A485322ED707300F6C675 /* RemoteConfigViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigViewController.swift; sourceTree = ""; }; 499A485722ED715200F6C675 /* RemoteConfigModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigModel.swift; sourceTree = ""; }; @@ -176,9 +241,18 @@ 49B445152457CDF000B27E3E /* ClashStatusTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashStatusTool.swift; sourceTree = ""; }; 49B4575C244F4A2A00463C39 /* PrivilegedHelperManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegedHelperManager.swift; sourceTree = ""; }; 49B4575E244FD4D100463C39 /* PrivilegedHelperManager+Legacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PrivilegedHelperManager+Legacy.swift"; sourceTree = ""; }; - 49BB31E6246853EA008A4CB0 /* iCloudManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iCloudManager.swift; sourceTree = ""; }; + 49BB31E6246853EA008A4CB0 /* ICloudManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICloudManager.swift; sourceTree = ""; }; 49BC061B212931F4005A0FE7 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; + 49C9AFEB2A592C5D00178BB4 /* ConnectionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailViewModel.swift; sourceTree = ""; }; + 49C9AFF12A59366200178BB4 /* ConnectionDetailInfoGeneralView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailInfoGeneralView.swift; sourceTree = ""; }; + 49C9AFF32A593B8800178BB4 /* ConnectionTopListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTopListViewModel.swift; sourceTree = ""; }; 49C9EF63223E78F5005D8B6A /* ClashProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashProxy.swift; sourceTree = ""; }; + 49CCDA272A54F8DA00FF1E13 /* ConnectionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionsViewController.swift; sourceTree = ""; }; + 49CCDA292A54F9AC00FF1E13 /* ClashWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashWindowController.swift; sourceTree = ""; }; + 49CCDA2C2A54FBA600FF1E13 /* ConnectionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionsViewModel.swift; sourceTree = ""; }; + 49CCDA2F2A54FC3300FF1E13 /* ConnectionLeftPannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionLeftPannelView.swift; sourceTree = ""; }; + 49CCDA312A5506D100FF1E13 /* ConnectionTopListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTopListView.swift; sourceTree = ""; }; + 49CCDA352A55126A00FF1E13 /* ConnectionDetailInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailInfoView.swift; sourceTree = ""; }; 49CF3B1D20CD7463001EBF94 /* ClashX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ClashX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49CF3B2020CD7463001EBF94 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49CF3B2720CD7465001EBF94 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -190,8 +264,15 @@ 49D176A62355FE680093DD7B /* NetworkChangeNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkChangeNotifier.swift; sourceTree = ""; }; 49D176A8235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupSpeedTestMenuItem.swift; sourceTree = ""; }; 49D176AA23575BB20093DD7B /* ProxyGroupMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupMenuItemView.swift; sourceTree = ""; }; + 49D223382A1DA5F10002FFCB /* SSIDSuspendTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSIDSuspendTool.swift; sourceTree = ""; }; + 49D6A45129AEEC15006487EF /* StatusItemTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemTool.swift; sourceTree = ""; }; + 49D6A45529AEEC55006487EF /* StatusItemViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemViewProtocol.swift; sourceTree = ""; }; + 49D767732A6195C800830333 /* ConnectionsReq.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionsReq.swift; sourceTree = ""; }; + 49D767752A6195E600830333 /* StructedLogReq.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StructedLogReq.swift; sourceTree = ""; }; 49D8276627E9B01700159D93 /* LoginKitWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginKitWrapper.h; sourceTree = ""; }; 49D8276727E9B01700159D93 /* LoginKitWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginKitWrapper.m; sourceTree = ""; }; + 49D84AD22A56E9760074CCDB /* ConnectionStatusIconCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStatusIconCellView.swift; sourceTree = ""; }; + 49FEC6682AD9369C00BAD9F5 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; 5217C006C5A22A1CEA24BFC1 /* Pods-ClashX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ClashX.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ClashX/Pods-ClashX.debug.xcconfig"; sourceTree = ""; }; 8A2BBEA627A03ACB0081EBEF /* ProxySetting.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = ProxySetting.sdef; sourceTree = ""; }; 8ACD21BA27A04C7800BC4632 /* ProxySettingCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxySettingCommand.swift; sourceTree = ""; }; @@ -234,7 +315,9 @@ buildActionMask = 2147483647; files = ( F9C180A3243C6590005EE8C4 /* goClash.a in Frameworks */, + 4905A2C82A2058D400AEDA2E /* KeyboardShortcuts in Frameworks */, 9E0E2F1481CB327D3753969D /* libPods-ClashX.a in Frameworks */, + 4908087B29F8F405007A4944 /* libresolv.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -278,9 +361,12 @@ isa = PBXGroup; children = ( 4960A6DA2136529200B940C9 /* JSBridgeHandler.swift */, + 490C8A0F2AC26E67007140F2 /* JSBridge.swift */, 493AEAE2221AE3420016FE98 /* AppVersionUtil.swift */, 49D176A62355FE680093DD7B /* NetworkChangeNotifier.swift */, 49B445152457CDF000B27E3E /* ClashStatusTool.swift */, + 49D223382A1DA5F10002FFCB /* SSIDSuspendTool.swift */, + 49FEC6682AD9369C00BAD9F5 /* Command.swift */, ); path = Utils; sourceTree = ""; @@ -299,7 +385,7 @@ F9E754CF239CC21F00CEE7CC /* WebPortalManager.swift */, 49B4575C244F4A2A00463C39 /* PrivilegedHelperManager.swift */, 49B4575E244FD4D100463C39 /* PrivilegedHelperManager+Legacy.swift */, - 49BB31E6246853EA008A4CB0 /* iCloudManager.swift */, + 49BB31E6246853EA008A4CB0 /* ICloudManager.swift */, 499ADAFC2498CC5900C488FE /* RemoteControlManager.swift */, 4929F67E258CE04700A435F6 /* Settings.swift */, ); @@ -333,21 +419,31 @@ 4931969C21631F2E00A8E6E7 /* Views */ = { isa = PBXGroup; children = ( + 49BC1B4029AEEBBB002007B1 /* StatusItem */, F92D0B2B236C7C3600575E15 /* MenuItemBaseView.swift */, 49D176AA23575BB20093DD7B /* ProxyGroupMenuItemView.swift */, 49D176A8235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift */, F92D0B2D236D35C000575E15 /* ProxyItemView.swift */, 493AEAE4221AE7230016FE98 /* ProxyMenuItem.swift */, 499A485922ED781100F6C675 /* RemoteConfigAddView.xib */, - 495340B220DE68C300B0D3FF /* StatusItemView.swift */, - 495340AF20DE5F7200B0D3FF /* StatusItemView.xib */, F910AA23240134AF00116E95 /* ProxyGroupMenu.swift */, 493A9F272453E60400D35296 /* ProxyDelayHistoryMenu.swift */, 49228456270AADE20027A4B6 /* RemoteConfigUpdateIntervalSettingView.swift */, + 4994B5532A47C4FF00E595B9 /* NormalMenuItemView.swift */, ); path = Views; sourceTree = ""; }; + 496322202AA5D88100854231 /* Actions */ = { + isa = PBXGroup; + children = ( + 496322212AA5D89E00854231 /* UpdateExternalResourceAction.swift */, + 49870ADA2AA75DC7002B106B /* TerminalCleanUpAction.swift */, + 49870ADC2AA76602002B106B /* UpdateConfigAction.swift */, + ); + path = Actions; + sourceTree = ""; + }; 49722FDD211ED2A900650A41 /* Vendor */ = { isa = PBXGroup; children = ( @@ -385,6 +481,8 @@ 4989F98520D0AA300001E564 /* ViewControllers */ = { isa = PBXGroup; children = ( + 49CCDA2B2A54FA8900FF1E13 /* Connections */, + 498BC2512929CC0A00CA8084 /* Settings */, 49BC061B212931F4005A0FE7 /* AboutViewController.swift */, 499976C721359F0400E7BF83 /* ClashWebViewContoller.swift */, 499A485322ED707300F6C675 /* RemoteConfigViewController.swift */, @@ -393,18 +491,93 @@ path = ViewControllers; sourceTree = ""; }; + 498BC2512929CC0A00CA8084 /* Settings */ = { + isa = PBXGroup; + children = ( + 498BC2522929CC2A00CA8084 /* SettingTabViewController.swift */, + 498BC2542929CCAE00CA8084 /* GeneralSettingViewController.swift */, + 49281C7F2A1F01FA00F60935 /* DebugSettingViewController.swift */, + 4905A2C42A2058B000AEDA2E /* GlobalShortCutViewController.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 4991D2262A56467600978143 /* Cell */ = { + isa = PBXGroup; + children = ( + 4991D2272A5646D200978143 /* ConnectionProxyClientCellView.swift */, + 4991D2292A56472300978143 /* ConnectionCellProtocol.swift */, + 4991D22D2A564A4200978143 /* ConnectionTextCellView.swift */, + 49D84AD22A56E9760074CCDB /* ConnectionStatusIconCellView.swift */, + 0AD7506C2A5B9B26001FFBBD /* ConnectionLeftTextCellView.swift */, + ); + path = Cell; + sourceTree = ""; + }; 4997732220D251A60009B136 /* Basic */ = { isa = PBXGroup; children = ( 495A44D220D267D000888A0A /* LaunchAtLogin.swift */, - 4966E9E5211824F300A391FB /* NSImage+extension.swift */, 496BDEDF21196F1E00C5207F /* Logger.swift */, + 4905A2C92A20841B00AEDA2E /* NSView+Layout.swift */, 49B10869216A356D0064FFCE /* String+Extension.swift */, 49ABB748236B0F9E00535CD7 /* UnsafePointer+bridge.swift */, + 4991D22F2A564DDC00978143 /* SpeedUtils.swift */, + 4991D2312A565E6A00978143 /* Combine+Ext.swift */, ); path = Basic; sourceTree = ""; }; + 49BC1B4029AEEBBB002007B1 /* StatusItem */ = { + isa = PBXGroup; + children = ( + 495340AF20DE5F7200B0D3FF /* StatusItemView.xib */, + 495340B220DE68C300B0D3FF /* StatusItemView.swift */, + 49D6A45129AEEC15006487EF /* StatusItemTool.swift */, + 49D6A45529AEEC55006487EF /* StatusItemViewProtocol.swift */, + ); + path = StatusItem; + sourceTree = ""; + }; + 49C9AFF52A596ABA00178BB4 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 49CCDA2C2A54FBA600FF1E13 /* ConnectionsViewModel.swift */, + 49C9AFEB2A592C5D00178BB4 /* ConnectionDetailViewModel.swift */, + 49C9AFF32A593B8800178BB4 /* ConnectionTopListViewModel.swift */, + 0AD7506A2A5B9A04001FFBBD /* ConnectionLeftPannelViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 49CCDA2B2A54FA8900FF1E13 /* Connections */ = { + isa = PBXGroup; + children = ( + 49D767722A6195BE00830333 /* Requests */, + 49C9AFF52A596ABA00178BB4 /* ViewModels */, + 49CCDA2E2A54FC1900FF1E13 /* Views */, + 49CCDA272A54F8DA00FF1E13 /* ConnectionsViewController.swift */, + 498D87D72A617D2200CD456F /* DashboardViewController.swift */, + 498D87D92A617E9900CD456F /* DashboardSubViewControllerProtocol.swift */, + ); + path = Connections; + sourceTree = ""; + }; + 49CCDA2E2A54FC1900FF1E13 /* Views */ = { + isa = PBXGroup; + children = ( + 4991D2262A56467600978143 /* Cell */, + 4991D22B2A56478400978143 /* ConnectionColume.swift */, + 49C9AFF12A59366200178BB4 /* ConnectionDetailInfoGeneralView.swift */, + 4969E73F2A5E3CB20012E005 /* ConnectionDetailInfoGeneralView.xib */, + 49CCDA352A55126A00FF1E13 /* ConnectionDetailInfoView.swift */, + 49CCDA2F2A54FC3300FF1E13 /* ConnectionLeftPannelView.swift */, + 49CCDA312A5506D100FF1E13 /* ConnectionTopListView.swift */, + 0AEF6EAE2A5F961B00EFEE23 /* SectionedTableView.swift */, + ); + path = Views; + sourceTree = ""; + }; 49CF3B1420CD7463001EBF94 = { isa = PBXGroup; children = ( @@ -436,6 +609,7 @@ 491C24FF21BD558B00AB5D44 /* Extensions */, 492C4866210EE69B004554A0 /* General */, 4931969C21631F2E00A8E6E7 /* Views */, + 496322202AA5D88100854231 /* Actions */, 4989F98520D0AA300001E564 /* ViewControllers */, 49761DA521C9490400AE13EF /* Resources */, 49CF3B3A20CD783A001EBF94 /* Support Files */, @@ -445,6 +619,7 @@ 49CF3B2A20CD7465001EBF94 /* ClashX.entitlements */, 49CF3B3520CD75DF001EBF94 /* ClashX-Bridging-Header.h */, F9FAB31D262BE04800DE02A6 /* Images.xcassets */, + 49CCDA292A54F9AC00FF1E13 /* ClashWindowController.swift */, ); path = ClashX; sourceTree = ""; @@ -459,6 +634,15 @@ path = "Support Files"; sourceTree = ""; }; + 49D767722A6195BE00830333 /* Requests */ = { + isa = PBXGroup; + children = ( + 49D767732A6195C800830333 /* ConnectionsReq.swift */, + 49D767752A6195E600830333 /* StructedLogReq.swift */, + ); + path = Requests; + sourceTree = ""; + }; 76229F122B00E935D126742A /* Pods */ = { isa = PBXGroup; children = ( @@ -481,6 +665,7 @@ CF1AC9FACC36FCE7663C5583 /* Frameworks */ = { isa = PBXGroup; children = ( + 4908087A29F8F3FF007A4944 /* libresolv.tbd */, CAE7981A9F34B5E210C549CB /* libPods-ClashX.a */, ); name = Frameworks; @@ -520,13 +705,14 @@ buildConfigurationList = 49CF3B2D20CD7465001EBF94 /* Build configuration list for PBXNativeTarget "ClashX" */; buildPhases = ( 2FD76081F5FF64514C3E5C9D /* [CP] Check Pods Manifest.lock */, + 49A7A54F2AA6C0980083025C /* Swift Format */, 49CF3B1920CD7463001EBF94 /* Sources */, 49CF3B1A20CD7463001EBF94 /* Frameworks */, 49CF3B1B20CD7463001EBF94 /* Resources */, A741C26F5755233F0D7CEC6F /* [CP] Embed Pods Frameworks */, 663E4677213FCDC4006F11BB /* Copy Files */, - 494ED8F023EB0B36008D5D2F /* Run Script - Sparkle */, 318032FABBC2E552CB58B254 /* [CP] Copy Pods Resources */, + 49B93AD42A3965B40080967C /* Swift Lint */, ); buildRules = ( ); @@ -534,6 +720,9 @@ F935B2EC2307B7CD009E4D33 /* PBXTargetDependency */, ); name = ClashX; + packageProductDependencies = ( + 4905A2C72A2058D400AEDA2E /* KeyboardShortcuts */, + ); productName = ClashX; productReference = 49CF3B1D20CD7463001EBF94 /* ClashX.app */; productType = "com.apple.product-type.application"; @@ -561,8 +750,9 @@ 49CF3B1520CD7463001EBF94 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1030; - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = west2online; TargetAttributes = { 49CF3B1C20CD7463001EBF94 = { @@ -590,8 +780,12 @@ en, Base, "zh-Hans", + "zh-Hant", ); mainGroup = 49CF3B1420CD7463001EBF94; + packageReferences = ( + 4905A2C62A2058D400AEDA2E /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, + ); productRefGroup = 49CF3B1E20CD7463001EBF94 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -607,6 +801,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4969E73D2A5E3CB20012E005 /* ConnectionDetailInfoGeneralView.xib in Resources */, 49761DA721C9497000AE13EF /* dashboard in Resources */, 4929F677258CD89B00A435F6 /* Country.mmdb.gz in Resources */, 4981C88B216BAE4A008CC14A /* Localizable.strings in Resources */, @@ -658,23 +853,43 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ClashX/Pods-ClashX-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 494ED8F023EB0B36008D5D2F /* Run Script - Sparkle */ = { + 49A7A54F2AA6C0980083025C /* Swift Format */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Swift Format"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat\" \"$SRCROOT\" --swiftversion 5.6 --disable unusedArguments,numberFormatting,redundantReturn,andOperator,wrapMultilineStatementBraces,trailingCommas\n"; + }; + 49B93AD42A3965B40080967C /* Swift Lint */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 12; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); - name = "Run Script - Sparkle"; + name = "Swift Lint"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nLOCATION=\"${BUILT_PRODUCTS_DIR}\"/\"${FRAMEWORKS_FOLDER_PATH}\"\n\n# By default, use the configured code signing identity for the project/target\nIDENTITY=\"${CODE_SIGN_IDENTITY}\"\nif [ \"$IDENTITY\" == \"\" ]\nthen\n # If a code signing identity is not specified, use ad hoc signing\n IDENTITY=\"-\"\nfi\ncodesign --verbose --force --deep -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate.app\"\ncodesign --verbose --force -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A\"\n"; + shellScript = "${PODS_ROOT}/SwiftLint/swiftlint\n"; }; A741C26F5755233F0D7CEC6F /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -702,6 +917,7 @@ files = ( 49ABB749236B0F9E00535CD7 /* UnsafePointer+bridge.swift in Sources */, 499A485522ED707300F6C675 /* RemoteConfigViewController.swift in Sources */, + 49D6A45229AEEC15006487EF /* StatusItemTool.swift in Sources */, 49CF3B5C20CE8068001EBF94 /* ClashResourceManager.swift in Sources */, 4952C3D02117027C004A4FA8 /* ConfigFileManager.swift in Sources */, 49BC061C212931F4005A0FE7 /* AboutViewController.swift in Sources */, @@ -712,15 +928,31 @@ F977FAAE23669D6400C17F1F /* ConnectionManager.swift in Sources */, 495340B320DE68C300B0D3FF /* StatusItemView.swift in Sources */, F935B2FC23085515009E4D33 /* SystemProxyManager.swift in Sources */, + 0AD7506D2A5B9B26001FFBBD /* ConnectionLeftTextCellView.swift in Sources */, 495A44D320D267D000888A0A /* LaunchAtLogin.swift in Sources */, + 490C8A102AC26E67007140F2 /* JSBridge.swift in Sources */, + 4991D2322A565E6A00978143 /* Combine+Ext.swift in Sources */, + 49281C802A1F01FA00F60935 /* DebugSettingViewController.swift in Sources */, + 49D767762A6195E600830333 /* StructedLogReq.swift in Sources */, + 49CCDA2A2A54F9AC00FF1E13 /* ClashWindowController.swift in Sources */, 4929F67F258CE04700A435F6 /* Settings.swift in Sources */, + 49D6A45629AEEC55006487EF /* StatusItemViewProtocol.swift in Sources */, 493AEAE3221AE3420016FE98 /* AppVersionUtil.swift in Sources */, 49CF3B2120CD7463001EBF94 /* AppDelegate.swift in Sources */, + 49CCDA2D2A54FBA600FF1E13 /* ConnectionsViewModel.swift in Sources */, + 49C9AFEC2A592C5D00178BB4 /* ConnectionDetailViewModel.swift in Sources */, + 4991D2302A564DDC00978143 /* SpeedUtils.swift in Sources */, 496BDEE021196F1E00C5207F /* Logger.swift in Sources */, + 4905A2CA2A20841B00AEDA2E /* NSView+Layout.swift in Sources */, 49722FEF211F338B00650A41 /* FileEvent.swift in Sources */, 49D176A72355FE680093DD7B /* NetworkChangeNotifier.swift in Sources */, + 49D84AD32A56E9760074CCDB /* ConnectionStatusIconCellView.swift in Sources */, + 4905A2C52A2058B000AEDA2E /* GlobalShortCutViewController.swift in Sources */, 4913C82321157D0200F6B87C /* Notification.swift in Sources */, 8ACD21BD27A04ED500BC4632 /* ProxyModeChangeCommand.swift in Sources */, + 498BC2552929CCAE00CA8084 /* GeneralSettingViewController.swift in Sources */, + 498D87D82A617D2200CD456F /* DashboardViewController.swift in Sources */, + 4991D22E2A564A4200978143 /* ConnectionTextCellView.swift in Sources */, 49228457270AADE20027A4B6 /* RemoteConfigUpdateIntervalSettingView.swift in Sources */, F9203A26236342820020D57D /* AppDelegate+..swift in Sources */, 499A485C22ED793C00F6C675 /* NSView+Nib.swift in Sources */, @@ -728,6 +960,7 @@ 492C4871210EF62E004554A0 /* ClashConfig.swift in Sources */, 492C4869210EE6B9004554A0 /* ApiRequest.swift in Sources */, 49CF3B6520CEE06C001EBF94 /* ConfigManager.swift in Sources */, + 49870ADB2AA75DC7002B106B /* TerminalCleanUpAction.swift in Sources */, F9E754D0239CC21F00CEE7CC /* WebPortalManager.swift in Sources */, 495BFB8821919B9800C8779D /* RemoteConfigManager.swift in Sources */, 4982F51F2344A216008804B0 /* Cgo+Convert.swift in Sources */, @@ -736,31 +969,49 @@ 49722FF0211F338B00650A41 /* EventStream.swift in Sources */, 499A486522EEA3FD00F6C675 /* Array+Safe.swift in Sources */, F92D0B24236BC12000575E15 /* SavedProxyModel.swift in Sources */, + 49FEC6692AD9369C00BAD9F5 /* Command.swift in Sources */, F92D0B2A236C759100575E15 /* NSTextField+Vibrancy.swift in Sources */, + 49CCDA302A54FC3300FF1E13 /* ConnectionLeftPannelView.swift in Sources */, + 49D223392A1DA5F10002FFCB /* SSIDSuspendTool.swift in Sources */, F910AA24240134AF00116E95 /* ProxyGroupMenu.swift in Sources */, + 4991D22A2A56472300978143 /* ConnectionCellProtocol.swift in Sources */, 4952C3BF2115C7CA004A4FA8 /* MenuItemFactory.swift in Sources */, + 49D767742A6195C800830333 /* ConnectionsReq.swift in Sources */, + 49C9AFF22A59366200178BB4 /* ConnectionDetailInfoGeneralView.swift in Sources */, F977FAAC2366790500C17F1F /* AutoUpgardeManager.swift in Sources */, 499A485822ED715200F6C675 /* RemoteConfigModel.swift in Sources */, + 49C9AFF42A593B8800178BB4 /* ConnectionTopListViewModel.swift in Sources */, + 498D87DA2A617E9900CD456F /* DashboardSubViewControllerProtocol.swift in Sources */, 49D176AB23575BB20093DD7B /* ProxyGroupMenuItemView.swift in Sources */, 49B4575D244F4A2A00463C39 /* PrivilegedHelperManager.swift in Sources */, + 4991D2282A5646D200978143 /* ConnectionProxyClientCellView.swift in Sources */, + 4991D22C2A56478400978143 /* ConnectionColume.swift in Sources */, 49B4575F244FD4D100463C39 /* PrivilegedHelperManager+Legacy.swift in Sources */, 4966E9E32118153A00A391FB /* NSUserNotificationCenter+Extension.swift in Sources */, F9E754D2239CC28D00CEE7CC /* NSAlert+Extension.swift in Sources */, 499976C821359F0400E7BF83 /* ClashWebViewContoller.swift in Sources */, + 49CCDA282A54F8DA00FF1E13 /* ConnectionsViewController.swift in Sources */, + 49CCDA322A5506D100FF1E13 /* ConnectionTopListView.swift in Sources */, 49D176A9235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift in Sources */, F976275C23634DF8000EDEFE /* LoginServiceKit.swift in Sources */, - 4966E9E6211824F300A391FB /* NSImage+extension.swift in Sources */, + 4994B5542A47C4FF00E595B9 /* NormalMenuItemView.swift in Sources */, + 0AD7506B2A5B9A04001FFBBD /* ConnectionLeftPannelViewModel.swift in Sources */, F92D0B2E236D35C000575E15 /* ProxyItemView.swift in Sources */, - 49BB31E7246853EA008A4CB0 /* iCloudManager.swift in Sources */, + 49BB31E7246853EA008A4CB0 /* ICloudManager.swift in Sources */, 49B1086A216A356D0064FFCE /* String+Extension.swift in Sources */, F92D0B2C236C7C3600575E15 /* MenuItemBaseView.swift in Sources */, 499ADAFD2498CC5900C488FE /* RemoteControlManager.swift in Sources */, 49769FB427E9B3E400E3D664 /* LoginKitWrapper.m in Sources */, 4960A6DB2136529200B940C9 /* JSBridgeHandler.swift in Sources */, 493AEAE5221AE7230016FE98 /* ProxyMenuItem.swift in Sources */, + 496322222AA5D89E00854231 /* UpdateExternalResourceAction.swift in Sources */, + 49870ADD2AA76602002B106B /* UpdateConfigAction.swift in Sources */, 499A485E22ED9B7C00F6C675 /* NSTableView+Reload.swift in Sources */, F939724C23A4B33500FE5A3F /* ClashProvider.swift in Sources */, + 0AEF6EAF2A5F961B00EFEE23 /* SectionedTableView.swift in Sources */, + 498BC2532929CC2A00CA8084 /* SettingTabViewController.swift in Sources */, 49862FA0218418C600A1D5EC /* ClashRule.swift in Sources */, + 49CCDA362A55126A00FF1E13 /* ConnectionDetailInfoView.swift in Sources */, 49C9EF64223E78F5005D8B6A /* ClashProxy.swift in Sources */, 4929F684258CE07500A435F6 /* UserDefaultWrapper.swift in Sources */, F9E8F34623A12B89002DE5E8 /* String+Encode.swift in Sources */, @@ -790,11 +1041,22 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 4969E73F2A5E3CB20012E005 /* ConnectionDetailInfoGeneralView.xib */ = { + isa = PBXVariantGroup; + children = ( + 4969E73E2A5E3CB20012E005 /* Base */, + 4969E7412A5E3CB80012E005 /* zh-Hans */, + 4969E7432A5E3CB90012E005 /* zh-Hant */, + ); + name = ConnectionDetailInfoGeneralView.xib; + sourceTree = ""; + }; 4981C88D216BAE4A008CC14A /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( 4981C88C216BAE4A008CC14A /* en */, 4981C88E216BAE4D008CC14A /* zh-Hans */, + 496C16472A3418C80052064A /* zh-Hant */, ); name = Localizable.strings; sourceTree = ""; @@ -804,6 +1066,7 @@ children = ( 49CF3B2720CD7465001EBF94 /* Base */, 4981C887216BAB8A008CC14A /* zh-Hans */, + 496C16462A3418C80052064A /* zh-Hant */, ); name = Main.storyboard; sourceTree = ""; @@ -863,7 +1126,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -918,7 +1181,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -938,7 +1201,7 @@ CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; COMPRESS_PNG_FILES = YES; - CURRENT_PROJECT_VERSION = 1.91.1; + CURRENT_PROJECT_VERSION = 1.116.2; DEVELOPMENT_TEAM = MEWHFZ92DY; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -959,8 +1222,8 @@ "$(PROJECT_DIR)/ClashX", "$(PROJECT_DIR)/ClashX/goClash", ); - MACOSX_DEPLOYMENT_TARGET = 10.12; - MARKETING_VERSION = 1.91.1; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MARKETING_VERSION = 1.116.2; PRODUCT_BUNDLE_IDENTIFIER = com.west2online.ClashX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -985,7 +1248,7 @@ COMBINE_HIDPI_IMAGES = YES; COMPRESS_PNG_FILES = YES; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 1.91.1; + CURRENT_PROJECT_VERSION = 1.116.2; DEPLOYMENT_POSTPROCESSING = YES; DEVELOPMENT_TEAM = MEWHFZ92DY; ENABLE_HARDENED_RUNTIME = YES; @@ -1007,8 +1270,8 @@ "$(PROJECT_DIR)/ClashX", "$(PROJECT_DIR)/ClashX/goClash", ); - MACOSX_DEPLOYMENT_TARGET = 10.12; - MARKETING_VERSION = 1.91.1; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MARKETING_VERSION = 1.116.2; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.west2online.ClashX; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1030,7 +1293,7 @@ DEVELOPMENT_TEAM = MEWHFZ92DY; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "$(SRCROOT)/ProxyConfigHelper/Helper-Info.plist"; - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 10.14; MARKETING_VERSION = 1.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -1062,7 +1325,7 @@ DEVELOPMENT_TEAM = MEWHFZ92DY; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "$(SRCROOT)/ProxyConfigHelper/Helper-Info.plist"; - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 10.14; MARKETING_VERSION = 1.6; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -1113,6 +1376,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 4905A2C62A2058D400AEDA2E /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4905A2C72A2058D400AEDA2E /* KeyboardShortcuts */ = { + isa = XCSwiftPackageProductDependency; + package = 4905A2C62A2058D400AEDA2E /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; + productName = KeyboardShortcuts; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 49CF3B1520CD7463001EBF94 /* Project object */; } diff --git a/ClashX.xcodeproj/xcshareddata/xcschemes/ClashX.xcscheme b/ClashX.xcodeproj/xcshareddata/xcschemes/ClashX.xcscheme index 5fe716f38..755fca964 100644 --- a/ClashX.xcodeproj/xcshareddata/xcschemes/ClashX.xcscheme +++ b/ClashX.xcodeproj/xcshareddata/xcschemes/ClashX.xcscheme @@ -1,6 +1,6 @@ NSApplication.TerminateReply { + guard confirmAction() else { + return .terminateCancel + } + let group = DispatchGroup() + var shouldWait = false + + if ConfigManager.shared.proxyPortAutoSet && !ConfigManager.shared.isProxySetByOtherVariable.value || NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) || + NetworkChangeNotifier.hasInterfaceProxySetToClash() { + Logger.log("ClashX quit need clean proxy setting") + shouldWait = true + group.enter() + + SystemProxyManager.shared.disableProxy(forceDisable: ConfigManager.shared.isProxySetByOtherVariable.value) { + group.leave() + } + } + + if !shouldWait { + Logger.log("ClashX quit without clean waiting") + return .terminateNow + } + + if let statusItem = AppDelegate.shared.statusItem, statusItem.menu != nil { + statusItem.menu = nil + } + AppDelegate.shared.disposeBag = DisposeBag() + + DispatchQueue.global(qos: .default).async { + let res = group.wait(timeout: .now() + 5) + switch res { + case .success: + Logger.log("ClashX quit after clean up finish") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + NSApp.reply(toApplicationShouldTerminate: true) + } + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + NSApp.reply(toApplicationShouldTerminate: true) + } + case .timedOut: + Logger.log("ClashX quit after clean up timeout") + DispatchQueue.main.async { + NSApp.reply(toApplicationShouldTerminate: true) + } + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + NSApp.reply(toApplicationShouldTerminate: true) + } + } + } + + Logger.log("ClashX quit wait for clean up") + return .terminateLater + } + + static func confirmAction() -> Bool { + if NSApp.activationPolicy() == .regular { + let alert = NSAlert() + alert.messageText = NSLocalizedString("Quit ClashX?", comment: "") + alert.informativeText = NSLocalizedString("The active connections will be interrupted.", comment: "") + alert.alertStyle = .informational + alert.addButton(withTitle: NSLocalizedString("Quit", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + return alert.runModal() == .alertFirstButtonReturn + } + return true + } +} diff --git a/ClashX/Actions/UpdateConfigAction.swift b/ClashX/Actions/UpdateConfigAction.swift new file mode 100644 index 000000000..e4ddcf147 --- /dev/null +++ b/ClashX/Actions/UpdateConfigAction.swift @@ -0,0 +1,27 @@ +// +// UpdateConfigAction.swift +// ClashX +// +// Created by yicheng on 2023/9/5. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import Foundation + +enum UpdateConfigAction { + static func showError(text: String, configName: String) { + let alert = NSAlert() + alert.alertStyle = .critical + alert.messageText = NSLocalizedString("Reload Config Fail", comment: "") + alert.informativeText = text + alert.addButton(withTitle: NSLocalizedString("Edit in Text Mode", comment: "")) + alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) + NSApp.activate(ignoringOtherApps: true) + if alert.runModal() == .alertFirstButtonReturn { + ConfigManager.getConfigPath(configName: configName) { + NSWorkspace.shared.open(URL(fileURLWithPath: $0)) + } + } + } +} diff --git a/ClashX/Actions/UpdateExternalResourceAction.swift b/ClashX/Actions/UpdateExternalResourceAction.swift new file mode 100644 index 000000000..11eff0539 --- /dev/null +++ b/ClashX/Actions/UpdateExternalResourceAction.swift @@ -0,0 +1,54 @@ +// +// UpdateExternalResourceAction.swift +// ClashX +// +// Created by yicheng on 2023/9/4. +// Copyright © 2023 west2online. All rights reserved. +// + +import Foundation +enum UpdateExternalResourceAction { + static func run() { + ApiRequest.requestExternalProviderNames { provider in + let group = DispatchGroup() + var successCount = 0 + let totalCount = provider.proxies.count + provider.rules.count + if totalCount == 0 { + onFinished(success: 0, total: 0, fails: []) + return + } + var fails = [String]() + for name in provider.proxies { + group.enter() + ApiRequest.updateProvider(name: name, type: .proxy) { success in + if success { successCount += 1 } else { + fails.append(name) + } + group.leave() + } + } + + for name in provider.rules { + group.enter() + ApiRequest.updateProvider(name: name, type: .rule) { success in + if success { successCount += 1 } else { + fails.append(name) + } + group.leave() + } + } + + group.notify(queue: .main) { + onFinished(success: successCount, total: totalCount, fails: fails) + } + } + } + + private static func onFinished(success: Int, total: Int, fails: [String]) { + var info = String(format: NSLocalizedString("total: %d, success: %d", comment: ""), total, success) + if !fails.isEmpty { + info.append(String(format: NSLocalizedString("fails: %@", comment: ""), fails.joined(separator: " "))) + } + NSUserNotificationCenter.default.post(title: NSLocalizedString("Update external resource complete", comment: ""), info: info) + } +} diff --git a/ClashX/AppDelegate.swift b/ClashX/AppDelegate.swift index 7d255cbe7..cf2861e7b 100644 --- a/ClashX/AppDelegate.swift +++ b/ClashX/AppDelegate.swift @@ -8,19 +8,21 @@ import Alamofire import Cocoa +import CocoaLumberjack import LetsMove import RxCocoa import RxSwift import AppCenter import AppCenterAnalytics +import AppCenterCrashes +let statusItemLengthWithSpeed: CGFloat = 72 -private let statusItemLengthWithSpeed: CGFloat = 72 - -@NSApplicationMain +@main class AppDelegate: NSObject, NSApplicationDelegate { - var statusItem: NSStatusItem! + private(set) var statusItem: NSStatusItem! + @IBOutlet var checkForUpdateMenuItem: NSMenuItem! @IBOutlet var statusMenu: NSMenu! @IBOutlet var proxySettingMenuItem: NSMenuItem! @@ -43,22 +45,19 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet var apiPortMenuItem: NSMenuItem! @IBOutlet var ipMenuItem: NSMenuItem! @IBOutlet var remoteConfigAutoupdateMenuItem: NSMenuItem! - @IBOutlet var buildApiModeMenuitem: NSMenuItem! - @IBOutlet var showProxyGroupCurrentMenuItem: NSMenuItem! @IBOutlet var copyExportCommandMenuItem: NSMenuItem! @IBOutlet var copyExportCommandExternalMenuItem: NSMenuItem! - @IBOutlet var experimentalMenu: NSMenu! @IBOutlet var externalControlSeparator: NSMenuItem! + @IBOutlet var connectionsMenuItem: NSMenuItem! var disposeBag = DisposeBag() - var statusItemView: StatusItemView! + var statusItemView: StatusItemViewProtocol! var isSpeedTesting = false var runAfterConfigReload: (() -> Void)? - var dashboardWindowController: ClashWebViewWindowController? - func applicationWillFinishLaunching(_ notification: Notification) { + Logger.log("applicationWillFinishLaunching") signal(SIGPIPE, SIG_IGN) // crash recorder failLaunchProtect() @@ -76,23 +75,35 @@ class AppDelegate: NSObject, NSApplicationDelegate { // setup menu item first statusItem = NSStatusBar.system.statusItem(withLength: statusItemLengthWithSpeed) statusItemView = StatusItemView.create(statusItem: statusItem) - statusItemView.frame = CGRect(x: 0, y: 0, width: statusItemLengthWithSpeed, height: 22) + statusItemView.updateSize(width: statusItemLengthWithSpeed) statusMenu.delegate = self - registCrashLogger() + setupStatusMenuItemData() DispatchQueue.main.async { self.postFinishLaunching() } } func postFinishLaunching() { + Logger.log("postFinishLaunching") defer { statusItem.menu = statusMenu + DispatchQueue.main.asyncAfter(deadline: .now() + 8) { + self.checkMenuIconVisable() + } + } + if #unavailable(macOS 10.15) { + // dashboard is not support in macOS 10.15 below + self.dashboardMenuItem.isHidden = true + self.connectionsMenuItem.isHidden = true } - setupStatusMenuItemData() AppVersionUtil.showUpgradeAlert() - iCloudManager.shared.setup() - setupExperimentalMenuItem() + ICloudManager.shared.setup() + if WebPortalManager.hasWebProtal { + WebPortalManager.shared.addWebProtalMenuItem(&statusMenu) + } + AutoUpgardeManager.shared.setup() + AutoUpgardeManager.shared.setupCheckForUpdatesMenuItem(checkForUpdateMenuItem) // install proxy helper _ = ClashResourceManager.check() PrivilegedHelperManager.shared.checkInstall() @@ -103,13 +114,34 @@ class AppDelegate: NSObject, NSApplicationDelegate { // claer not existed selected model removeUnExistProxyGroups() + // clash logger + if ApiRequest.useDirectApi() { + Logger.log("setup built in logger/traffic") + clash_setLogBlock { line, level in + let clashLevel = ClashLogLevel(rawValue: level ?? "info") + Logger.log(line ?? "", level: clashLevel ?? .info, function: "") + } + clashSetupLogger() + + clash_setTrafficBlock { [weak self] up, down in + if RemoteControlManager.selectConfig == nil { + DispatchQueue.main.async { + self?.didUpdateTraffic(up: Int(up), down: Int(down)) + } + } + } + clashSetupTraffic() + + } else { + Logger.log("do not setup built in logger/traffic, useDirectApi = false") + } // start proxy Logger.log("initClashCore") initClashCore() Logger.log("initClashCore finish") setupData() runAfterConfigReload = { [weak self] in - if !ConfigManager.builtInApiMode { + if !Settings.builtInApiMode { self?.selectAllowLanWithMenory() } } @@ -121,68 +153,54 @@ class AppDelegate: NSObject, NSApplicationDelegate { RemoteConfigManager.shared.autoUpdateCheck() - setupNetworkNotifier() + registCrashLogger() + KeyboardShortCutManager.setup() + RemoteControlManager.setupMenuItem(separator: externalControlSeparator) } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - let group = DispatchGroup() - var shouldWait = false + return TerminalConfirmAction.run() + } - if ConfigManager.shared.proxyPortAutoSet && !ConfigManager.shared.isProxySetByOtherVariable.value || NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) || + func applicationWillTerminate(_ aNotification: Notification) { + UserDefaults.standard.set(0, forKey: "launch_fail_times") + Logger.log("ClashX will terminate") + if NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) || NetworkChangeNotifier.hasInterfaceProxySetToClash() { - Logger.log("ClashX quit need clean proxy setting") - shouldWait = true - group.enter() - - SystemProxyManager.shared.disableProxy(forceDisable: ConfigManager.shared.isProxySetByOtherVariable.value) { - group.leave() - } - } - - if !shouldWait { - Logger.log("ClashX quit without clean waiting") - return .terminateNow + Logger.log("Need Reset Proxy Setting again", level: .error) + SystemProxyManager.shared.disableProxy() } + } - if statusItem != nil, statusItem.menu != nil { - statusItem.menu = nil + func checkMenuIconVisable() { + guard let button = statusItem.button else { assertionFailure(); return } + guard let window = button.window else { assertionFailure(); return } + let buttonRect = button.convert(button.bounds, to: nil) + let onScreenRect = window.convertToScreen(buttonRect) + var leftScreenX: CGFloat = 0 + for screen in NSScreen.screens where screen.frame.origin.x < leftScreenX { + leftScreenX = screen.frame.origin.x } - disposeBag = DisposeBag() + let isMenuIconHidden = onScreenRect.midX < leftScreenX - DispatchQueue.global(qos: .default).async { - let res = group.wait(timeout: .now() + 5) - switch res { - case .success: - Logger.log("ClashX quit after clean up finish") - DispatchQueue.main.asyncAfter(deadline: .now()+0.2) { - NSApp.reply(toApplicationShouldTerminate: true) - } - DispatchQueue.global().asyncAfter(deadline: .now()+1) { - NSApp.reply(toApplicationShouldTerminate: true) - } - case .timedOut: - Logger.log("ClashX quit after clean up timeout") - DispatchQueue.main.async { - NSApp.reply(toApplicationShouldTerminate: true) - } - DispatchQueue.global().asyncAfter(deadline: .now()+1) { - NSApp.reply(toApplicationShouldTerminate: true) - } + var isCoverdByNotch = false + if #available(macOS 12, *), NSScreen.screens.count == 1, let screen = NSScreen.screens.first, let leftArea = screen.auxiliaryTopLeftArea, let rightArea = screen.auxiliaryTopRightArea { + if onScreenRect.minX > leftArea.maxX, onScreenRect.maxX < rightArea.minX { + isCoverdByNotch = true } } - Logger.log("ClashX quit wait for clean up") - return .terminateLater - } + Logger.log("checkMenuIconVisable: \(onScreenRect) \(leftScreenX), hidden: \(isMenuIconHidden), coverd by notch:\(isCoverdByNotch)") - func applicationWillTerminate(_ aNotification: Notification) { - UserDefaults.standard.set(0, forKey: "launch_fail_times") - Logger.log("ClashX will terminate") - if NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) || - NetworkChangeNotifier.hasInterfaceProxySetToClash() { - Logger.log("Need Reset Proxy Setting again",level: .error) - SystemProxyManager.shared.disableProxy() + if isMenuIconHidden || isCoverdByNotch, !Settings.disableMenubarNotice { + let alert = NSAlert() + alert.messageText = NSLocalizedString("The status icon is coverd or hide by other app.", comment: "") + alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Never show again", comment: "")) + if alert.runModal() == .alertSecondButtonReturn { + Settings.disableMenubarNotice = true + } } } @@ -194,24 +212,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.showNetSpeedIndicatorMenuItem.state = (show ?? true) ? .on : .off let statusItemLength: CGFloat = (show ?? true) ? statusItemLengthWithSpeed : 25 self.statusItem.length = statusItemLength - self.statusItemView.frame.size.width = statusItemLength + self.statusItemView.updateSize(width: statusItemLength) self.statusItemView.showSpeedContainer(show: show ?? true) }.disposed(by: disposeBag) statusItemView.updateViewStatus(enableProxy: ConfigManager.shared.proxyPortAutoSet) - - LaunchAtLogin.shared - .isEnableVirable - .asObservable() - .subscribe(onNext: { [weak self] enable in - guard let self = self else { return } - self.autoStartMenuItem.state = enable ? .on : .off - }).disposed(by: disposeBag) - - remoteConfigAutoupdateMenuItem.state = RemoteConfigManager.autoUpdateEnable ? .on : .off } func setupData() { + SSIDSuspendTool.shared.setup() ConfigManager.shared .showNetSpeedIndicatorObservable.skip(1) .bind { @@ -221,9 +230,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { Observable .merge([ConfigManager.shared.proxyPortAutoSetObservable, - ConfigManager.shared.isProxySetByOtherVariable.asObservable()]) + ConfigManager.shared.isProxySetByOtherVariable.asObservable(), + ConfigManager.shared.proxyShouldPaused.asObservable()]) + .observe(on: MainScheduler.instance) .map { _ -> NSControl.StateValue in - if ConfigManager.shared.isProxySetByOtherVariable.value && ConfigManager.shared.proxyPortAutoSet { + if (ConfigManager.shared.isProxySetByOtherVariable.value || ConfigManager.shared.proxyShouldPaused.value) && ConfigManager.shared.proxyPortAutoSet { return .mixed } return ConfigManager.shared.proxyPortAutoSet ? .on : .off @@ -239,6 +250,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { .asObservable() Observable.zip(configObservable, configObservable.skip(1)) .filter { _, new in return new != nil } + .observe(on: MainScheduler.instance) .bind { [weak self] old, config in guard let self = self, let config = config else { return } self.proxyModeDirectMenuItem.state = .off @@ -271,13 +283,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { } }.disposed(by: disposeBag) - + if !PrivilegedHelperManager.shared.isHelperCheckFinished.value && ConfigManager.shared.proxyPortAutoSet { PrivilegedHelperManager.shared.isHelperCheckFinished - .filter({$0}) + .filter { $0 } .take(1) - .take(while:{_ in ConfigManager.shared.proxyPortAutoSet}) + .take(while: { _ in ConfigManager.shared.proxyPortAutoSet }) .observe(on: MainScheduler.instance) .bind(onNext: { _ in SystemProxyManager.shared.enableProxy() @@ -285,11 +297,21 @@ class AppDelegate: NSObject, NSApplicationDelegate { } else if ConfigManager.shared.proxyPortAutoSet { SystemProxyManager.shared.enableProxy() } - + + LaunchAtLogin.shared + .isEnableVirable + .asObservable() + .subscribe(onNext: { [weak self] enable in + guard let self = self else { return } + self.autoStartMenuItem.state = enable ? .on : .off + }).disposed(by: disposeBag) + + remoteConfigAutoupdateMenuItem.state = RemoteConfigManager.autoUpdateEnable ? .on : .off + if !PrivilegedHelperManager.shared.isHelperCheckFinished.value { proxySettingMenuItem.target = nil PrivilegedHelperManager.shared.isHelperCheckFinished - .filter({$0}) + .filter { $0 } .take(1) .observe(on: MainScheduler.instance) .subscribe { [weak self] _ in @@ -328,9 +350,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { NotificationCenter .default .rx - .notification(.systemNetworkStatusIPUpdate).map({ _ in + .notification(.systemNetworkStatusIPUpdate).map { _ in NetworkChangeNotifier.getPrimaryIPAddress(allowIPV6: false) - }) + } .startWith(NetworkChangeNotifier.getPrimaryIPAddress(allowIPV6: false)) .distinctUntilChanged() .skip(1) @@ -345,7 +367,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { .asObservable() .filter { _ in ConfigManager.shared.proxyPortAutoSet } .distinctUntilChanged() - .filter { $0 }.bind { _ in + .filter { $0 } + .filter { _ in !ConfigManager.shared.proxyShouldPaused.value } + .bind { _ in let rawProxy = NetworkChangeNotifier.getRawProxySetting() Logger.log("proxy changed to no clashX setting: \(rawProxy)", level: .warning) NSUserNotificationCenter.default.postProxyChangeByOtherAppNotice() @@ -354,9 +378,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { NotificationCenter .default .rx - .notification(.systemNetworkStatusIPUpdate).map({ _ in + .notification(.systemNetworkStatusIPUpdate).map { _ in NetworkChangeNotifier.getPrimaryIPAddress(allowIPV6: false) - }).bind { [weak self] _ in + }.bind { [weak self] _ in if RemoteControlManager.selectConfig != nil { self?.resetStreamApi() } @@ -366,8 +390,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { func updateProxyList(withMenus menus: [NSMenuItem]) { let startIndex = statusMenu.items.firstIndex(of: separatorLineTop)! + 1 let endIndex = statusMenu.items.firstIndex(of: sepatatorLineEndProxySelect)! - sepatatorLineEndProxySelect.isHidden = menus.count == 0 - for _ in 0.. 0 { + if Settings.apiPortAllowLan { + apiAddr = "0.0.0.0:\(Settings.apiPort)" + } else { + apiAddr = "127.0.0.1:\(Settings.apiPort)" + } + } + let startRes = run(Settings.builtInApiMode.goObject(), + ConfigManager.allowConnectFromLan.goObject(), + Settings.enableIPV6.goObject(), + GoUint32(Settings.proxyPort), + apiAddr.goStringBuffer())? + .toString() ?? "" + let jsonData = startRes.data(using: .utf8) ?? Data() if let res = try? JSONDecoder().decode(StartProxyResp.self, from: jsonData) { let port = res.externalController.components(separatedBy: ":").last ?? "9090" + ConfigManager.shared.allowExternalControl = !res.externalController.contains("127.0.0.1") && !res.externalController.contains("localhost") ConfigManager.shared.apiPort = port ConfigManager.shared.apiSecret = res.secret ConfigManager.shared.isRunning = true @@ -424,7 +475,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { } else { ConfigManager.shared.isRunning = false proxyModeMenuItem.isEnabled = false - NSUserNotificationCenter.default.postConfigErrorNotice(msg: string) + Logger.log(startRes, level: .error) + NSUserNotificationCenter.default.postConfigErrorNotice(msg: startRes) } Logger.log("Start proxy done") } @@ -457,10 +509,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { completeHandler?(err) } - if let error = err { - NSUserNotificationCenter.default - .postNotificationAlert(title: NSLocalizedString("Reload Config Fail", comment: ""), - info: error) + if let err { + UpdateConfigAction.showError(text: err, configName: config) } else { self.syncConfig() self.resetStreamApi() @@ -483,29 +533,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - func setupExperimentalMenuItem() { - ConnectionManager.addCloseOptionMenuItem(&experimentalMenu) - ClashResourceManager.addUpdateMMDBMenuItem(&experimentalMenu) - SystemProxyManager.shared.addDisableRestoreProxyMenuItem(&experimentalMenu) - MenuItemFactory.addExperimentalMenuItem(&experimentalMenu) - if WebPortalManager.hasWebProtal { - WebPortalManager.shared.addWebProtalMenuItem(&statusMenu) - } - iCloudManager.shared.addEnableMenuItem(&experimentalMenu) - AutoUpgardeManager.shared.setup() - AutoUpgardeManager.shared.addChanelMenuItem(&experimentalMenu) - updateExperimentalFeatureStatus() - RemoteControlManager.setupMenuItem(separator: externalControlSeparator) - } - - func updateExperimentalFeatureStatus() { - buildApiModeMenuitem.state = ConfigManager.builtInApiMode ? .on : .off - showProxyGroupCurrentMenuItem.state = ConfigManager.shared.disableShowCurrentProxyInMenu ? .off : .on - } - @objc func resetProxySettingOnWakeupFromSleep() { guard !ConfigManager.shared.isProxySetByOtherVariable.value, - ConfigManager.shared.proxyPortAutoSet else { return } + ConfigManager.shared.proxyPortAutoSet else { return } guard NetworkChangeNotifier.getPrimaryInterface() != nil else { return } if !NetworkChangeNotifier.isCurrentSystemSetToClash() { let rawProxy = NetworkChangeNotifier.getRawProxySetting() @@ -522,24 +552,24 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func healthCheckOnNetworkChange() { ApiRequest.getMergedProxyData { proxyResp in - guard let proxyResp = proxyResp else {return} - + guard let proxyResp = proxyResp else { return } + var providers = Set() - - let groups = proxyResp.proxyGroups.filter({$0.type.isAutoGroup}) + + let groups = proxyResp.proxyGroups.filter(\.type.isAutoGroup) for group in groups { - group.all?.compactMap{ + group.all?.compactMap { proxyResp.proxiesMap[$0]?.enclosingProvider?.name - }.forEach{ + }.forEach { providers.insert($0) } } - + for group in groups { Logger.log("Start auto health check for group \(group.name)") ApiRequest.healthCheck(proxy: group.name) } - + for provider in providers { Logger.log("Start auto health check for provider \(provider)") ApiRequest.healthCheck(proxy: provider) @@ -551,15 +581,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: Main actions extension AppDelegate { - @IBAction func actionDashboard(_ sender: NSMenuItem) { - if dashboardWindowController == nil { - dashboardWindowController = ClashWebViewWindowController.create() - dashboardWindowController?.onWindowClose = { - [weak self] in - self?.dashboardWindowController = nil - } + @IBAction func actionDashboard(_ sender: NSMenuItem?) { + ClashWindowController.create().showWindow(sender) + } + + @IBAction func actionConnections(_ sender: NSMenuItem?) { + if #available(macOS 10.15, *) { + ClashWindowController.create().showWindow(sender) } - dashboardWindowController?.showWindow(sender) } @IBAction func actionAllowFromLan(_ sender: NSMenuItem) { @@ -587,9 +616,13 @@ extension AppDelegate { default: return } + switchProxyMode(mode: mode) + } + + func switchProxyMode(mode: ClashProxyMode) { let config = ConfigManager.shared.currentConfig?.copy() config?.mode = mode - ApiRequest.updateOutBoundMode(mode: mode) { success in + ApiRequest.updateOutBoundMode(mode: mode) { _ in ConfigManager.shared.currentConfig = config ConfigManager.selectOutBoundMode = mode MenuItemFactory.recreateProxyMenuItems() @@ -600,9 +633,11 @@ extension AppDelegate { ConfigManager.shared.showNetSpeedIndicator = !(sender.state == .on) } - @IBAction func actionSetSystemProxy(_ sender: Any) { + @IBAction func actionSetSystemProxy(_ sender: Any?) { var canSaveProxy = true - if ConfigManager.shared.isProxySetByOtherVariable.value { + if ConfigManager.shared.proxyPortAutoSet && ConfigManager.shared.proxyShouldPaused.value { + ConfigManager.shared.proxyPortAutoSet = false + } else if ConfigManager.shared.isProxySetByOtherVariable.value { // should reset proxy to clashx ConfigManager.shared.isProxySetByOtherVariable.accept(false) ConfigManager.shared.proxyPortAutoSet = true @@ -667,9 +702,17 @@ extension AppDelegate { } } + @IBAction func actionUpdateExternalResource(_ sender: Any) { + UpdateExternalResourceAction.run() + } + @IBAction func actionQuit(_ sender: Any) { NSApplication.shared.terminate(self) } + + @IBAction func actionMoreSetting(_ sender: Any) { + ClashWindowController.create().showWindow(sender) + } } // MARK: Streaming Info @@ -687,7 +730,7 @@ extension AppDelegate: ApiRequestStreamDelegate { // MARK: Help actions extension AppDelegate { - @IBAction func actionShowLog(_ sender: Any) { + @IBAction func actionShowLog(_ sender: Any?) { NSWorkspace.shared.openFile(Logger.shared.logFilePath()) } } @@ -696,8 +739,8 @@ extension AppDelegate { extension AppDelegate { @IBAction func openConfigFolder(_ sender: Any) { - if iCloudManager.shared.isICloudEnable() { - iCloudManager.shared.getUrl() { + if ICloudManager.shared.useiCloud.value { + ICloudManager.shared.getUrl { url in if let url = url { NSWorkspace.shared.open(url) @@ -715,6 +758,7 @@ extension AppDelegate { @IBAction func actionSetLogLevel(_ sender: NSMenuItem) { let level = ClashLogLevel(rawValue: sender.title.lowercased()) ?? .unknow ConfigManager.selectLoggingApiLevel = level + dynamicLogLevel = level.toDDLogLevel() updateLoggingLevel() resetStreamApi() } @@ -727,47 +771,10 @@ extension AppDelegate { @IBAction func actionUpdateRemoteConfig(_ sender: Any) { RemoteConfigManager.shared.updateCheck(ignoreTimeLimit: true, showNotification: true) } - + @IBAction func actionSetUpdateInterval(_ sender: Any) { RemoteConfigManager.showAdd() } - - @IBAction func actionSetUseApiMode(_ sender: Any) { - let alert = NSAlert() - alert.informativeText = NSLocalizedString("Need to Restart the ClashX to Take effect, Please start clashX manually", comment: "") - alert.addButton(withTitle: NSLocalizedString("Apply and Quit", comment: "")) - alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) - if alert.runModal() == .alertFirstButtonReturn { - ConfigManager.builtInApiMode = !ConfigManager.builtInApiMode - NSApp.terminate(nil) - } - } - - @IBAction func actionUpdateProxyGroupMenu(_ sender: Any) { - ConfigManager.shared.disableShowCurrentProxyInMenu = !ConfigManager.shared.disableShowCurrentProxyInMenu - updateExperimentalFeatureStatus() - MenuItemFactory.recreateProxyMenuItems() - } - - @IBAction func actionSetBenchmarkUrl(_ sender: Any) { - let alert = NSAlert() - let textfiled = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 20)) - textfiled.stringValue = ConfigManager.shared.benchMarkUrl - alert.messageText = NSLocalizedString("Benchmark", comment: "") - alert.accessoryView = textfiled - alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) - alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) - - if alert.runModal() == .alertFirstButtonReturn { - if textfiled.stringValue.isUrlVaild() { - ConfigManager.shared.benchMarkUrl = textfiled.stringValue - } else { - let err = NSAlert() - err.messageText = NSLocalizedString("URL is not valid", comment: "") - err.runModal() - } - } - } } // MARK: crash hanlder @@ -780,9 +787,10 @@ extension AppDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 5) { AppCenter.start(withAppSecret: "dce6e9a3-b6e3-4fd2-9f2d-35c767a99663", services: [ Analytics.self, + Crashes.self ]) } - + #endif } @@ -790,9 +798,9 @@ extension AppDelegate { #if DEBUG return #else - UserDefaults.standard.register(defaults: ["NSApplicationCrashOnExceptions": true]) + UserDefaults.standard.register(defaults: ["NSApplicationCrashOnExceptions": false]) let x = UserDefaults.standard - var launch_fail_times: Int = 0 + var launch_fail_times = 0 if let xx = x.object(forKey: "launch_fail_times") as? Int { launch_fail_times = xx } launch_fail_times += 1 x.set(launch_fail_times, forKey: "launch_fail_times") @@ -804,11 +812,11 @@ extension AppDelegate { UserDefaults.standard.removePersistentDomain(forName: domain) UserDefaults.standard.synchronize() } - NSUserNotificationCenter.default.post(title: "Fail on launch protect", info: "You origin Config has been renamed") + NSUserNotificationCenter.default.post(title: "Fail on launch protect", info: "You origin Config has been renamed", notiOnly: false) } - DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + Double(Int64(5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { + DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + Double(Int64(5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { x.set(0, forKey: "launch_fail_times") - }) + } #endif } } @@ -841,8 +849,8 @@ extension AppDelegate { } } - if iCloudManager.shared.isICloudEnable() { - iCloudManager.shared.getConfigFilesList { list in + if ICloudManager.shared.useiCloud.value { + ICloudManager.shared.getConfigFilesList { list in action(list) } } else { @@ -932,7 +940,7 @@ extension AppDelegate { NotificationCenter.default.post(name: Notification.Name(rawValue: "didGetUrl"), object: nil, userInfo: userInfo) } } else if host == "update-config" { - updateConfig() + updateConfig() } } } diff --git a/ClashX/AppleScript/ProxyModeChangeCommand.swift b/ClashX/AppleScript/ProxyModeChangeCommand.swift index 0c195c6a9..d6eccfd1d 100644 --- a/ClashX/AppleScript/ProxyModeChangeCommand.swift +++ b/ClashX/AppleScript/ProxyModeChangeCommand.swift @@ -6,8 +6,8 @@ // Copyright © 2022 west2online. All rights reserved. // -import Foundation import AppKit +import Foundation @objc class ProxyModeChangeCommand: NSScriptCommand { override func performDefaultImplementation() -> Any? { @@ -31,6 +31,10 @@ import AppKit menuItem = delegate.proxyModeGlobalMenuItem case .direct: menuItem = delegate.proxyModeDirectMenuItem + #if PRO_VERSION + case .script: + menuItem = delegate.proxyModeScriptMenuItem + #endif } delegate.actionSwitchProxyMode(menuItem) return nil diff --git a/ClashX/AppleScript/ProxySettingCommand.swift b/ClashX/AppleScript/ProxySettingCommand.swift index dccaf6517..a256528fb 100644 --- a/ClashX/AppleScript/ProxySettingCommand.swift +++ b/ClashX/AppleScript/ProxySettingCommand.swift @@ -6,8 +6,8 @@ // Copyright © 2022 west2online. All rights reserved. // -import Foundation import AppKit +import Foundation @objc class ProxySettingCommand: NSScriptCommand { override func performDefaultImplementation() -> Any? { diff --git a/ClashX/Base.lproj/Main.storyboard b/ClashX/Base.lproj/Main.storyboard index 4cbbb0a16..6c960ccf7 100644 --- a/ClashX/Base.lproj/Main.storyboard +++ b/ClashX/Base.lproj/Main.storyboard @@ -1,11 +1,525 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -15,6 +529,13 @@ + + + + + + + @@ -176,12 +697,12 @@ - + + - @@ -195,7 +716,6 @@ - @@ -274,10 +794,16 @@ + + + + + + - + - + @@ -290,6 +816,12 @@ + + + + + + @@ -319,40 +851,15 @@ - + - + - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - + @@ -361,6 +868,12 @@ + + + + + + @@ -373,9 +886,6 @@ - - - @@ -464,7 +974,7 @@ - + @@ -677,7 +1187,7 @@ - + @@ -716,7 +1226,7 @@ - + @@ -755,7 +1265,7 @@ - + @@ -880,7 +1390,7 @@ - + @@ -912,7 +1422,7 @@ - + @@ -957,7 +1467,7 @@ - + @@ -999,7 +1509,7 @@ - + @@ -1050,7 +1560,7 @@ - + @@ -1119,8 +1629,424 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ClashX/Basic/Combine+Ext.swift b/ClashX/Basic/Combine+Ext.swift new file mode 100644 index 000000000..501c4e3f8 --- /dev/null +++ b/ClashX/Basic/Combine+Ext.swift @@ -0,0 +1,22 @@ +// +// Combine+Ext.swift +// ClashX +// +// Created by yicheng on 2023/7/6. +// Copyright © 2023 west2online. All rights reserved. +// + +import Combine +import Foundation + +@available(macOS 10.15, *) +public extension Publisher where Failure == Never { + func weakAssign( + to keyPath: ReferenceWritableKeyPath, + on object: T + ) -> AnyCancellable { + sink { [weak object] value in + object?[keyPath: keyPath] = value + } + } +} diff --git a/ClashX/Basic/LaunchAtLogin.swift b/ClashX/Basic/LaunchAtLogin.swift index 3400ac66a..7bd73edff 100644 --- a/ClashX/Basic/LaunchAtLogin.swift +++ b/ClashX/Basic/LaunchAtLogin.swift @@ -1,5 +1,5 @@ // -// AutoStartManager.swift +// LaunchAtLogin.swift // ClashX // // Created by CYC on 2018/6/14. diff --git a/ClashX/Basic/Logger.swift b/ClashX/Basic/Logger.swift index 063a8c93d..e4fd5d6b0 100644 --- a/ClashX/Basic/Logger.swift +++ b/ClashX/Basic/Logger.swift @@ -10,41 +10,46 @@ import CocoaLumberjack import Foundation class Logger { static let shared = Logger() - var fileLogger: DDFileLogger = DDFileLogger() + var fileLogger: DDFileLogger = .init() private init() { #if DEBUG DDLog.add(DDOSLogger.sharedInstance) #endif - //default time zone is "UTC" + // default time zone is "UTC" let dataFormatter = DateFormatter() dataFormatter.setLocalizedDateFormatFromTemplate("YYYY/MM/dd HH:mm:ss:SSS") - fileLogger.logFormatter = DDLogFileFormatterDefault.init(dateFormatter: dataFormatter) + fileLogger.logFormatter = DDLogFileFormatterDefault(dateFormatter: dataFormatter) fileLogger.rollingFrequency = TimeInterval(60 * 60 * 24) // 24 hours fileLogger.logFileManager.maximumNumberOfLogFiles = 3 DDLog.add(fileLogger) + dynamicLogLevel = ConfigManager.selectLoggingApiLevel.toDDLogLevel() } private func logToFile(msg: String, level: ClashLogLevel) { switch level { case .debug, .silent: - DDLogDebug(msg) + DDLogDebug(DDLogMessageFormat(stringLiteral: msg)) case .error: - DDLogError(msg) + DDLogError(DDLogMessageFormat(stringLiteral: msg)) case .info: - DDLogInfo(msg) + DDLogInfo(DDLogMessageFormat(stringLiteral: msg)) case .warning: - DDLogWarn(msg) + DDLogWarn(DDLogMessageFormat(stringLiteral: msg)) case .unknow: - DDLogVerbose(msg) + DDLogWarn(DDLogMessageFormat(stringLiteral: msg)) } } - static func log(_ msg: String, level: ClashLogLevel = .info) { - shared.logToFile(msg: "[\(level.rawValue)] \(msg)", level: level) + static func log(_ msg: String, level: ClashLogLevel = .info, file: String = #file, function: String = #function) { + shared.logToFile(msg: "[\(level.rawValue)] \(file) \(function) \(msg)", level: level) } func logFilePath() -> String { return fileLogger.logFileManager.sortedLogFilePaths.first ?? "" } + + func logFolder() -> String { + return fileLogger.logFileManager.logsDirectory + } } diff --git a/ClashX/Basic/NSImage+extension.swift b/ClashX/Basic/NSImage+extension.swift deleted file mode 100644 index 0daecfb3a..000000000 --- a/ClashX/Basic/NSImage+extension.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// NSImage+extension.swift -// ClashX -// -// Created by CYC on 2018/8/6. -// Copyright © 2018年 yichengchen. All rights reserved. -// - -import AppKit -import Foundation - -extension NSImage { - func tint(color: NSColor) -> NSImage { - let image = copy() as! NSImage - image.lockFocus() - - color.set() - - let imageRect = NSRect(origin: NSZeroPoint, size: image.size) - imageRect.fill(using: .sourceIn) - - image.unlockFocus() - image.isTemplate = false - - return image - } -} diff --git a/ClashX/Basic/NSView+Layout.swift b/ClashX/Basic/NSView+Layout.swift new file mode 100644 index 000000000..0f13094e2 --- /dev/null +++ b/ClashX/Basic/NSView+Layout.swift @@ -0,0 +1,35 @@ +// +// NSView+Layout.swift +// BilibiliLive +// +// Created by Etan Chen on 2021/4/4. +// + +import AppKit + +extension NSView { + @discardableResult + func makeConstraints(_ block: (NSView) -> [NSLayoutConstraint]) -> Self { + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(block(self)) + return self + } + + @discardableResult + func makeConstraintsToBindToSuperview(_ inset: NSEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: 0)) -> Self { + return makeConstraints { [ + $0.leftAnchor.constraint(equalTo: $0.superview!.leftAnchor, constant: inset.left), + $0.rightAnchor.constraint(equalTo: $0.superview!.rightAnchor, constant: -inset.right), + $0.topAnchor.constraint(equalTo: $0.superview!.topAnchor, constant: inset.top), + $0.bottomAnchor.constraint(equalTo: $0.superview!.bottomAnchor, constant: -inset.bottom) + ] } + } + + @discardableResult + func makeConstraintsBindToCenterOfSuperview() -> Self { + return makeConstraints { [ + $0.centerXAnchor.constraint(equalTo: $0.superview!.centerXAnchor), + $0.centerYAnchor.constraint(equalTo: $0.superview!.centerYAnchor) + ] } + } +} diff --git a/ClashX/Basic/SpeedUtils.swift b/ClashX/Basic/SpeedUtils.swift new file mode 100644 index 000000000..14d1dd2f6 --- /dev/null +++ b/ClashX/Basic/SpeedUtils.swift @@ -0,0 +1,32 @@ +// +// SpeedUtils.swift +// ClashX +// +// Created by yicheng on 2023/7/6. +// Copyright © 2023 west2online. All rights reserved. +// + +import Foundation + +enum SpeedUtils { + static func getSpeedString(for byte: Int) -> String { + return getNetString(for: byte).appending("/s") + } + + static func getNetString(for byte: Int) -> String { + let kb = byte / 1024 + if kb < 1024 { + return "\(kb)KB" + } else { + let mb = Double(kb) / 1024.0 + if mb >= 100 { + if mb >= 1000 { + return String(format: "%.1fGB", mb / 1024) + } + return String(format: "%.1fMB", mb) + } else { + return String(format: "%.2fMB", mb) + } + } + } +} diff --git a/ClashX/Basic/String+Extension.swift b/ClashX/Basic/String+Extension.swift index 0d94f3416..6710cfd3f 100644 --- a/ClashX/Basic/String+Extension.swift +++ b/ClashX/Basic/String+Extension.swift @@ -9,11 +9,11 @@ import Foundation extension String { func isUrlVaild() -> Bool { - guard count > 0 else { return false } + guard !isEmpty else { return false } guard let url = URL(string: self) else { return false } guard url.host != nil, - let scheme = url.scheme else { + let scheme = url.scheme else { return false } return ["http", "https"].contains(scheme) diff --git a/ClashX/ClashWindowController.swift b/ClashX/ClashWindowController.swift new file mode 100644 index 000000000..ffc9d66c1 --- /dev/null +++ b/ClashX/ClashWindowController.swift @@ -0,0 +1,87 @@ +// +// ClashWindowController.swift +// ClashX +// +// Created by yicheng on 2023/7/5. +// Copyright © 2023 west2online. All rights reserved. +// +import AppKit + +private class ClashWindowsRecorder { + static let shared = ClashWindowsRecorder() + var windowControllers = [NSWindowController]() { + didSet { + if windowControllers.isEmpty { + NSApp.setActivationPolicy(.accessory) + } else { + if NSApp.activationPolicy() == .accessory { + NSApp.setActivationPolicy(.regular) + } + } + } + } +} + +class ClashWindowController: NSWindowController, NSWindowDelegate { + var onWindowClose: (() -> Void)? + private var fromCache = false + private var lastSize: CGSize? { + get { + if let str = UserDefaults.standard.value(forKey: "lastSize.\(T.className())") as? String { + return NSSizeFromString(str) as CGSize + } + return nil + } + set { + if let size = newValue { + UserDefaults.standard.set(NSStringFromSize(size), forKey: "lastSize.\(T.className())") + } + } + } + + static func create() -> NSWindowController { + if let wc = ClashWindowsRecorder.shared.windowControllers.first(where: { $0 is Self }) { + (wc as? ClashWindowController)?.fromCache = true + return wc + } + let win = NSWindow() + let wc = ClashWindowController(window: win) + if let X = T.self as? NibLoadable.Type { + wc.contentViewController = (X.createFromNib(in: .main) as! NSViewController) + } else { + wc.contentViewController = T() + } + win.titlebarAppearsTransparent = false + win.styleMask.insert(.closable) + win.styleMask.insert(.resizable) + win.styleMask.insert(.miniaturizable) + if let title = wc.contentViewController?.title { + win.title = title + } + ClashWindowsRecorder.shared.windowControllers.append(wc) + return wc + } + + override func showWindow(_ sender: Any?) { + super.showWindow(sender) + NSApp.activate(ignoringOtherApps: true) + if !fromCache, let lastSize = lastSize, lastSize != .zero { + window?.setContentSize(lastSize) + window?.center() + } + window?.makeKeyAndOrderFront(self) + window?.delegate = self + NSApp.activate(ignoringOtherApps: true) + window?.makeKeyAndOrderFront(nil) + } + + func windowWillClose(_ notification: Notification) { + ClashWindowsRecorder.shared.windowControllers.removeAll(where: { $0 == self }) + onWindowClose?() + if let win = window { + if !win.styleMask.contains(.fullScreen) { + lastSize = win.frame.size + } + } + } +} diff --git a/ClashX/ClashX-Bridging-Header.h b/ClashX/ClashX-Bridging-Header.h index 8a813a9fc..05b7dc49e 100644 --- a/ClashX/ClashX-Bridging-Header.h +++ b/ClashX/ClashX-Bridging-Header.h @@ -4,3 +4,5 @@ #import "goClash.h" #import "ProxyConfigRemoteProcessProtocol.h" #import "LoginKitWrapper.h" +#import +#import diff --git a/ClashX/Extensions/DateFormatter+.swift b/ClashX/Extensions/DateFormatter+.swift index 4a970c2fe..aa6f8f518 100644 --- a/ClashX/Extensions/DateFormatter+.swift +++ b/ClashX/Extensions/DateFormatter+.swift @@ -15,4 +15,10 @@ extension DateFormatter { dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" return dateFormatter } + + static var simple: DateFormatter { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MM-dd HH:mm:ss" + return dateFormatter + } } diff --git a/ClashX/Extensions/NSTableView+Reload.swift b/ClashX/Extensions/NSTableView+Reload.swift index 16ef4a831..ee50f3d6a 100644 --- a/ClashX/Extensions/NSTableView+Reload.swift +++ b/ClashX/Extensions/NSTableView+Reload.swift @@ -10,7 +10,7 @@ import Cocoa extension NSTableView { func reloadDataKeepingSelection() { - let selectedRowIndexes = self.selectedRowIndexes + let selectedRowIndexes = selectedRowIndexes reloadData() var indexs = IndexSet() for index in selectedRowIndexes { diff --git a/ClashX/Extensions/NSUserNotificationCenter+Extension.swift b/ClashX/Extensions/NSUserNotificationCenter+Extension.swift index 4fef6be01..81fc63480 100644 --- a/ClashX/Extensions/NSUserNotificationCenter+Extension.swift +++ b/ClashX/Extensions/NSUserNotificationCenter+Extension.swift @@ -10,7 +10,7 @@ import Cocoa import UserNotifications extension NSUserNotificationCenter { - func post(title: String, info: String, identifier: String? = nil, notiOnly: Bool = false) { + func post(title: String, info: String, identifier: String? = nil, notiOnly: Bool = true) { if #available(OSX 10.14, *) { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.delegate = UserNotificationCenterDelegate.shared @@ -27,7 +27,7 @@ extension NSUserNotificationCenter { self?.postNotification(title: title, info: info, identifier: identifier) } case .notDetermined: - notificationCenter.requestAuthorization(options: .alert) { granted, err in + notificationCenter.requestAuthorization(options: .alert) { granted, _ in if granted { DispatchQueue.main.async { self?.postNotification(title: title, info: info, identifier: identifier) @@ -49,9 +49,9 @@ extension NSUserNotificationCenter { postNotification(title: title, info: info, identifier: identifier) } } - + private func postNotification(title: String, info: String, identifier: String? = nil) { - var userInfo:[String : Any] = [:] + var userInfo: [String: Any] = [:] if let identifier = identifier { userInfo = ["identifier": identifier] } @@ -60,20 +60,20 @@ extension NSUserNotificationCenter { notificationCenter.delegate = UserNotificationCenterDelegate.shared notificationCenter.removeAllDeliveredNotifications() notificationCenter.removeAllPendingNotificationRequests() - let content = UNMutableNotificationContent(); + let content = UNMutableNotificationContent() content.title = title content.body = info content.userInfo = userInfo let uuidString = UUID().uuidString let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: nil) notificationCenter.add(request) { error in - if let err = error { - Logger.log("send noti fail: \(String(describing: err))") - DispatchQueue.main.async { - self.postNotificationAlert(title: title, info: info, identifier: identifier) - } + if let err = error { + Logger.log("send noti fail: \(String(describing: err))") + DispatchQueue.main.async { + self.postNotificationAlert(title: title, info: info, identifier: identifier) } } + } } else { let notification = NSUserNotification() notification.title = title @@ -83,7 +83,7 @@ extension NSUserNotificationCenter { deliver(notification) } } - + func postNotificationAlert(title: String, info: String, identifier: String? = nil) { if Settings.disableNoti { return @@ -97,41 +97,41 @@ extension NSUserNotificationCenter { UserNotificationCenterDelegate.shared.handleNotificationActive(with: identifier) } } - + func postConfigFileChangeDetectionNotice() { post(title: NSLocalizedString("Config file have been changed", comment: ""), info: NSLocalizedString("Tap to reload config", comment: ""), identifier: "postConfigFileChangeDetectionNotice") } - + func postStreamApiConnectFail(api: String) { post(title: "\(api) api connect error!", info: NSLocalizedString("Use reload config to try reconnect.", comment: "")) } - + func postConfigErrorNotice(msg: String) { - let configName = ConfigManager.selectConfigName.count > 0 ? - Paths.configFileName(for: ConfigManager.selectConfigName) : "" - + let configName = ConfigManager.selectConfigName.isEmpty ? "" : + Paths.configFileName(for: ConfigManager.selectConfigName) + let message = "\(configName): \(msg)" postNotificationAlert(title: NSLocalizedString("Config loading Fail!", comment: ""), info: message) } - + func postSpeedTestBeginNotice() { post(title: NSLocalizedString("Benchmark", comment: ""), info: NSLocalizedString("Benchmark has begun, please wait.", comment: "")) } - + func postSpeedTestingNotice() { post(title: NSLocalizedString("Benchmark", comment: ""), info: NSLocalizedString("Benchmark is processing, please wait.", comment: "")) } - + func postSpeedTestFinishNotice() { post(title: NSLocalizedString("Benchmark", comment: ""), - info: NSLocalizedString("Benchmark Finished!", comment: "")) + info: NSLocalizedString("Benchmark Finished!", comment: ""), notiOnly: false) } - + func postProxyChangeByOtherAppNotice() { post(title: NSLocalizedString("System Proxy Changed", comment: ""), info: NSLocalizedString("Proxy settings are changed by another process. ClashX is no longer the default system proxy.", comment: ""), notiOnly: true) @@ -140,33 +140,32 @@ extension NSUserNotificationCenter { class UserNotificationCenterDelegate: NSObject, NSUserNotificationCenterDelegate, UNUserNotificationCenterDelegate { static let shared = UserNotificationCenterDelegate() - + func userNotificationCenter(_ center: NSUserNotificationCenter, didActivate notification: NSUserNotification) { - if let identifier = notification.userInfo?["identifier"] as? String { - handleNotificationActive(with: identifier) - } - center.removeAllDeliveredNotifications() + if let identifier = notification.userInfo?["identifier"] as? String { + handleNotificationActive(with: identifier) + } + center.removeAllDeliveredNotifications() } - + func userNotificationCenter(_ center: NSUserNotificationCenter, shouldPresent notification: NSUserNotification) -> Bool { return true } - + @available(macOS 10.14, *) func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - if let identifier = response.notification.request.content.userInfo["identifier"] as? String { handleNotificationActive(with: identifier) } center.removeAllDeliveredNotifications() completionHandler() } - + @available(macOS 10.14, *) func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler(.alert) } - + func handleNotificationActive(with identifier: String) { switch identifier { case "postConfigFileChangeDetectionNotice": @@ -176,4 +175,3 @@ class UserNotificationCenterDelegate: NSObject, NSUserNotificationCenterDelegate } } } - diff --git a/ClashX/Extensions/NSView+Nib.swift b/ClashX/Extensions/NSView+Nib.swift index 09bef9396..492a51d0f 100644 --- a/ClashX/Extensions/NSView+Nib.swift +++ b/ClashX/Extensions/NSView+Nib.swift @@ -27,3 +27,15 @@ extension NibLoadable where Self: NSView { return views.last as! Self } } + +extension NibLoadable where Self: NSViewController { + static var nibName: String? { + return String(describing: Self.self) + } + + static func createFromNib(in bundle: Bundle = Bundle.main) -> Self { + guard let nibName = nibName else { fatalError() } + let sb = NSStoryboard(name: "Main", bundle: Bundle.main) + return sb.instantiateController(withIdentifier: nibName) as! Self + } +} diff --git a/ClashX/General/ApiRequest.swift b/ClashX/General/ApiRequest.swift index ec910c6e8..d661666e8 100644 --- a/ClashX/General/ApiRequest.swift +++ b/ClashX/General/ApiRequest.swift @@ -11,7 +11,7 @@ import Cocoa import Starscream import SwiftyJSON -protocol ApiRequestStreamDelegate: class { +protocol ApiRequestStreamDelegate: AnyObject { func didUpdateTraffic(up: Int, down: Int) func didGetLog(log: String, level: String) } @@ -36,9 +36,9 @@ class ApiRequest { alamoFireManager = Session(configuration: configuration) } - private static func authHeader() -> HTTPHeaders { + static func authHeader() -> HTTPHeaders { let secret = ConfigManager.shared.overrideSecret ?? ConfigManager.shared.apiSecret - return (secret.count > 0) ? ["Authorization": "Bearer \(secret)"] : [:] + return (!secret.isEmpty) ? ["Authorization": "Bearer \(secret)"] : [:] } @discardableResult @@ -77,7 +77,7 @@ class ApiRequest { if ConfigManager.shared.overrideApiURL != nil { return false } - return ConfigManager.builtInApiMode + return Settings.builtInApiMode } static func requestConfig(completeHandler: @escaping ((ClashConfig) -> Void)) { @@ -105,18 +105,8 @@ class ApiRequest { } static func requestConfigUpdate(configName: String, callback: @escaping ((ErrorString?) -> Void)) { - if iCloudManager.shared.isICloudEnable() { - iCloudManager.shared.getUrl { url in - guard let url = url else { - callback("icloud error") - return - } - let configPath = url.appendingPathComponent(Paths.configFileName(for: configName)).path - requestConfigUpdate(configPath: configPath, callback: callback) - } - } else { - let filePath = Paths.localConfigPath(for: configName) - requestConfigUpdate(configPath: filePath, callback: callback) + ConfigManager.getConfigPath(configName: configName) { + requestConfigUpdate(configPath: $0, callback: callback) } } @@ -125,7 +115,7 @@ class ApiRequest { // DEV MODE: Use API if !useDirectApi() { - req("/configs", method: .put, parameters: ["Path": configPath], encoding: JSONEncoding.default).responseJSON { res in + req("/configs", method: .put, parameters: ["Path": configPath], encoding: JSONEncoding.default).responseData { res in if res.response?.statusCode == 204 { ConfigManager.shared.isRunning = true callback(nil) @@ -155,7 +145,7 @@ class ApiRequest { static func updateOutBoundMode(mode: ClashProxyMode, callback: ((Bool) -> Void)? = nil) { req("/configs", method: .patch, parameters: ["mode": mode.rawValue], encoding: JSONEncoding.default) - .responseJSON { response in + .responseData { response in switch response.result { case .success: callback?(true) @@ -166,7 +156,7 @@ class ApiRequest { } static func updateLogLevel(level: ClashLogLevel, callback: ((Bool) -> Void)? = nil) { - req("/configs", method: .patch, parameters: ["log-level": level.rawValue], encoding: JSONEncoding.default).responseJSON(completionHandler: { response in + req("/configs", method: .patch, parameters: ["log-level": level.rawValue], encoding: JSONEncoding.default).responseData(completionHandler: { response in switch response.result { case .success: callback?(true) @@ -177,7 +167,7 @@ class ApiRequest { } static func requestProxyGroupList(completeHandler: ((ClashProxyResp) -> Void)? = nil) { - req("/proxies").responseJSON { + req("/proxies").responseData { res in let proxies = ClashProxyResp(try? res.result.get()) ApiRequest.shared.proxyRespCache = proxies @@ -192,7 +182,7 @@ class ApiRequest { case let .success(providerResp): completeHandler?(providerResp) case let .failure(err): - print(err) + Logger.log("\(err)") completeHandler?(ClashProviderResp()) assertionFailure() } @@ -215,7 +205,7 @@ class ApiRequest { method: .put, parameters: ["name": selectProxy], encoding: JSONEncoding.default) - .responseJSON { response in + .responseData { response in callback(response.response?.statusCode == 204) } } @@ -262,8 +252,8 @@ class ApiRequest { static func getProxyDelay(proxyName: String, callback: @escaping ((Int) -> Void)) { req("/proxies/\(proxyName.encoded)/delay", method: .get, - parameters: ["timeout": 5000, "url": ConfigManager.shared.benchMarkUrl]) - .responseJSON { res in + parameters: ["timeout": 5000, "url": Settings.benchMarkUrl]) + .responseData { res in switch res.result { case let .success(value): let json = JSON(value) @@ -298,8 +288,8 @@ class ApiRequest { // MARK: - Connections extension ApiRequest { - static func getConnections(completeHandler: @escaping ([ClashConnectionSnapShot.Connection]) -> Void) { - req("/connections").responseDecodable(of: ClashConnectionSnapShot.self) { resp in + static func getConnections(completeHandler: @escaping ([ClashConnectionBaseSnapShot.Connection]) -> Void) { + req("/connections").responseDecodable(of: ClashConnectionBaseSnapShot.self) { resp in switch resp.result { case let .success(snapshot): completeHandler(snapshot.connections) @@ -310,12 +300,88 @@ extension ApiRequest { } } - static func closeConnection(_ conn: ClashConnectionSnapShot.Connection) { - req("/connections/".appending(conn.id), method: .delete).response { _ in } + static func closeConnection(_ id: String) { + req("/connections/\(id)", method: .delete).response { _ in } } static func closeAllConnection() { - req("/connections", method: .delete).response { _ in } + if useDirectApi() { + clash_closeAllConnections() + } else { + req("/connections", method: .delete).response { _ in } + } + } + + // MARK: - Providers + + struct AllProviders { + var proxies = [String]() + var rules = [String]() + } + + static func requestExternalProviderNames(completeHandler: @escaping (AllProviders) -> Void) { + var providers = AllProviders() + let group = DispatchGroup() + group.enter() + ApiRequest.req("/providers/proxies").responseData { resp in + switch resp.result { + case let .success(res): + let json = JSON(res) + let provoders = json["providers"].dictionaryValue + .filter { $0.value["vehicleType"] == "HTTP" }.map(\.key) + providers.proxies = provoders + case let .failure(err): + Logger.log(err.localizedDescription, level: .warning) + } + group.leave() + } + + #if PRO_VERSION + group.enter() + ApiRequest.req("/providers/rules").responseData { resp in + switch resp.result { + case let .success(res): + let json = JSON(res) + let provoders = json["providers"].dictionaryValue + .filter { $0.value["vehicleType"] == "HTTP" }.map(\.key) + providers.rules = provoders + case let .failure(err): + Logger.log(err.localizedDescription, level: .warning) + } + group.leave() + } + #endif + group.notify(queue: .main) { + completeHandler(providers) + } + } + + enum ProviderType { + case proxy + case rule + } + + static func updateProvider(name: String, type: ProviderType, completeHandler: @escaping (Bool) -> Void) { + let url: String + switch type { + case .proxy: + url = "/providers/proxies/\(name.encoded)" + case .rule: + url = "/providers/rules/\(name.encoded)" + } + ApiRequest.req(url, method: .put).response { resp in + if resp.response?.statusCode == 204 { + completeHandler(true) + } else { + completeHandler(false) + } + } + } + + static func resetFakeIpCache() { + ApiRequest.req("/cache/fakeip/flush", method: .post).response { resp in + Logger.log("flush fake ip: \(resp.response?.statusCode ?? -1)") + } } } @@ -342,9 +408,13 @@ extension ApiRequest { } private func requestTrafficInfo() { + if ApiRequest.useDirectApi() { + trafficWebSocket?.disconnect(forceTimeout: 0.5) + return + } trafficWebSocketRetryTimer?.invalidate() trafficWebSocketRetryTimer = nil - trafficWebSocket?.disconnect(forceTimeout: 0, closeCode: 0) + trafficWebSocket?.disconnect(forceTimeout: 0.5) let socket = WebSocket(url: URL(string: ConfigManager.apiUrl.appending("/traffic"))!) @@ -357,9 +427,13 @@ extension ApiRequest { } private func requestLog() { + if ApiRequest.useDirectApi() { + loggingWebSocket?.disconnect(forceTimeout: 1) + return + } loggingWebSocketRetryTimer?.invalidate() loggingWebSocketRetryTimer = nil - loggingWebSocket?.disconnect() + loggingWebSocket?.disconnect(forceTimeout: 1) let uriString = "/logs?level=".appending(ConfigManager.selectLoggingApiLevel.rawValue) let socket = WebSocket(url: URL(string: ConfigManager.apiUrl.appending(uriString))!) diff --git a/ClashX/General/Managers/AutoUpgardeManager.swift b/ClashX/General/Managers/AutoUpgardeManager.swift index 9a85da188..3000745cf 100644 --- a/ClashX/General/Managers/AutoUpgardeManager.swift +++ b/ClashX/General/Managers/AutoUpgardeManager.swift @@ -10,74 +10,62 @@ import Cocoa import Sparkle class AutoUpgardeManager: NSObject { + var checkForUpdatesMenuItem: NSMenuItem? static let shared = AutoUpgardeManager() - + private var controller: SPUStandardUpdaterController? private var current: Channel = { if let value = UserDefaults.standard.object(forKey: "AutoUpgardeManager.current") as? Int, - let channel = Channel(rawValue: value) { return channel } - return .stable + let channel = Channel(rawValue: value) { return channel } + #if PRO_VERSION + return .appcenter + #else + return .stable + #endif }() { didSet { UserDefaults.standard.set(current.rawValue, forKey: "AutoUpgardeManager.current") } } - private lazy var menuItems: [Channel: NSMenuItem] = { - var items = [Channel: NSMenuItem]() - for channel in Channel.allCases { - let item = NSMenuItem(title: channel.title, action: #selector(didSelectUpgradeChannel(_:)), keyEquivalent: "") - item.target = self - item.tag = channel.rawValue - items[channel] = item - } - return items - }() - - private var allowSelectChannel:Bool { + private var allowSelectChannel: Bool { return Bundle.main.object(forInfoDictionaryKey: "SUDisallowSelectChannel") as? Bool != true } // MARK: Public func setup() { - guard WebPortalManager.hasWebProtal == false, allowSelectChannel else { return } - SUUpdater.shared()?.delegate = self + controller = SPUStandardUpdaterController(updaterDelegate: self, userDriverDelegate: nil) } - func addChanelMenuItem(_ menu: inout NSMenu) { - guard WebPortalManager.hasWebProtal == false, allowSelectChannel else { return } - let upgradeMenu = NSMenu(title: NSLocalizedString("Upgrade Channel", comment: "")) - for (_, item) in menuItems { - upgradeMenu.addItem(item) - } - - let upgradeMenuItem = NSMenuItem(title: NSLocalizedString("Upgrade Channel", comment: ""), action: nil, keyEquivalent: "") - upgradeMenuItem.submenu = upgradeMenu - menu.addItem(upgradeMenuItem) - updateDisplayStatus() + func setupCheckForUpdatesMenuItem(_ item: NSMenuItem) { + checkForUpdatesMenuItem = item + checkForUpdatesMenuItem?.target = controller + checkForUpdatesMenuItem?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) } -} -extension AutoUpgardeManager { - @objc private func didSelectUpgradeChannel(_ menuItem: NSMenuItem) { - guard let channel = Channel(rawValue: menuItem.tag) else { return } - current = channel - updateDisplayStatus() + func addChannelMenuItem(_ button: NSPopUpButton) { + for channel in Channel.allCases { + button.addItem(withTitle: channel.title) + button.lastItem?.tag = channel.rawValue + } + button.target = self + button.action = #selector(didselectChannel(sender:)) + button.selectItem(withTag: current.rawValue) } - private func updateDisplayStatus() { - for (channel, menuItem) in menuItems { - menuItem.state = channel == current ? .on : .off - } + @objc func didselectChannel(sender: NSPopUpButton) { + guard let tag = sender.selectedItem?.tag, let channel = Channel(rawValue: tag) else { return } + current = channel } } -extension AutoUpgardeManager: SUUpdaterDelegate { - func feedURLString(for updater: SUUpdater) -> String? { +extension AutoUpgardeManager: SPUUpdaterDelegate { + func feedURLString(for updater: SPUUpdater) -> String? { + guard WebPortalManager.hasWebProtal == false, allowSelectChannel else { return nil } return current.urlString } - func updaterWillRelaunchApplication(_ updater: SUUpdater) { + func updaterWillRelaunchApplication(_ updater: SPUUpdater) { SystemProxyManager.shared.disableProxy(port: 0, socksPort: 0, forceDisable: true) } } @@ -86,8 +74,10 @@ extension AutoUpgardeManager: SUUpdaterDelegate { extension AutoUpgardeManager { enum Channel: Int, CaseIterable { - case stable - case prelease + #if !PRO_VERSION + case stable + case prelease + #endif case appcenter } } @@ -95,10 +85,12 @@ extension AutoUpgardeManager { extension AutoUpgardeManager.Channel { var title: String { switch self { - case .stable: - return NSLocalizedString("Stable", comment: "") - case .prelease: - return NSLocalizedString("Prelease", comment: "") + #if !PRO_VERSION + case .stable: + return NSLocalizedString("Stable", comment: "") + case .prelease: + return NSLocalizedString("Prelease", comment: "") + #endif case .appcenter: return "Appcenter" } @@ -106,12 +98,18 @@ extension AutoUpgardeManager.Channel { var urlString: String { switch self { - case .stable: - return "https://yichengchen.github.io/clashX/appcast.xml" - case .prelease: - return "https://yichengchen.github.io/clashX/appcast_pre.xml" + #if !PRO_VERSION + case .stable: + return "https://yichengchen.github.io/clashX/appcast.xml" + case .prelease: + return "https://yichengchen.github.io/clashX/appcast_pre.xml" + #endif case .appcenter: - return "https://api.appcenter.ms/v0.1/public/sparkle/apps/dce6e9a3-b6e3-4fd2-9f2d-35c767a99663" + #if PRO_VERSION + return "https://api.appcenter.ms/v0.1/public/sparkle/apps/1cd052f7-e118-4d13-87fb-35176f9702c1" + #else + return "https://api.appcenter.ms/v0.1/public/sparkle/apps/dce6e9a3-b6e3-4fd2-9f2d-35c767a99663" + #endif } } } diff --git a/ClashX/General/Managers/ClashResourceManager.swift b/ClashX/General/Managers/ClashResourceManager.swift index 6cb9c298d..77acca29b 100644 --- a/ClashX/General/Managers/ClashResourceManager.swift +++ b/ClashX/General/Managers/ClashResourceManager.swift @@ -1,4 +1,3 @@ - import Alamofire import AppKit import Foundation @@ -32,13 +31,14 @@ class ClashResourceManager { if fileManage.fileExists(atPath: destMMDBPath) { let vaild = verifyGEOIPDataBase().toBool() let versionChange = AppVersionUtil.hasVersionChanged || AppVersionUtil.isFirstLaunch - let customMMDBSet = !Settings.mmdbDownloadUrl.isEmpty - if !vaild || (versionChange && customMMDBSet) { + if !vaild || versionChange { + Logger.log("removing new mmdb file") try? fileManage.removeItem(atPath: destMMDBPath) } } if !fileManage.fileExists(atPath: destMMDBPath) { + Logger.log("installing new mmdb file") if let mmdbUrl = Bundle.main.url(forResource: "Country.mmdb", withExtension: "gz") { do { let data = try Data(contentsOf: mmdbUrl).gunzipped() @@ -61,15 +61,9 @@ class ClashResourceManager { } extension ClashResourceManager { - static func addUpdateMMDBMenuItem(_ menu: inout NSMenu) { - let item = NSMenuItem(title: NSLocalizedString("Update GEOIP Database", comment: ""), action: #selector(updateGeoIP), keyEquivalent: "") - item.target = self - menu.addItem(item) - } - - @objc private static func updateGeoIP() { + static func updateGeoIP() { guard let url = showCustomAlert() else { return } - AF.download(url, to: { (_, _) in + AF.download(url, to: { _, _ in let path = kConfigFolderPath.appending("/Country.mmdb") return (URL(fileURLWithPath: path), .removePreviousFile) }).response { res in @@ -92,13 +86,17 @@ extension ClashResourceManager { alert.runModal() } } - + private static func showCustomAlert() -> String? { let alert = NSAlert() alert.messageText = NSLocalizedString("Custom your GEOIP MMDB download address.", comment: "") let inputView = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24)) - inputView.placeholderString = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb" - inputView.stringValue = Settings.mmdbDownloadUrl + inputView.placeholderString = Settings.defaultMmdbDownloadUrl + if Settings.mmdbDownloadUrl.isEmpty { + inputView.stringValue = Settings.defaultMmdbDownloadUrl + } else { + inputView.stringValue = Settings.mmdbDownloadUrl + } alert.accessoryView = inputView alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) diff --git a/ClashX/General/Managers/ConfigFileManager.swift b/ClashX/General/Managers/ConfigFileManager.swift index 24db02075..a18c63012 100644 --- a/ClashX/General/Managers/ConfigFileManager.swift +++ b/ClashX/General/Managers/ConfigFileManager.swift @@ -1,5 +1,5 @@ // -// ConfigFileFactory.swift +// ConfigFileManager.swift // ClashX // // Created by CYC on 2018/8/5. diff --git a/ClashX/General/Managers/ConfigManager.swift b/ClashX/General/Managers/ConfigManager.swift index 2b30ec958..c6d09e854 100644 --- a/ClashX/General/Managers/ConfigManager.swift +++ b/ClashX/General/Managers/ConfigManager.swift @@ -15,6 +15,7 @@ class ConfigManager { static let shared = ConfigManager() private let disposeBag = DisposeBag() var apiPort = "8080" + var allowExternalControl = false var apiSecret: String = "" var overrideApiURL: URL? var overrideSecret: String? @@ -55,8 +56,8 @@ class ConfigManager { } static func watchCurrentConfigFile() { - if iCloudManager.shared.isICloudEnable() { - iCloudManager.shared.getUrl { url in + if ICloudManager.shared.useiCloud.value { + ICloudManager.shared.getUrl { url in guard let url = url else { return } let configUrl = url.appendingPathComponent(Paths.configFileName(for: selectConfigName)) ConfigFileManager.shared.watchFile(path: configUrl.path) @@ -77,9 +78,10 @@ class ConfigManager { } } - let proxyPortAutoSetObservable = UserDefaults.standard.rx.observe(Bool.self, "proxyPortAutoSet").map({ $0 ?? false }) + let proxyPortAutoSetObservable = UserDefaults.standard.rx.observe(Bool.self, "proxyPortAutoSet").map { $0 ?? false } var isProxySetByOtherVariable = BehaviorRelay(value: false) + var proxyShouldPaused = BehaviorRelay(value: false) var showNetSpeedIndicator: Bool { get { @@ -92,12 +94,6 @@ class ConfigManager { let showNetSpeedIndicatorObservable = UserDefaults.standard.rx.observe(Bool.self, "showNetSpeedIndicator") - var benchMarkUrl: String = UserDefaults.standard.string(forKey: "benchMarkUrl") ?? "http://cp.cloudflare.com/generate_204" { - didSet { - UserDefaults.standard.set(benchMarkUrl, forKey: "benchMarkUrl") - } - } - static var apiUrl: String { if let override = shared.overrideApiURL { return override.absoluteString @@ -150,15 +146,18 @@ class ConfigManager { } } - static var builtInApiMode = (UserDefaults.standard.object(forKey: "kBuiltInApiMode") as? Bool) ?? true { - didSet { - UserDefaults.standard.set(builtInApiMode, forKey: "kBuiltInApiMode") - } - } - - var disableShowCurrentProxyInMenu: Bool = UserDefaults.standard.object(forKey: "kSDisableShowCurrentProxyInMenu") as? Bool ?? !AppDelegate.isAboveMacOS14 { - didSet { - UserDefaults.standard.set(disableShowCurrentProxyInMenu, forKey: "kSDisableShowCurrentProxyInMenu") + static func getConfigPath(configName: String, complete: ((String) -> Void)? = nil) { + if ICloudManager.shared.useiCloud.value { + ICloudManager.shared.getUrl { url in + guard let url = url else { + return + } + let configPath = url.appendingPathComponent(Paths.configFileName(for: configName)).path + complete?(configPath) + } + } else { + let filePath = Paths.localConfigPath(for: configName) + complete?(filePath) } } } diff --git a/ClashX/General/Managers/ConnectionManager.swift b/ClashX/General/Managers/ConnectionManager.swift index ce18d1047..9eeb91e51 100644 --- a/ClashX/General/Managers/ConnectionManager.swift +++ b/ClashX/General/Managers/ConnectionManager.swift @@ -8,30 +8,11 @@ import Cocoa -class ConnectionManager { - static var enableAutoClose = UserDefaults.standard.object(forKey: "ConnectionManager.enableAutoClose") as? Bool ?? true { - didSet { - UserDefaults.standard.set(enableAutoClose, forKey: "ConnectionManager.enableAutoClose") - } - } - - private static var closeMenuItem: NSMenuItem? - - static func addCloseOptionMenuItem(_ menu: inout NSMenu) { - let item = NSMenuItem(title: NSLocalizedString("Auto Close Connection", comment: ""), action: #selector(optionMenuItemTap(sender:)), keyEquivalent: "") - item.target = ConnectionManager.self - menu.addItem(item) - closeMenuItem = item - updateMenuItemStatus(item) - } - +enum ConnectionManager { static func closeConnection(for group: String) { - guard enableAutoClose else { return } ApiRequest.getConnections { conns in - for conn in conns { - if conn.chains.contains(group) { - ApiRequest.closeConnection(conn) - } + for conn in conns where conn.chains.contains(group) { + ApiRequest.closeConnection(conn.id) } } } @@ -40,14 +21,3 @@ class ConnectionManager { ApiRequest.closeAllConnection() } } - -extension ConnectionManager { - static func updateMenuItemStatus(_ item: NSMenuItem? = closeMenuItem) { - item?.state = enableAutoClose ? .on : .off - } - - @objc static func optionMenuItemTap(sender: NSMenuItem) { - enableAutoClose = !enableAutoClose - updateMenuItemStatus(sender) - } -} diff --git a/ClashX/General/Managers/iCloudManager.swift b/ClashX/General/Managers/ICloudManager.swift similarity index 51% rename from ClashX/General/Managers/iCloudManager.swift rename to ClashX/General/Managers/ICloudManager.swift index e36a2d769..16636a4d2 100644 --- a/ClashX/General/Managers/iCloudManager.swift +++ b/ClashX/General/Managers/ICloudManager.swift @@ -1,5 +1,5 @@ // -// iCloudManager.swift +// ICloudManager.swift // ClashX // // Created by yicheng on 2020/5/10. @@ -7,36 +7,44 @@ // import Cocoa +import RxCocoa +import RxSwift -class iCloudManager { - static let shared = iCloudManager() +class ICloudManager { + static let shared = ICloudManager() private let queue = DispatchQueue(label: "com.clashx.icloud") private var metaQuery: NSMetadataQuery? private var enableMenuItem: NSMenuItem? - private var icloudAvailable = false { - didSet { updateMenuItemStatus() } + private(set) var icloudAvailable = false { + didSet { useiCloud.accept(userEnableiCloud && icloudAvailable) } } - private var userEnableiCloud: Bool = UserDefaults.standard.bool(forKey: "kUserEnableiCloud") { - didSet { UserDefaults.standard.set(userEnableiCloud, forKey: "kUserEnableiCloud") } + private var disposeBag = DisposeBag() + + let useiCloud = BehaviorRelay(value: false) + + var userEnableiCloud: Bool = UserDefaults.standard.bool(forKey: "kUserEnableiCloud") { + didSet { + UserDefaults.standard.set(userEnableiCloud, forKey: "kUserEnableiCloud") + useiCloud.accept(userEnableiCloud && icloudAvailable) + } } func setup() { addNotification() - icloudAvailable = isICloudAvailable() - if isICloudEnable() { - checkiCloud() - } - } + useiCloud.distinctUntilChanged().filter { $0 }.subscribe { + [weak self] _ in + self?.checkiCloud() + }.disposed(by: disposeBag) - func isICloudEnable() -> Bool { - return icloudAvailable && userEnableiCloud + icloudAvailable = isICloudAvailable() + useiCloud.accept(userEnableiCloud && icloudAvailable) } func getConfigFilesList(configs: @escaping (([String]) -> Void)) { getUrl { url in guard let url = url, - let fileURLs = try? FileManager.default.contentsOfDirectory(atPath: url.path) else { + let fileURLs = try? FileManager.default.contentsOfDirectory(atPath: url.path) else { configs([]) return } @@ -48,19 +56,16 @@ class iCloudManager { } private func checkiCloud() { - if isICloudAvailable() { - icloudAvailable = true - getUrl { url in - guard let url = url else { - self.icloudAvailable = false - return - } - let files = try? FileManager.default.contentsOfDirectory(atPath: url.path) - if let count = files?.count, count == 0 { - let path = Bundle.main.path(forResource: "sampleConfig", ofType: "yaml")! - try? FileManager.default.copyItem(atPath: path, toPath: kDefaultConfigFilePath) - try? FileManager.default.copyItem(atPath: Bundle.main.path(forResource: "sampleConfig", ofType: "yaml")!, toPath: url.appendingPathComponent("config.yaml").path) - } + getUrl { url in + guard let url = url else { + self.icloudAvailable = false + return + } + let files = try? FileManager.default.contentsOfDirectory(atPath: url.path) + if files?.isEmpty == true { + let path = Bundle.main.path(forResource: "sampleConfig", ofType: "yaml")! + try? FileManager.default.copyItem(atPath: path, toPath: kDefaultConfigFilePath) + try? FileManager.default.copyItem(atPath: Bundle.main.path(forResource: "sampleConfig", ofType: "yaml")!, toPath: url.appendingPathComponent("config.yaml").path) } } } @@ -86,7 +91,7 @@ class iCloudManager { complete?(url) } } catch let err { - print(err) + Logger.log("\(err)") DispatchQueue.main.async { complete?(nil) } @@ -103,23 +108,3 @@ class iCloudManager { icloudAvailable = isICloudAvailable() } } - -extension iCloudManager { - func addEnableMenuItem(_ menu: inout NSMenu) { - let item = NSMenuItem(title: NSLocalizedString("Use iCloud", comment: ""), action: #selector(enableMenuItemTap(sender:)), keyEquivalent: "") - menu.addItem(item) - enableMenuItem = item - updateMenuItemStatus() - } - - @objc func enableMenuItemTap(sender: NSMenuItem) { - userEnableiCloud = !userEnableiCloud - updateMenuItemStatus() - checkiCloud() - } - - func updateMenuItemStatus() { - enableMenuItem?.state = isICloudEnable() ? .on : .off - enableMenuItem?.target = icloudAvailable ? self : nil - } -} diff --git a/ClashX/General/Managers/MenuItemFactory.swift b/ClashX/General/Managers/MenuItemFactory.swift index feb188a7e..211211e0a 100644 --- a/ClashX/General/Managers/MenuItemFactory.swift +++ b/ClashX/General/Managers/MenuItemFactory.swift @@ -13,11 +13,7 @@ import SwiftyJSON class MenuItemFactory { private static var cachedProxyData: ClashProxyResp? - static var useViewToRenderProxy: Bool = UserDefaults.standard.object(forKey: "useViewToRenderProxy") as? Bool ?? AppDelegate.isAboveMacOS152 { - didSet { - UserDefaults.standard.set(useViewToRenderProxy, forKey: "useViewToRenderProxy") - } - } + static let useViewToRenderProxy: Bool = AppDelegate.isAboveMacOS152 // MARK: - Public @@ -77,14 +73,14 @@ class MenuItemFactory { item.state = ConfigManager.selectConfigName == config ? .on : .off return item } - + if RemoteControlManager.selectConfig != nil { complete([]) return } - if iCloudManager.shared.isICloudEnable() { - iCloudManager.shared.getConfigFilesList { + if ICloudManager.shared.useiCloud.value { + ICloudManager.shared.getConfigFilesList { complete($0.map { generateMenuItem($0) }) } } else { @@ -100,8 +96,8 @@ class MenuItemFactory { let app = AppDelegate.shared let startIndex = app.statusMenu.items.firstIndex(of: app.separatorLineTop)! + 1 let endIndex = app.statusMenu.items.firstIndex(of: app.sepatatorLineEndProxySelect)! - app.sepatatorLineEndProxySelect.isHidden = menus.count == 0 - for _ in 0.. NSMenuItem? - { + leftPadding: Bool) -> NSMenuItem? { let proxyMap = proxyInfo.proxiesMap let isGlobalMode = ConfigManager.shared.currentConfig?.mode == .global @@ -124,7 +119,7 @@ class MenuItemFactory { let menu = NSMenuItem(title: proxyGroup.name, action: nil, keyEquivalent: "") let selectedName = proxyGroup.now ?? "" - if !ConfigManager.shared.disableShowCurrentProxyInMenu { + if !Settings.disableShowCurrentProxyInMenu { menu.view = ProxyGroupMenuItemView(group: proxyGroup.name, targetProxy: selectedName, hasLeftPadding: leftPadding) } let submenu = ProxyGroupMenu(title: proxyGroup.name) @@ -150,12 +145,11 @@ class MenuItemFactory { private static func generateUrlTestFallBackMenuItem(proxyGroup: ClashProxy, proxyInfo: ClashProxyResp, - leftPadding: Bool) -> NSMenuItem? - { + leftPadding: Bool) -> NSMenuItem? { let proxyMap = proxyInfo.proxiesMap let selectedName = proxyGroup.now ?? "" let menu = NSMenuItem(title: proxyGroup.name, action: nil, keyEquivalent: "") - if !ConfigManager.shared.disableShowCurrentProxyInMenu { + if !Settings.disableShowCurrentProxyInMenu { menu.view = ProxyGroupMenuItemView(group: proxyGroup.name, targetProxy: selectedName, hasLeftPadding: leftPadding) } let submenu = NSMenu(title: proxyGroup.name) @@ -178,7 +172,7 @@ class MenuItemFactory { } private static func addSpeedTestMenuItem(_ menu: NSMenu, proxyGroup: ClashProxy) { - guard proxyGroup.speedtestAble.count > 0 else { return } + guard !proxyGroup.speedtestAble.isEmpty else { return } let speedTestItem = ProxyGroupSpeedTestMenuItem(group: proxyGroup) let separator = NSMenuItem.separator() menu.insertItem(separator, at: 0) @@ -190,7 +184,7 @@ class MenuItemFactory { let proxyMap = proxyInfo.proxiesMap let menu = NSMenuItem(title: proxyGroup.name, action: nil, keyEquivalent: "") - if !ConfigManager.shared.disableShowCurrentProxyInMenu { + if !Settings.disableShowCurrentProxyInMenu { menu.view = ProxyGroupMenuItemView(group: proxyGroup.name, targetProxy: NSLocalizedString("Load Balance", comment: ""), hasLeftPadding: leftPadding, observeUpdate: false) } let submenu = ProxyGroupMenu(title: proxyGroup.name) @@ -233,27 +227,6 @@ class MenuItemFactory { } } -// MARK: - Experimental - -extension MenuItemFactory { - static func addExperimentalMenuItem(_ menu: inout NSMenu) { - let useViewRender = NSMenuItem(title: NSLocalizedString("Enhance proxy list render", comment: ""), action: #selector(optionUseViewRenderMenuItemTap(sender:)), keyEquivalent: "") - useViewRender.target = self - menu.addItem(useViewRender) - updateUseViewRenderMenuItem(useViewRender) - } - - static func updateUseViewRenderMenuItem(_ item: NSMenuItem) { - item.state = useViewToRenderProxy ? .on : .off - } - - @objc static func optionUseViewRenderMenuItemTap(sender: NSMenuItem) { - useViewToRenderProxy = !useViewToRenderProxy - updateUseViewRenderMenuItem(sender) - recreateProxyMenuItems() - } -} - // MARK: - Action extension MenuItemFactory { diff --git a/ClashX/General/Managers/PrivilegedHelperManager+Legacy.swift b/ClashX/General/Managers/PrivilegedHelperManager+Legacy.swift index 0b44861a7..b0aac92dc 100644 --- a/ClashX/General/Managers/PrivilegedHelperManager+Legacy.swift +++ b/ClashX/General/Managers/PrivilegedHelperManager+Legacy.swift @@ -14,7 +14,7 @@ extension PrivilegedHelperManager { let bash = """ #!/bin/bash set -e - + plistPath=/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist rm -rf /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) if [ -e ${plistPath} ]; then @@ -22,10 +22,10 @@ extension PrivilegedHelperManager { rm ${plistPath} fi launchctl remove \(PrivilegedHelperManager.machServiceName) || true - + mkdir -p /Library/PrivilegedHelperTools/ rm -f /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) - + cp "\(appPath)/Contents/Library/LaunchServices/\(PrivilegedHelperManager.machServiceName)" "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)" echo ' @@ -49,32 +49,51 @@ extension PrivilegedHelperManager { ' > ${plistPath} - + launchctl load -w ${plistPath} """ return bash } - func legacyInstallHelper() { - defer { - resetConnection() - Thread.sleep(forTimeInterval: 1) - } - let script = getInstallScript() + func runScriptWithRootPermission(script: String) { let tmpPath = FileManager.default.temporaryDirectory.appendingPathComponent(NSUUID().uuidString).appendingPathExtension("sh") do { try script.write(to: tmpPath, atomically: true, encoding: .utf8) let appleScriptStr = "do shell script \"bash \(tmpPath.path) \" with administrator privileges" let appleScript = NSAppleScript(source: appleScriptStr) var dict: NSDictionary? - if let _ = appleScript?.executeAndReturnError(&dict) { - return + if appleScript?.executeAndReturnError(&dict) == nil { + Logger.log("apple script failed") } else { - Logger.log("apple script fail: \(String(describing: dict))") + Logger.log("apple script result: \(String(describing: dict))") } } catch let err { Logger.log("legacyInstallHelper create script fail: \(err)") } try? FileManager.default.removeItem(at: tmpPath) } + + func legacyInstallHelper() { + defer { + resetConnection() + Thread.sleep(forTimeInterval: 1) + } + let script = getInstallScript() + runScriptWithRootPermission(script: script) + } + + func removeInstallHelper() { + defer { + resetConnection() + Thread.sleep(forTimeInterval: 5) + } + let script = """ + /bin/launchctl remove \(PrivilegedHelperManager.machServiceName) || true + /usr/bin/killall -u root -9 \(PrivilegedHelperManager.machServiceName) + /bin/rm -rf /Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist + /bin/rm -rf /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) + """ + + runScriptWithRootPermission(script: script) + } } diff --git a/ClashX/General/Managers/PrivilegedHelperManager.swift b/ClashX/General/Managers/PrivilegedHelperManager.swift index 03bc72b14..9390d5c03 100644 --- a/ClashX/General/Managers/PrivilegedHelperManager.swift +++ b/ClashX/General/Managers/PrivilegedHelperManager.swift @@ -7,9 +7,9 @@ // import AppKit -import ServiceManagement -import RxSwift import RxCocoa +import RxSwift +import ServiceManagement class PrivilegedHelperManager { let isHelperCheckFinished = BehaviorRelay(value: false) @@ -19,8 +19,11 @@ class PrivilegedHelperManager { private var authRef: AuthorizationRef? private var connection: NSXPCConnection? private var _helper: ProxyConfigRemoteProcessProtocol? - static let machServiceName = "com.west2online.ClashX.ProxyConfigHelper" - + #if PRO_VERSION + static let machServiceName = "com.west2online.ClashXPro.ProxyConfigHelper" + #else + static let machServiceName = "com.west2online.ClashX.ProxyConfigHelper" + #endif static let shared = PrivilegedHelperManager() init() { initAuthorizationRef() @@ -30,10 +33,30 @@ class PrivilegedHelperManager { func checkInstall() { Logger.log("checkInstall", level: .debug) - - getHelperStatus { [weak self] installed in - guard let self = self else {return} - if !installed { + getHelperStatus { [weak self] status in + Logger.log("check result: \(status)", level: .debug) + guard let self = self else { return } + switch status { + case .noFound: + if #available(macOS 13, *) { + let url = URL(string: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist")! + let status = SMAppService.statusForLegacyPlist(at: url) + if status == .requiresApproval { + let alert = NSAlert() + let notice = NSLocalizedString("ClashX use a daemon helper to setup your system proxy. Please enable ClashX in the Login Items under the Allow in the Background section and relaunch the app", comment: "") + let addition = NSLocalizedString("If you can not find ClashX in the settings, you can try reset daemon", comment: "") + alert.messageText = notice + "\n" + addition + alert.addButton(withTitle: NSLocalizedString("Open System Login Item Setting", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Reset Daemon", comment: "")) + if alert.runModal() == .alertFirstButtonReturn { + SMAppService.openSystemSettingsLoginItems() + } else { + self.removeInstallHelper() + } + } + } + fallthrough + case .needUpdate: Logger.log("need to install helper", level: .debug) if Thread.isMainThread { self.notifyInstall() @@ -42,7 +65,7 @@ class PrivilegedHelperManager { self.notifyInstall() } } - } else { + case .installed: self.isHelperCheckFinished.accept(true) } } @@ -101,7 +124,6 @@ class PrivilegedHelperManager { // Launch the privileged helper using SMJobBless tool var error: Unmanaged? - if SMJobBless(kSMDomainSystemLaunchd, PrivilegedHelperManager.machServiceName as CFString, authRef, &error) == false { let blessError = error!.takeRetainedValue() as Error Logger.log("Bless Error: \(blessError)", level: .error) @@ -112,76 +134,66 @@ class PrivilegedHelperManager { return .success } - private func helperConnection() -> NSXPCConnection? { - // Check that the connection is valid before trying to do an inter process call to helper - if connection == nil { - connection = NSXPCConnection(machServiceName: PrivilegedHelperManager.machServiceName, options: NSXPCConnection.Options.privileged) - connection?.remoteObjectInterface = NSXPCInterface(with: ProxyConfigRemoteProcessProtocol.self) - connection?.invalidationHandler = { - [weak self] in - guard let self = self else { return } - self.connection?.invalidationHandler = nil - OperationQueue.main.addOperation { - self.connection = nil - self._helper = nil - Logger.log("XPC Connection Invalidated") - } - } - connection?.resume() - } - return connection - } - func helper(failture: (() -> Void)? = nil) -> ProxyConfigRemoteProcessProtocol? { - if _helper == nil { - guard let newHelper = helperConnection()?.remoteObjectProxyWithErrorHandler({ error in - Logger.log("Helper connection was closed with error: \(error)") - failture?() - }) as? ProxyConfigRemoteProcessProtocol else { return nil } - _helper = newHelper + connection = NSXPCConnection(machServiceName: PrivilegedHelperManager.machServiceName, options: NSXPCConnection.Options.privileged) + connection?.remoteObjectInterface = NSXPCInterface(with: ProxyConfigRemoteProcessProtocol.self) + connection?.invalidationHandler = { + Logger.log("XPC Connection Invalidated") } - return _helper + connection?.resume() + guard let helper = connection?.remoteObjectProxyWithErrorHandler({ error in + Logger.log("Helper connection was closed with error: \(error)") + failture?() + }) as? ProxyConfigRemoteProcessProtocol else { return nil } + return helper } + var timer: Timer? - private func getHelperStatus(callback:@escaping ((Bool)->Void)) { - + + enum HelperStatus { + case installed + case noFound + case needUpdate + } + + private func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) { var called = false - let reply:((Bool)->Void) = { - installed in - if called {return} + let reply: ((HelperStatus) -> Void) = { + status in + if called { return } called = true - callback(installed) + callback(status) } - + let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName) guard let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any], let helperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String else { Logger.log("check helper status fail") - reply(false) + reply(.noFound) return } let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)") if !helperFileExists { - reply(false) + reply(.noFound) return } let timeout: TimeInterval = helperFileExists ? 15 : 5 let time = Date() - + timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in Logger.log("check helper timeout time: \(timeout)") - reply(false) + reply(.noFound) } - + helper()?.getVersion { [weak timer] installedHelperVersion in timer?.invalidate() timer = nil Logger.log("helper version \(installedHelperVersion ?? "") require version \(helperVersion)", level: .debug) - let installed = installedHelperVersion == helperVersion + let versionMatch = installedHelperVersion == helperVersion let interval = Date().timeIntervalSince(time) Logger.log("check helper using time: \(interval)") - reply(installed) + reply(versionMatch ? .installed : .needUpdate) } } } @@ -240,7 +252,7 @@ extension PrivilegedHelperManager { } } -fileprivate struct AppAuthorizationRights { +private enum AppAuthorizationRights { static let rightName: NSString = "\(PrivilegedHelperManager.machServiceName).config" as NSString static let rightDefaultRule: Dictionary = adminRightsRule static let rightDescription: CFString = "ProxyConfigHelper wants to configure your proxy setting'" as CFString @@ -250,7 +262,7 @@ fileprivate struct AppAuthorizationRights { "version": 1] } -fileprivate enum DaemonInstallResult { +private enum DaemonInstallResult { case success case authorizationFail case getAdminFail @@ -270,7 +282,6 @@ fileprivate enum DaemonInstallResult { case kSMErrorToolNotValid: return "blessError: kSMErrorToolNotValid" case kSMErrorJobNotFound: return "blessError: kSMErrorJobNotFound" case kSMErrorServiceUnavailable: return "blessError: kSMErrorServiceUnavailable" - case kSMErrorJobNotFound: return "blessError: kSMErrorJobNotFound" case kSMErrorJobMustBeEnabled: return "ClashX Helper is disabled by other process. Please run \"sudo launchctl enable system/\(PrivilegedHelperManager.machServiceName)\" in your terminal. The command has been copied to your pasteboard" case kSMErrorInvalidPlist: return "blessError: kSMErrorInvalidPlist" default: diff --git a/ClashX/General/Managers/RemoteConfigManager.swift b/ClashX/General/Managers/RemoteConfigManager.swift index a1554da65..fda5bb3fc 100755 --- a/ClashX/General/Managers/RemoteConfigManager.swift +++ b/ClashX/General/Managers/RemoteConfigManager.swift @@ -38,7 +38,7 @@ class RemoteConfigManager { func migrateOldRemoteConfig() { if let url = UserDefaults.standard.string(forKey: "kRemoteConfigUrl"), - let name = URL(string: url)?.host { + let name = URL(string: url)?.host { configs.append(RemoteConfigModel(url: url, name: name)) UserDefaults.standard.removeObject(forKey: "kRemoteConfigUrl") saveConfigs() @@ -146,10 +146,10 @@ class RemoteConfigManager { urlRequest.cachePolicy = .reloadIgnoringCacheData AF.request(urlRequest) - .validate() - .responseString(encoding: .utf8) { res in - complete(try? res.result.get(), res.response?.suggestedFilename) - } + .validate() + .responseString(encoding: .utf8) { res in + complete(try? res.result.get(), res.response?.suggestedFilename) + } } static func updateConfig(config: RemoteConfigModel, complete: ((String?) -> Void)? = nil) { @@ -173,9 +173,10 @@ class RemoteConfigManager { } config.isPlaceHolderName = false - if iCloudManager.shared.isICloudEnable() { + if ICloudManager.shared.useiCloud.value { ConfigFileManager.shared.stopWatchConfigFile() - } else if config.name == ConfigManager.selectConfigName { + } + if config.name == ConfigManager.selectConfigName { ConfigFileManager.shared.pauseForNextChange() } @@ -192,8 +193,8 @@ class RemoteConfigManager { } } - if iCloudManager.shared.isICloudEnable() { - iCloudManager.shared.getUrl { url in + if ICloudManager.shared.useiCloud.value { + ICloudManager.shared.getUrl { url in guard let url = url else { return } let saveUrl = url.appendingPathComponent(Paths.configFileName(for: config.name)) saveAction(saveUrl.path) @@ -214,7 +215,7 @@ class RemoteConfigManager { return res } } - + static func showAdd() { let alertView = NSAlert() alertView.addButton(withTitle: NSLocalizedString("OK", comment: "")) diff --git a/ClashX/General/Managers/RemoteControlManager.swift b/ClashX/General/Managers/RemoteControlManager.swift index af416fc52..b4fa1914a 100644 --- a/ClashX/General/Managers/RemoteControlManager.swift +++ b/ClashX/General/Managers/RemoteControlManager.swift @@ -1,5 +1,5 @@ // -// ClientOnlyManager.swift +// RemoteControlManager.swift // ClashX Pro // // Created by 称一称 on 2020/6/16. @@ -28,7 +28,6 @@ class RemoteControlManager { static var selected: String } - static let shared = RemoteControlManager() static var configs: [RemoteControl] = loadConfig() { didSet { @@ -44,7 +43,7 @@ class RemoteControlManager { Recorder.selected = selectConfig?.uuid ?? "" } } - + private static var menuSeparator: NSMenuItem? static func loadConfig() -> [RemoteControl] { @@ -63,13 +62,13 @@ class RemoteControlManager { menuSeparator = separator updateMenuItems() updateDropDownMenuItems() - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { RemoteControlManager.recoverSelection() } } - - static private func recoverSelection() { - if Recorder.selected != "" { + + private static func recoverSelection() { + if !Recorder.selected.isEmpty { if let config = configs.first(where: { $0.uuid == Recorder.selected }) { selectConfig = config updateRemoteControl() @@ -83,7 +82,7 @@ class RemoteControlManager { static func updateMenuItems() { guard let separator = menuSeparator, let menu = separator.menu else { return } let idx = menu.index(of: separator) - for _ in 0.. Void)? = nil) { @@ -77,7 +73,7 @@ class SystemProxyManager: NSObject { func disableProxy(port: Int, socksPort: Int, forceDisable: Bool = false, complete: (() -> Void)? = nil) { Logger.log("disableProxy", level: .debug) - if disableRestoreProxy || forceDisable { + if Settings.disableRestoreProxy || forceDisable { helper?.disableProxy(withFilterInterface: Settings.filterInterface) { error in if let error = error { Logger.log("disableProxy \(error)", level: .error) @@ -94,22 +90,4 @@ class SystemProxyManager: NSObject { complete?() }) } - - // MARK: - Expriment Menu Items - - func addDisableRestoreProxyMenuItem(_ menu: inout NSMenu) { - let item = NSMenuItem(title: NSLocalizedString("Disable Restore Proxy Setting", comment: ""), action: #selector(optionMenuItemTap(sender:)), keyEquivalent: "") - item.target = self - menu.addItem(item) - updateMenuItemStatus(item) - } - - func updateMenuItemStatus(_ item: NSMenuItem) { - item.state = disableRestoreProxy ? .on : .off - } - - @objc func optionMenuItemTap(sender: NSMenuItem) { - disableRestoreProxy = !disableRestoreProxy - updateMenuItemStatus(sender) - } } diff --git a/ClashX/General/Utils/AppVersionUtil.swift b/ClashX/General/Utils/AppVersionUtil.swift index 9ef4ff4fa..dcb9600e3 100644 --- a/ClashX/General/Utils/AppVersionUtil.swift +++ b/ClashX/General/Utils/AppVersionUtil.swift @@ -44,6 +44,7 @@ class AppVersionUtil: NSObject { extension AppVersionUtil { static func showUpgradeAlert() { if let lastVersion = shared.lastVersionNumber, hasVersionChanged { + WebCacheCleaner.clean() guard lastVersion.compare("1.30.0", options: .numeric) == .orderedAscending else { return } let alert = NSAlert() alert.messageText = NSLocalizedString("This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly.", comment: "") diff --git a/ClashX/General/Utils/ClashStatusTool.swift b/ClashX/General/Utils/ClashStatusTool.swift index 88057f09f..89ad01de0 100644 --- a/ClashX/General/Utils/ClashStatusTool.swift +++ b/ClashX/General/Utils/ClashStatusTool.swift @@ -26,7 +26,6 @@ class ClashStatusTool { } NSApp.terminate(nil) } - } } } diff --git a/ClashX/General/Utils/Command.swift b/ClashX/General/Utils/Command.swift new file mode 100644 index 000000000..528bb5f8d --- /dev/null +++ b/ClashX/General/Utils/Command.swift @@ -0,0 +1,34 @@ +// +// Command.swift +// ClashX +// +// Created by yicheng on 2023/10/13. +// Copyright © 2023 west2online. All rights reserved. +// + +import Foundation + +struct Command { + let cmd: String + let args: [String] + + func run() -> String { + var output = "" + + let task = Process() + task.launchPath = cmd + task.arguments = args + + let outpipe = Pipe() + task.standardOutput = outpipe + + task.launch() + + task.waitUntilExit() + let outdata = outpipe.fileHandleForReading.readDataToEndOfFile() + if var string = String(data: outdata, encoding: .utf8) { + output = string.trimmingCharacters(in: .newlines) + } + return output + } +} diff --git a/ClashX/General/Utils/JSBridge.swift b/ClashX/General/Utils/JSBridge.swift new file mode 100644 index 000000000..6daf29662 --- /dev/null +++ b/ClashX/General/Utils/JSBridge.swift @@ -0,0 +1,81 @@ +// +// JSBridge.swift +// ClashX +// +// Created by yicheng on 2023/9/26. +// Copyright © 2023 west2online. All rights reserved. +// + +import Foundation +import WebKit + +class JSBridge: NSObject { + typealias ResponseCallback = (Any?) -> Void + typealias BridgeHandler = (Any?, @escaping ResponseCallback) -> Void + + private weak var webView: WKWebView? + private var handlers = [String: BridgeHandler]() + init(_ webView: WKWebView) { + self.webView = webView + super.init() + setup() + } + + deinit { + webView?.configuration.userContentController.removeAllUserScripts() + webView?.configuration.userContentController.removeScriptMessageHandler(forName: "jsBridge") + } + + private func setup() { + addScriptMessageHandler() + } + + private func addScriptMessageHandler() { + let scriptMessageHandler = ClashScriptMessageHandler(delegate: self) + webView?.configuration.userContentController.add(scriptMessageHandler, name: "jsBridge") + } + + private func sendBackMessage(data: Any?, eventID: String) { + let data = ["id": eventID, "data": data, "type": "jsBridge"] as [String: Any?] + do { + let jsonData = try JSONSerialization.data(withJSONObject: data, options: []) + let jsonString = String(data: jsonData, encoding: .utf8)! + let str = "window.postMessage(\(jsonString), window.origin);" + webView?.evaluateJavaScript(str) + } catch let err { + Logger.log(err.localizedDescription, level: .warning) + } + } + + func registerHandler(_ name: String, handler: @escaping BridgeHandler) { + handlers[name] = handler + } +} + +extension JSBridge: WKScriptMessageHandler { + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if + let body = message.body as? [String: Any], + let handlerName = body["name"] as? String, + let handler = handlers[handlerName], + let eventID = body["id"] as? String { + let data = body["data"] + handler(data) { [weak self] res in + self?.sendBackMessage(data: res, eventID: eventID) + } + } + } +} + +private class ClashScriptMessageHandler: NSObject, WKScriptMessageHandler { + weak var delegate: WKScriptMessageHandler? + + public init(delegate: WKScriptMessageHandler) { + self.delegate = delegate + super.init() + } + + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + delegate?.userContentController(userContentController, didReceive: message) + } +} diff --git a/ClashX/General/Utils/JSBridgeHandler.swift b/ClashX/General/Utils/JSBridgeHandler.swift index ac0d998ab..0877fa154 100644 --- a/ClashX/General/Utils/JSBridgeHandler.swift +++ b/ClashX/General/Utils/JSBridgeHandler.swift @@ -8,16 +8,14 @@ import Alamofire import SwiftyJSON -import WebViewJavascriptBridge +import WebKit class JsBridgeUtil { - static func initJSbridge(webview: Any, delegate: Any) -> WebViewJavascriptBridge { - let bridge = WebViewJavascriptBridge(webview)! + static func initJSbridge(webview: WKWebView, delegate: Any) -> JSBridge { + let bridge = JSBridge(webview) - bridge.setWebViewDelegate(delegate) - - bridge.registerHandler("isSystemProxySet") { anydata, responseCallback in - responseCallback?(ConfigManager.shared.proxyPortAutoSet) + bridge.registerHandler("isSystemProxySet") { _, responseCallback in + responseCallback(ConfigManager.shared.proxyPortAutoSet) } bridge.registerHandler("setSystemProxy") { anydata, responseCallback in @@ -29,36 +27,22 @@ class JsBridgeUtil { } else { SystemProxyManager.shared.disableProxy() } - responseCallback?(true) + responseCallback(true) } else { - responseCallback?(false) + responseCallback(false) } } bridge.registerHandler("getStartAtLogin") { _, responseCallback in - responseCallback?(LaunchAtLogin.shared.isEnabled) + responseCallback(LaunchAtLogin.shared.isEnabled) } bridge.registerHandler("setStartAtLogin") { anydata, responseCallback in if let enable = anydata as? Bool { LaunchAtLogin.shared.isEnabled = enable - responseCallback?(true) - } else { - responseCallback?(false) - } - } - - bridge.registerHandler("getBreakConnections") { _, responseCallback in - responseCallback?(ConnectionManager.enableAutoClose) - } - - bridge.registerHandler("setBreakConnections") { anydata, responseCallback in - if let enable = anydata as? Bool { - ConnectionManager.enableAutoClose = enable - ConnectionManager.updateMenuItemStatus() - responseCallback?(true) + responseCallback(true) } else { - responseCallback?(false) + responseCallback(false) } } @@ -71,10 +55,10 @@ class JsBridgeUtil { } else { resp = delay } - responseCallback?(resp) + responseCallback(resp) } } else { - responseCallback?(nil) + responseCallback(nil) } } @@ -82,22 +66,21 @@ class JsBridgeUtil { var host = "127.0.0.1" var port = ConfigManager.shared.apiPort if let override = ConfigManager.shared.overrideApiURL, - let overridedHost = override.host { + let overridedHost = override.host { host = overridedHost port = "\(override.port ?? 80)" } let data = [ "host": host, "port": port, - "secret": ConfigManager.shared.overrideSecret ?? ConfigManager.shared.apiSecret, + "secret": ConfigManager.shared.overrideSecret ?? ConfigManager.shared.apiSecret ] - callback?(data) + callback(data) } // ping-pong - bridge.registerHandler("ping") { [weak bridge] anydata, responseCallback in - bridge?.callHandler("pong") - responseCallback?(true) + bridge.registerHandler("ping") { _, responseCallback in + responseCallback("pong") } return bridge } diff --git a/ClashX/General/Utils/NetworkChangeNotifier.swift b/ClashX/General/Utils/NetworkChangeNotifier.swift index 76d3dfb69..e51162167 100644 --- a/ClashX/General/Utils/NetworkChangeNotifier.swift +++ b/ClashX/General/Utils/NetworkChangeNotifier.swift @@ -7,6 +7,7 @@ // import Cocoa +import CoreWLAN import SystemConfiguration class NetworkChangeNotifier { @@ -27,11 +28,11 @@ class NetworkChangeNotifier { name: NSWorkspace.didWakeNotification, object: nil ) - let changed: SCDynamicStoreCallBack = { dynamicStore, _, _ in + let changed: SCDynamicStoreCallBack = { _, _, _ in NotificationCenter.default.post(name: .systemNetworkStatusDidChange, object: nil) } var dynamicContext = SCDynamicStoreContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) - let dcAddress = withUnsafeMutablePointer(to: &dynamicContext, { UnsafeMutablePointer($0) }) + let dcAddress = withUnsafeMutablePointer(to: &dynamicContext) { UnsafeMutablePointer($0) } if let dynamicStore = SCDynamicStoreCreate(kCFAllocatorDefault, "com.clashx.proxy.networknotification" as CFString, changed, dcAddress) { let keysArray = ["State:/Network/Global/Proxies" as CFString] as CFArray @@ -43,11 +44,11 @@ class NetworkChangeNotifier { } private static func startIPChangeWatch() { - let changed: SCDynamicStoreCallBack = { dynamicStore, _, _ in + let changed: SCDynamicStoreCallBack = { _, _, _ in NotificationCenter.default.post(name: .systemNetworkStatusIPUpdate, object: nil) } var dynamicContext = SCDynamicStoreContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) - let dcAddress = withUnsafeMutablePointer(to: &dynamicContext, { UnsafeMutablePointer($0) }) + let dcAddress = withUnsafeMutablePointer(to: &dynamicContext) { UnsafeMutablePointer($0) } if let dynamicStore = SCDynamicStoreCreate(kCFAllocatorDefault, "com.clashx.ipv4.networknotification" as CFString, changed, dcAddress) { let keysArray = ["State:/Network/Global/IPv4" as CFString] as CFArray @@ -78,7 +79,7 @@ class NetworkChangeNotifier { return (httpProxy, httpsProxy, socksProxy) } - static func isCurrentSystemSetToClash(looser:Bool = false) -> Bool { + static func isCurrentSystemSetToClash(looser: Bool = false) -> Bool { let (http, https, socks) = NetworkChangeNotifier.currentSystemProxySetting() let currentPort = ConfigManager.shared.currentConfig?.usedHttpPort ?? 0 let currentSocks = ConfigManager.shared.currentConfig?.usedSocksPort ?? 0 @@ -91,15 +92,15 @@ class NetworkChangeNotifier { return http == currentPort && https == currentPort && socks == currentSocks } } - + static func hasInterfaceProxySetToClash() -> Bool { let currentPort = ConfigManager.shared.currentConfig?.usedHttpPort let currentSocks = ConfigManager.shared.currentConfig?.usedSocksPort if let prefRef = SCPreferencesCreate(nil, "ClashX" as CFString, nil), - let sets = SCPreferencesGetValue(prefRef, kSCPrefNetworkServices){ + let sets = SCPreferencesGetValue(prefRef, kSCPrefNetworkServices) { for key in sets.allKeys { let dict = sets[key] as? NSDictionary - let proxySettings = dict?["Proxies"] as? [String:Any] + let proxySettings = dict?["Proxies"] as? [String: Any] if currentPort != nil { if proxySettings?[kCFNetworkProxiesHTTPPort as String] as? Int == currentPort || proxySettings?[kCFNetworkProxiesHTTPSPort as String] as? Int == currentPort { @@ -107,7 +108,7 @@ class NetworkChangeNotifier { } } if currentSocks != nil { - if proxySettings?[kCFNetworkProxiesSOCKSPort as String] as? Int == currentSocks { + if proxySettings?[kCFNetworkProxiesSOCKSPort as String] as? Int == currentSocks { return true } } @@ -127,6 +128,24 @@ class NetworkChangeNotifier { return dict?[kSCDynamicStorePropNetPrimaryInterface as String] } + static func getPrimaryIsDhcp() -> Bool { + let store = SCDynamicStoreCreate(nil, "ClashX" as CFString, nil, nil) + if store == nil { + return false + } + + let key = SCDynamicStoreKeyCreateNetworkGlobalEntity(nil, kSCDynamicStoreDomainState, kSCEntNetIPv4) + let dict = SCDynamicStoreCopyValue(store, key) as? [String: String] + + guard let serviceID = dict?[kSCDynamicStorePropNetPrimaryService as String] else { return false } + let dhcpInfoKey = SCDynamicStoreKeyCreateNetworkServiceEntity(nil, + kSCDynamicStoreDomainState, + serviceID as CFString, + kSCEntNetDHCP) + let dhcpInfo = SCDynamicStoreCopyValue(store, dhcpInfoKey) as? [String: Any] + return dhcpInfo != nil + } + static func getCurrentDns() -> [String] { let store = SCDynamicStoreCreate(nil, "ClashX" as CFString, nil, nil) if store == nil { diff --git a/ClashX/General/Utils/SSIDSuspendTool.swift b/ClashX/General/Utils/SSIDSuspendTool.swift new file mode 100644 index 000000000..7e9ff48e7 --- /dev/null +++ b/ClashX/General/Utils/SSIDSuspendTool.swift @@ -0,0 +1,144 @@ +// +// SSIDSuspendTool.swift +// ClashX Pro +// +// Created by yicheng on 2023/5/24. +// Copyright © 2023 west2online. All rights reserved. +// + +import CoreLocation +import CoreWLAN +import Foundation +import RxCocoa +import RxSwift + +class SSIDSuspendTool: NSObject { + static let shared = SSIDSuspendTool() + private var ssidChangePublisher = PublishSubject() + private var disposeBag = DisposeBag() + private lazy var locationManager = CLLocationManager() + + var showNoticeOnNotPermission = false + + func setup() { + if AppVersionUtil.hasVersionChanged { + showNoticeOnNotPermission = true + } + requestPermissionIfNeed() + do { + try CWWiFiClient.shared().startMonitoringEvent(with: .ssidDidChange) + CWWiFiClient.shared().delegate = self + ssidChangePublisher + .observe(on: MainScheduler.instance) + .debounce(.seconds(1), scheduler: MainScheduler.instance) + .delay(.seconds(1), scheduler: MainScheduler.instance) + .bind { [weak self] _ in + self?.update() + }.disposed(by: disposeBag) + } catch let err { + Logger.log(String(describing: err), level: .warning) + NotificationCenter + .default + .rx + .notification(.systemNetworkStatusDidChange) + .observe(on: MainScheduler.instance) + .delay(.seconds(2), scheduler: MainScheduler.instance) + .bind { [weak self] _ in + self?.update() + }.disposed(by: disposeBag) + } + ConfigManager.shared + .proxyShouldPaused + .asObservable() + .distinctUntilChanged() + .filter { _ in ConfigManager.shared.proxyPortAutoSet } + .bind { pause in + if pause { + SystemProxyManager.shared.disableProxy() + } else { + SystemProxyManager.shared.enableProxy() + } + }.disposed(by: disposeBag) + + update() + } + + func requestPermissionIfNeed() { + defer { + showNoticeOnNotPermission = false + } + if #available(macOS 14, *) { + if Settings.disableSSIDList.isEmpty { return } + if locationManager.authorizationStatus == .notDetermined { + Logger.log("request location permission") + locationManager.desiredAccuracy = kCLLocationAccuracyReduced + locationManager.delegate = self + locationManager.requestAlwaysAuthorization() + } else if locationManager.authorizationStatus != .authorized { + if showNoticeOnNotPermission { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.openLocationSettings() + } + } + } + } + } + + func update() { + if shouldSuspend() { + ConfigManager.shared.proxyShouldPaused.accept(true) + } else { + ConfigManager.shared.proxyShouldPaused.accept(false) + } + } + + func shouldSuspend() -> Bool { + if let currentSSID = getCurrentSSID() { + return Settings.disableSSIDList.contains(currentSSID) + } else { + return false + } + } + + private func getCurrentSSID() -> String? { + if #available(macOS 14, *) { + if locationManager.authorizationStatus != .authorized { + let info = Command(cmd: "/System/Library/PrivateFrameworks/Apple80211.framework/Resources/airport", args: ["-I"]).run() + let ssid = info.components(separatedBy: "\n") + .lazy + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .first { $0.starts(with: "SSID:") }? + .components(separatedBy: ":") + .last?.trimmingCharacters(in: .whitespacesAndNewlines) + return ssid + } + } + return CWWiFiClient.shared().interface()?.ssid() + } + + private func openLocationSettings() { + NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Location")!) + NSApp.activate(ignoringOtherApps: true) + NSAlert.alert(with: NSLocalizedString("Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services.", comment: "")) + } +} + +extension SSIDSuspendTool: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + Logger.log("Location status: \(status.rawValue)") + if status != .authorized, showNoticeOnNotPermission { + openLocationSettings() + } + showNoticeOnNotPermission = false + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {} + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {} +} + +extension SSIDSuspendTool: CWEventDelegate { + func ssidDidChangeForWiFiInterface(withName interfaceName: String) { + ssidChangePublisher.onNext(interfaceName) + } +} diff --git a/ClashX/Images.xcassets/Contents.json b/ClashX/Images.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ClashX/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ClashX/Images.xcassets/icon_connection_done.imageset/Contents.json b/ClashX/Images.xcassets/icon_connection_done.imageset/Contents.json new file mode 100644 index 000000000..7b2aa6410 --- /dev/null +++ b/ClashX/Images.xcassets/icon_connection_done.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "icon_connected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_connected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected.png b/ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected.png new file mode 100644 index 000000000..ea79ade03 Binary files /dev/null and b/ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected.png differ diff --git a/ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected@2x.png b/ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected@2x.png new file mode 100644 index 000000000..51af80c40 Binary files /dev/null and b/ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected@2x.png differ diff --git a/ClashX/Images.xcassets/icon_connection_fail.imageset/Contents.json b/ClashX/Images.xcassets/icon_connection_fail.imageset/Contents.json new file mode 100644 index 000000000..b0a614ff2 --- /dev/null +++ b/ClashX/Images.xcassets/icon_connection_fail.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "icon_connection_fail.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_connection_fail@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail.png b/ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail.png new file mode 100644 index 000000000..0a3663e6f Binary files /dev/null and b/ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail.png differ diff --git a/ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail@2x.png b/ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail@2x.png new file mode 100644 index 000000000..0518b4698 Binary files /dev/null and b/ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail@2x.png differ diff --git a/ClashX/Images.xcassets/icon_connection_inprogress.imageset/Contents.json b/ClashX/Images.xcassets/icon_connection_inprogress.imageset/Contents.json new file mode 100644 index 000000000..373c4b379 --- /dev/null +++ b/ClashX/Images.xcassets/icon_connection_inprogress.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "icon_connecting.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_connecting@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting.png b/ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting.png new file mode 100644 index 000000000..7fe752a5b Binary files /dev/null and b/ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting.png differ diff --git a/ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting@2x.png b/ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting@2x.png new file mode 100644 index 000000000..35fd939bb Binary files /dev/null and b/ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting@2x.png differ diff --git a/ClashX/Info.plist b/ClashX/Info.plist index 5dd6f3771..c8b4d0bcd 100644 --- a/ClashX/Info.plist +++ b/ClashX/Info.plist @@ -2,8 +2,12 @@ + NSLocationAlwaysAndWhenInUseUsageDescription + ClashX use location info to detect your current WiFi network SSID name and provide the auto suspend services. + NSLocationWhenInUseUsageDescription + ClashX use location info to detect your current WiFi network SSID name and provide the auto suspend services. BETA - + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDocumentTypes diff --git a/ClashX/Macro/Notification.swift b/ClashX/Macro/Notification.swift index f0d9a6282..91f7d5e25 100644 --- a/ClashX/Macro/Notification.swift +++ b/ClashX/Macro/Notification.swift @@ -18,5 +18,4 @@ extension Notification.Name { static func proxyUpdate(for name: ClashProxyName) -> Notification.Name { return Notification.Name("kProxyUpdate_\(name)") } - } diff --git a/ClashX/Macro/Paths.swift b/ClashX/Macro/Paths.swift index 5ac6e9498..d51d72d97 100644 --- a/ClashX/Macro/Paths.swift +++ b/ClashX/Macro/Paths.swift @@ -11,7 +11,7 @@ let kConfigFolderPath = "\(NSHomeDirectory())/.config/clash/" let kDefaultConfigFilePath = "\(kConfigFolderPath)config.yaml" -struct Paths { +enum Paths { static func localConfigPath(for name: String) -> String { return "\(kConfigFolderPath)\(configFileName(for: name))" } diff --git a/ClashX/Models/ClashConfig.swift b/ClashX/Models/ClashConfig.swift index 110087212..9aceb9420 100644 --- a/ClashX/Models/ClashConfig.swift +++ b/ClashX/Models/ClashConfig.swift @@ -5,12 +5,16 @@ // Created by CYC on 2018/7/30. // Copyright © 2018年 yichengchen. All rights reserved. // +import CocoaLumberjack import Foundation enum ClashProxyMode: String, Codable { case rule case global case direct + #if PRO_VERSION + case script + #endif } extension ClashProxyMode { @@ -19,17 +23,41 @@ extension ClashProxyMode { case .rule: return NSLocalizedString("Rule", comment: "") case .global: return NSLocalizedString("Global", comment: "") case .direct: return NSLocalizedString("Direct", comment: "") + #if PRO_VERSION + case .script: return NSLocalizedString("Script", comment: "") + #endif } } } enum ClashLogLevel: String, Codable { case info - case warning + #if PRO_VERSION + case warning = "warn" + #else + case warning + #endif case error case debug case silent case unknow = "unknown" + + func toDDLogLevel() -> DDLogLevel { + switch self { + case .info: + return .info + case .warning: + return .warning + case .error: + return .error + case .debug: + return .debug + case .silent: + return .off + case .unknow: + return .error + } + } } class ClashConfig: Codable { diff --git a/ClashX/Models/ClashConnection.swift b/ClashX/Models/ClashConnection.swift index 0cc323d3d..d1e549f06 100644 --- a/ClashX/Models/ClashConnection.swift +++ b/ClashX/Models/ClashConnection.swift @@ -8,13 +8,166 @@ import Cocoa -struct ClashConnectionSnapShot: Codable { +struct ClashConnectionBaseSnapShot: Codable { let connections: [Connection] } -extension ClashConnectionSnapShot { +extension ClashConnectionBaseSnapShot { struct Connection: Codable { let id: String let chains: [String] } } + +@available(macOS 10.15, *) +class ClashConnectionSnapShot: Decodable { + var connections: [Connection] + let downloadTotal: Int + let uploadTotal: Int +} + +@available(macOS 10.15, *) +extension ClashConnectionSnapShot { + class Connection: NSObject, Decodable { + @objc enum ConnStatus: Int { + case connecting + case finished + case fail + + var image: NSImage? { + switch self { + case .connecting: return NSImage(named: "icon_connection_inprogress") + case .finished: return NSImage(named: "icon_connection_done") + case .fail: return NSImage(named: "icon_connection_fail") + } + } + + var title: String { + switch self { + case .connecting: return NSLocalizedString("Connecting", comment: "") + case .finished: return NSLocalizedString("Done", comment: "") + case .fail: return NSLocalizedString("Fail", comment: "") + } + } + } + + let id: String + let chains: [String] + @objc let metadata: MetaData + @objc @Published var upload: Int + @objc @Published var download: Int + @objc let start: Date + @objc let rule: String + let rulePayload: String + + @objc @Published var status = ConnStatus.connecting + @objc @Published var uploadSpeed = 0 { + didSet { + if uploadSpeed > maxUploadSpeed { + maxUploadSpeed = uploadSpeed + } + } + } + + @objc @Published var downloadSpeed = 0 { + didSet { + if downloadSpeed > maxDownloadSpeed { + maxDownloadSpeed = downloadSpeed + } + } + } + + @Published private(set) var maxUploadSpeed = 0 + @Published private(set) var maxDownloadSpeed = 0 + var error: String? + + enum CodingKeys: CodingKey { + case id + case chains + case metadata + case upload + case download + case start + case rule + case rulePayload + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + chains = try container.decode([String].self, forKey: .chains) + metadata = try container.decode(MetaData.self, forKey: .metadata) + upload = try container.decode(Int.self, forKey: .upload) + download = try container.decode(Int.self, forKey: .download) + start = try container.decode(Date.self, forKey: .start) + rule = try container.decode(String.self, forKey: .rule) + rulePayload = try container.decode(String.self, forKey: .rulePayload) + } + + init(id: String, chains: [String], meta: MetaData, upload: Int, download: Int, start: Date, rule: String, rulePayload: String) { + self.id = id + self.chains = chains + metadata = meta + self.upload = upload + self.download = download + self.start = start + self.rule = rule + self.rulePayload = rulePayload + super.init() + } + } + + // {"network":"tcp","type":"HTTP Connect","sourceIP":"127.0.0.1","destinationIP":"124.72.132.104","sourcePort":"59217","destinationPort":"443","host":"slardar-bd.feishu.cn","dnsMode":"normal","processPath":"","specialProxy":""} + class MetaData: NSObject, Codable { + @objc let network: String + @objc let type: String + let sourceIP: String + let destinationIP: String + let sourcePort: String + let destinationPort: String + @objc let host: String + let dnsMode: String + let specialProxy: String? + var processPath: String + + @objc var displayHost: String { + if !host.isEmpty { return host } + return destinationIP + } + + var pid: String? + var processImage: NSImage? + + @objc var processName: String? + + enum CodingKeys: CodingKey { + case network + case type + case sourceIP + case destinationIP + case host + case dnsMode + case specialProxy + case processPath + case sourcePort + case destinationPort + } + + init(network: String, type: String, sourceIP: String, destinationIP: String, sourcePort: String, destinationPort: String, host: String, dnsMode: String, specialProxy: String?, processPath: String, pid: String? = nil, processImage: NSImage? = nil, processName: String? = nil) { + self.network = network + self.type = type + self.sourceIP = sourceIP + self.destinationIP = destinationIP + self.sourcePort = sourcePort + self.destinationPort = destinationPort + self.host = host + self.dnsMode = dnsMode + self.specialProxy = specialProxy + self.processPath = processPath + self.pid = pid + self.processImage = processImage + self.processName = processName + super.init() + } + } +} diff --git a/ClashX/Models/ClashProvider.swift b/ClashX/Models/ClashProvider.swift index 34734b180..7510fe425 100644 --- a/ClashX/Models/ClashProvider.swift +++ b/ClashX/Models/ClashProvider.swift @@ -10,9 +10,7 @@ import Cocoa class ClashProviderResp: Codable { let allProviders: [ClashProxyName: ClashProvider] - lazy var providers: [ClashProxyName: ClashProvider] = { - return allProviders.filter({ $0.value.vehicleType != .Compatible }) - }() + lazy var providers: [ClashProxyName: ClashProvider] = allProviders.filter { $0.value.vehicleType != .Compatible } init() { allProviders = [:] diff --git a/ClashX/Models/ClashProxy.swift b/ClashX/Models/ClashProxy.swift index f821f7165..3b8166884 100644 --- a/ClashX/Models/ClashProxy.swift +++ b/ClashX/Models/ClashProxy.swift @@ -7,6 +7,7 @@ // import Cocoa +import SwiftyJSON enum ClashProxyType: String, Codable { case urltest = "URLTest" @@ -24,6 +25,8 @@ enum ClashProxyType: String, Codable { case trojan = "Trojan" case relay = "Relay" case unknown = "Unknown" + case wireguard = "Wireguard" + case vless = "Vless" static let proxyGroups: [ClashProxyType] = [.select, .urltest, .fallback, .loadBalance] @@ -57,9 +60,10 @@ typealias ClashProviderName = String class ClashProxySpeedHistory: Codable { let time: Date let delay: Int + let meanDelay: Int? - class hisDateFormaterInstance { - static let shared = hisDateFormaterInstance() + class HisDateFormaterInstance { + static let shared = HisDateFormaterInstance() lazy var formater: DateFormatter = { var f = DateFormatter() f.dateFormat = "HH:mm" @@ -68,15 +72,20 @@ class ClashProxySpeedHistory: Codable { } lazy var delayDisplay: String = { - switch delay { - case 0: return NSLocalizedString("fail", comment: "") - default: return "\(delay) ms" + if let meanDelay, meanDelay > 0 { + switch meanDelay { + case 0: return NSLocalizedString("fail", comment: "") + default: return "\(meanDelay) ms" + } + } else { + switch delay { + case 0: return NSLocalizedString("fail", comment: "") + default: return "\(delay) ms" + } } }() - lazy var dateDisplay: String = { - return hisDateFormaterInstance.shared.formater.string(from: time) - }() + lazy var dateDisplay: String = HisDateFormaterInstance.shared.formater.string(from: time) lazy var displayString: String = "\(dateDisplay) \(delayDisplay)" } @@ -87,8 +96,9 @@ class ClashProxy: Codable { let all: [ClashProxyName]? let history: [ClashProxySpeedHistory] let now: ClashProxyName? - weak var enclosingResp: ClashProxyResp? = nil - weak var enclosingProvider: ClashProvider? = nil + let alive: Bool? + weak var enclosingResp: ClashProxyResp? + weak var enclosingProvider: ClashProvider? enum SpeedtestAbleItem { case proxy(name: ClashProxyName) @@ -104,7 +114,7 @@ class ClashProxy: Codable { guard let resp = enclosingResp, let allProxys = all else { return [] } var proxys = [SpeedtestAbleItem]() for proxy in allProxys { - if let p = resp.proxiesMap[proxy], !ClashProxyType.isProxyGroup(p) { + if let p = resp.proxiesMap[proxy] { if let provider = p.enclosingProvider { proxys.append(.provider(name: p.name, provider: provider.name)) } else { @@ -115,18 +125,16 @@ class ClashProxy: Codable { return proxys }() - lazy var isSpeedTestable: Bool = { - return speedtestAble.count > 0 - }() + lazy var isSpeedTestable: Bool = !speedtestAble.isEmpty private enum CodingKeys: String, CodingKey { - case type, all, history, now, name + case type, all, history, now, name, alive } lazy var maxProxyNameLength: CGFloat = { let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: 20) - let lengths = all?.compactMap({ name -> CGFloat in + let lengths = all?.compactMap { name -> CGFloat in if let length = ClashProxy.nameLengthCachedMap[name] { return length } @@ -139,7 +147,7 @@ class ClashProxy: Codable { attributes: attr).width ClashProxy.nameLengthCachedMap[name] = length return length - }) + } return lengths?.max() ?? 0 }() } @@ -151,24 +159,22 @@ class ClashProxyResp { var enclosingProviderResp: ClashProviderResp? - init(_ data: Any?) { - guard - let data = data as? [String: [String: Any]], - let proxies = data["proxies"] + init(_ data: Data?) { + guard let data else { self.proxiesMap = [:] self.proxies = [] return } - + let proxies = JSON(data)["proxies"] var proxiesModel = [ClashProxy]() var proxiesMap = [ClashProxyName: ClashProxy]() let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(DateFormatter.js) - for value in proxies.values { - guard let data = try? JSONSerialization.data(withJSONObject: value, options: .prettyPrinted) else { + for value in proxies.dictionaryValue.values { + guard let data = try? value.rawData() else { continue } guard let proxy = try? decoder.decode(ClashProxy.self, from: data) else { @@ -204,15 +210,11 @@ class ClashProxyResp { return map }() - lazy var proxyGroups: [ClashProxy] = { - return proxies.filter { - ClashProxyType.isProxyGroup($0) - }.sorted(by: { proxiesSortMap[$0.name] ?? -1 < proxiesSortMap[$1.name] ?? -1 }) - }() + lazy var proxyGroups: [ClashProxy] = proxies.filter { + ClashProxyType.isProxyGroup($0) + }.sorted(by: { proxiesSortMap[$0.name] ?? -1 < proxiesSortMap[$1.name] ?? -1 }) - lazy var longestProxyGroupName = { - return proxyGroups.max { $1.name.count > $0.name.count }?.name ?? "" - }() + lazy var longestProxyGroupName = proxyGroups.max { $1.name.count > $0.name.count }?.name ?? "" lazy var maxProxyNameLength: CGFloat = { let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: 20) diff --git a/ClashX/Models/ClashRule.swift b/ClashX/Models/ClashRule.swift index ba3c39e5e..8be225b06 100644 --- a/ClashX/Models/ClashRule.swift +++ b/ClashX/Models/ClashRule.swift @@ -15,7 +15,7 @@ class ClashRule: Codable { } class ClashRuleResponse: Codable { - var rules: [ClashRule]? = nil + var rules: [ClashRule]? static func empty() -> ClashRuleResponse { return ClashRuleResponse() diff --git a/ClashX/Models/SavedProxyModel.swift b/ClashX/Models/SavedProxyModel.swift index 15f9715e9..38ff1666f 100644 --- a/ClashX/Models/SavedProxyModel.swift +++ b/ClashX/Models/SavedProxyModel.swift @@ -21,16 +21,13 @@ struct SavedProxyModel: Codable { static func loadsFromUserDefault() -> [SavedProxyModel] { if let data = UserDefaults.standard.object(forKey: key) as? Data, - let models = try? JSONDecoder().decode([SavedProxyModel].self, from: data) { + let models = try? JSONDecoder().decode([SavedProxyModel].self, from: data) { var set = Set() - let filtered = models.filter({ model in + let filtered = models.filter { model in let pass = !set.contains(model.key) set.insert(model.key) - if !pass { - print("pass", model) - } return pass - }) + } return filtered } return [] diff --git a/ClashX/Resources/sampleConfig.yaml b/ClashX/Resources/sampleConfig.yaml index cece8a179..e6ac56444 100644 --- a/ClashX/Resources/sampleConfig.yaml +++ b/ClashX/Resources/sampleConfig.yaml @@ -2,18 +2,13 @@ ## 配置文件需要放置在 $HOME/.config/clash/*.yaml ## 这份文件是clashX的基础配置文件,请尽量新建配置文件进行修改。 -## !!!只有这份文件的端口设置会随ClashX启动生效 +## 端口设置请在 菜单条图标->配置->更多配置 中进行修改 -## 如果您不知道如何操作,请参阅 官方Github文档 https://github.com/Dreamacro/clash/blob/dev/README.md +## 如果您不知道如何操作,请参阅 官方Github文档 https://dreamacro.github.io/clash/ #---------------------------------------------------# -# (HTTP and SOCKS5 in one port) -mixed-port: 7890 -# RESTful API for clash -external-controller: 127.0.0.1:9090 -allow-lan: false mode: rule -log-level: warning +log-level: info proxies: diff --git a/ClashX/Support Files/en.lproj/Localizable.strings b/ClashX/Support Files/en.lproj/Localizable.strings index 759d1d78b..913ba0e90 100644 --- a/ClashX/Support Files/en.lproj/Localizable.strings +++ b/ClashX/Support Files/en.lproj/Localizable.strings @@ -1,3 +1,6 @@ +/* No comment provided by engineer. */ +"Active Connections" = "Active Connections"; + /* No comment provided by engineer. */ "Add a remote config" = "Add a remote config"; @@ -5,10 +8,13 @@ "Add a remote control config" = "Add a remote control config"; /* No comment provided by engineer. */ -"Apply and Quit" = "Apply and Quit"; +"All Clients" = "All Clients"; /* No comment provided by engineer. */ -"Auto Close Connection" = "Auto Close Connection"; +"All Hosts" = "All Hosts"; + +/* No comment provided by engineer. */ +"Apply and Quit" = "Apply and Quit"; /* No comment provided by engineer. */ "Benchmark" = "Benchmark"; @@ -34,15 +40,39 @@ /* No comment provided by engineer. */ "ClashX Start Error!" = "ClashX Start Error!"; +/* No comment provided by engineer. */ +"ClashX use a daemon helper to setup your system proxy. Please enable ClashX in the Login Items under the Allow in the Background section and relaunch the app" = "ClashX use a daemon helper to setup your system proxy. Please enable ClashX in the Login Items under the Allow in the Background section and relaunch the app"; + +/* No comment provided by engineer. */ +"Click OK to quit the app and apply change." = "Click OK to quit the app and apply change."; + +/* No comment provided by engineer. */ +"Client" = "Client"; + +/* No comment provided by engineer. */ +"Close Connection" = "Close Connection"; + /* No comment provided by engineer. */ "Config file have been changed" = "Config file have been changed"; /* No comment provided by engineer. */ "Config loading Fail!" = "Config loading Fail!"; +/* No comment provided by engineer. */ +"Connecting" = "Connecting"; + +/* No comment provided by engineer. */ +"Copy Shell Command" = "Copy Shell Command"; + +/* No comment provided by engineer. */ +"Copy Shell Command (External)" = "Copy Shell Command (External)"; + /* No comment provided by engineer. */ "Custom your GEOIP MMDB download address." = "Custom your GEOIP MMDB download address."; +/* No comment provided by engineer. */ +"Date" = "Date"; + /* No comment provided by engineer. */ "Details" = "Details"; @@ -50,26 +80,56 @@ "Direct" = "Direct"; /* No comment provided by engineer. */ -"Disable Restore Proxy Setting" = "Disable Restore Proxy Setting"; +"Direct Mode" = "Direct Mode"; + +/* No comment provided by engineer. */ +"Done" = "Done"; + +/* No comment provided by engineer. */ +"Download" = "Download"; /* No comment provided by engineer. */ "Download fail" = "Download fail"; /* No comment provided by engineer. */ -"Enhance proxy list render" = "Enhance proxy list render"; +"Download speed" = "Download speed"; + +/* No comment provided by engineer. */ +"Edit in Text Mode" = "Edit in Text Mode"; /* No comment provided by engineer. */ "fail" = "fail"; +/* No comment provided by engineer. */ +"Fail" = "Fail"; + /* No comment provided by engineer. */ "Fail:" = "Fail:"; +/* No comment provided by engineer. */ +"fails: %@" = "fails: %@"; + /* No comment provided by engineer. */ "Global" = "Global"; +/* No comment provided by engineer. */ +"Global Mode" = "Global Mode"; + +/* No comment provided by engineer. */ +"Host" = "Host"; + +/* No comment provided by engineer. */ +"Hosts" = "Hosts"; + /* No comment provided by engineer. */ "hours" = "hours"; +/* No comment provided by engineer. */ +"iCloud not available" = "iCloud not available"; + +/* No comment provided by engineer. */ +"If you can not find ClashX in the settings, you can try reset daemon" = "If you can not find ClashX in the settings, you can try reset daemon"; + /* No comment provided by engineer. */ "Install" = "Install"; @@ -82,7 +142,8 @@ /* No comment provided by engineer. */ "Load Balance" = "Load Balance"; -"Mode" = "Mode"; +/* No comment provided by engineer. */ +"Local Clients" = "Local Clients"; /* No comment provided by engineer. */ "Need to Restart the ClashX to Take effect, Please start clashX manually" = "Need to Restart the ClashX to Take effect, Please start clashX manually"; @@ -90,9 +151,30 @@ /* No comment provided by engineer. */ "Never" = "Never"; +/* No comment provided by engineer. */ +"Never show again" = "Never show again"; + /* No comment provided by engineer. */ "OK" = "OK"; +/* No comment provided by engineer. */ +"Open Connection Details" = "Open Connection Details"; + +/* No comment provided by engineer. */ +"Open Dashboard" = "Open Dashboard"; + +/* No comment provided by engineer. */ +"Open Log" = "Open Log"; + +/* No comment provided by engineer. */ +"Open Menu" = "Open Menu"; + +/* No comment provided by engineer. */ +"Open System Login Item Setting" = "Open System Login Item Setting"; + +/* No comment provided by engineer. */ +"Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services." = "Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services."; + /* No comment provided by engineer. */ "Ports Open Fail, Please try to restart ClashX" = "Ports Open Fail, Please try to restart ClashX"; @@ -108,6 +190,15 @@ /* No comment provided by engineer. */ "Quit" = "Quit"; +/* No comment provided by engineer. */ +"Quit ClashX?" = "Quit ClashX?"; + +/* No comment provided by engineer. */ +"Recent Connections" = "Recent Connections"; + +/* No comment provided by engineer. */ +"Reduce alerts if notification permission is disabled" = "Reduce alerts if notification permission is disabled"; + /* No comment provided by engineer. */ "Reload Config Fail" = "Reload Config Fail"; @@ -123,12 +214,21 @@ /* No comment provided by engineer. */ "Remote Config Update Fail" = "Remote Config Update Fail"; +/* No comment provided by engineer. */ +"Requests" = "Requests"; + +/* No comment provided by engineer. */ +"Reset Daemon" = "Reset Daemon"; + /* No comment provided by engineer. */ "ReTest" = "ReTest"; /* No comment provided by engineer. */ "Rule" = "Rule"; +/* No comment provided by engineer. */ +"Rule Mode" = "Rule Mode"; + /* No comment provided by engineer. */ "Script" = "Script"; @@ -136,11 +236,14 @@ "Should be a least 1 hour" = "Should be a least 1 hour"; /* No comment provided by engineer. */ -"Show speedTest at top" = "Show speedTest at top"; +"Sources" = "Sources"; /* No comment provided by engineer. */ "Stable" = "Stable"; +/* No comment provided by engineer. */ +"Status" = "Status"; + /* No comment provided by engineer. */ "Succeed!" = "Succeed!"; @@ -150,6 +253,9 @@ /* No comment provided by engineer. */ "Success!" = "Success!"; +/* No comment provided by engineer. */ +"System Proxy" = "System Proxy"; + /* No comment provided by engineer. */ "System Proxy Changed" = "System Proxy Changed"; @@ -159,12 +265,30 @@ /* No comment provided by engineer. */ "Testing" = "Testing"; +/* No comment provided by engineer. */ +"The active connections will be interrupted." = "The active connections will be interrupted."; + /* No comment provided by engineer. */ "The remote config name is duplicated" = "The remote config name is duplicated"; +/* No comment provided by engineer. */ +"The status icon is coverd or hide by other app." = "The status icon is coverd or hide by other app."; + /* No comment provided by engineer. */ "This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly." = "This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly."; +/* No comment provided by engineer. */ +"total: %d, success: %d" = "total: %d, success: %d"; + +/* No comment provided by engineer. */ +"Type" = "Type"; + +/* No comment provided by engineer. */ +"Unknown" = "Unknown"; + +/* No comment provided by engineer. */ +"Update external resource complete" = "Update external resource complete"; + /* No comment provided by engineer. */ "Update GEOIP Database" = "Update GEOIP Database"; @@ -175,13 +299,10 @@ "Updating" = "Updating"; /* No comment provided by engineer. */ -"Upgrade Channel" = "Upgrade Channel"; - -/* No comment provided by engineer. */ -"URL is not valid" = "URL is not valid"; +"Upload" = "Upload"; /* No comment provided by engineer. */ -"Use iCloud" = "Use iCloud"; +"Upload speed" = "Upload speed"; /* No comment provided by engineer. */ "Use reload config to try reconnect." = "Use reload config to try reconnect."; diff --git a/ClashX/Support Files/zh-Hans.lproj/Localizable.strings b/ClashX/Support Files/zh-Hans.lproj/Localizable.strings index 292273ac7..b867ce1b5 100644 --- a/ClashX/Support Files/zh-Hans.lproj/Localizable.strings +++ b/ClashX/Support Files/zh-Hans.lproj/Localizable.strings @@ -1,3 +1,6 @@ +/* No comment provided by engineer. */ +"Active Connections" = "活动连接"; + /* No comment provided by engineer. */ "Add a remote config" = "添加托管配置文件"; @@ -5,10 +8,13 @@ "Add a remote control config" = "添加远程控制器配置"; /* No comment provided by engineer. */ -"Apply and Quit" = "应用并退出"; +"All Clients" = "所有客户端"; /* No comment provided by engineer. */ -"Auto Close Connection" = "切换代理时中断连接"; +"All Hosts" = "所有主机名"; + +/* No comment provided by engineer. */ +"Apply and Quit" = "应用并退出"; /* No comment provided by engineer. */ "Benchmark" = "延迟测速"; @@ -34,15 +40,39 @@ /* No comment provided by engineer. */ "ClashX Start Error!" = "ClashX 启动出错"; +/* No comment provided by engineer. */ +"ClashX use a daemon helper to setup your system proxy. Please enable ClashX in the Login Items under the Allow in the Background section and relaunch the app" = "ClashX需要通过后台Daemon进程来设置系统代理,请在\"系统偏好设置->登录项->允许在后台 中\"允许ClashX"; + +/* No comment provided by engineer. */ +"Click OK to quit the app and apply change." = "确认清理并退出软件"; + +/* No comment provided by engineer. */ +"Client" = "客户端"; + +/* No comment provided by engineer. */ +"Close Connection" = "关闭连接"; + /* No comment provided by engineer. */ "Config file have been changed" = "配置文件已被修改"; /* No comment provided by engineer. */ "Config loading Fail!" = "配置文件加载失败"; +/* No comment provided by engineer. */ +"Connecting" = "连接中"; + +/* No comment provided by engineer. */ +"Copy Shell Command" = "复制终端代理命令"; + +/* No comment provided by engineer. */ +"Copy Shell Command (External)" = "复制终端代理命令(外部IP)"; + /* No comment provided by engineer. */ "Custom your GEOIP MMDB download address." = "自定义下载地址"; +/* No comment provided by engineer. */ +"Date" = "时间"; + /* No comment provided by engineer. */ "Details" = "查看详情"; @@ -50,26 +80,56 @@ "Direct" = "直连"; /* No comment provided by engineer. */ -"Disable Restore Proxy Setting" = "关闭自动还原之前代理"; +"Direct Mode" = "直接连接"; + +/* No comment provided by engineer. */ +"Done" = "完成"; + +/* No comment provided by engineer. */ +"Download" = "下载"; /* No comment provided by engineer. */ "Download fail" = "下载失败"; /* No comment provided by engineer. */ -"Enhance proxy list render" = "增强渲染代理列表"; +"Download speed" = "下载速率"; + +/* No comment provided by engineer. */ +"Edit in Text Mode" = "编辑配置"; /* No comment provided by engineer. */ "fail" = "失败"; +/* No comment provided by engineer. */ +"Fail" = "失败"; + /* No comment provided by engineer. */ "Fail:" = "失败:"; +/* No comment provided by engineer. */ +"fails: %@" = "失败: %@"; + /* No comment provided by engineer. */ "Global" = "全局"; +/* No comment provided by engineer. */ +"Global Mode" = "全局连接"; + +/* No comment provided by engineer. */ +"Host" = "主机名"; + +/* No comment provided by engineer. */ +"Hosts" = "主机名"; + /* No comment provided by engineer. */ "hours" = "小时"; +/* No comment provided by engineer. */ +"iCloud not available" = "iCloud 不可用"; + +/* No comment provided by engineer. */ +"If you can not find ClashX in the settings, you can try reset daemon" = "如果在设置里没找到ClashX,可以尝试重置守护程序"; + /* No comment provided by engineer. */ "Install" = "安装"; @@ -82,7 +142,8 @@ /* No comment provided by engineer. */ "Load Balance" = "负载均衡"; -"Mode" = "模式"; +/* No comment provided by engineer. */ +"Local Clients" = "本地程序"; /* No comment provided by engineer. */ "Need to Restart the ClashX to Take effect, Please start clashX manually" = "需要重启ClashX生效,请手动启动ClashX"; @@ -90,9 +151,30 @@ /* No comment provided by engineer. */ "Never" = "从未"; +/* No comment provided by engineer. */ +"Never show again" = "不再提示"; + /* No comment provided by engineer. */ "OK" = "确定"; +/* No comment provided by engineer. */ +"Open Connection Details" = "打开连接查看器"; + +/* No comment provided by engineer. */ +"Open Dashboard" = "打开控制台"; + +/* No comment provided by engineer. */ +"Open Log" = "打开日志"; + +/* No comment provided by engineer. */ +"Open Menu" = "打开菜单"; + +/* No comment provided by engineer. */ +"Open System Login Item Setting" = "打开系统登录项设置"; + +/* No comment provided by engineer. */ +"Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services." = "请允许ClashX使用定位服务来获取当前所连接的WiFi名称从而提供按需暂停服务。"; + /* No comment provided by engineer. */ "Ports Open Fail, Please try to restart ClashX" = "端口打开失败,请尝试重启ClashX"; @@ -108,6 +190,15 @@ /* No comment provided by engineer. */ "Quit" = "退出"; +/* No comment provided by engineer. */ +"Quit ClashX?" = "退出ClashX?"; + +/* No comment provided by engineer. */ +"Recent Connections" = "最近连接"; + +/* No comment provided by engineer. */ +"Reduce alerts if notification permission is disabled" = "在通知权限关闭时减少提示弹窗"; + /* No comment provided by engineer. */ "Reload Config Fail" = "重载配置文件失败"; @@ -123,12 +214,21 @@ /* No comment provided by engineer. */ "Remote Config Update Fail" = "托管配置文件更新失败"; +/* No comment provided by engineer. */ +"Requests" = "请求"; + +/* No comment provided by engineer. */ +"Reset Daemon" = "重置守护程序"; + /* No comment provided by engineer. */ "ReTest" = "重新测速"; /* No comment provided by engineer. */ "Rule" = "规则"; +/* No comment provided by engineer. */ +"Rule Mode" = "规则判断"; + /* No comment provided by engineer. */ "Script" = "脚本"; @@ -136,11 +236,14 @@ "Should be a least 1 hour" = "至少需要1小时间隔"; /* No comment provided by engineer. */ -"Show speedTest at top" = "顶端显示测速按钮"; +"Sources" = "来源"; /* No comment provided by engineer. */ "Stable" = "Stable"; +/* No comment provided by engineer. */ +"Status" = "状态"; + /* No comment provided by engineer. */ "Succeed!" = "成功!"; @@ -150,6 +253,9 @@ /* No comment provided by engineer. */ "Success!" = "成功!"; +/* No comment provided by engineer. */ +"System Proxy" = "设置系统代理"; + /* No comment provided by engineer. */ "System Proxy Changed" = "系统代理设置已修改"; @@ -159,12 +265,30 @@ /* No comment provided by engineer. */ "Testing" = "测速中"; +/* No comment provided by engineer. */ +"The active connections will be interrupted." = "活动的连接将被强制打断。"; + /* No comment provided by engineer. */ "The remote config name is duplicated" = "托管配置文件名称重复"; +/* No comment provided by engineer. */ +"The status icon is coverd or hide by other app." = "菜单栏图标被刘海屏遮挡或者被其他app隐藏,请检查菜单栏状态。"; + /* No comment provided by engineer. */ "This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly." = "由于Clash Core发布1.0版本,使用此版本的 ClashX 可能需要更新配置内容\n前往 https://github.com/Dreamacro/clash/wiki/breaking-changes-in-1.0.0 查看详情"; +/* No comment provided by engineer. */ +"total: %d, success: %d" = "共计: %d, 成功: %d"; + +/* No comment provided by engineer. */ +"Type" = "类型"; + +/* No comment provided by engineer. */ +"Unknown" = "未知"; + +/* No comment provided by engineer. */ +"Update external resource complete" = "更新外部资源完成"; + /* No comment provided by engineer. */ "Update GEOIP Database" = "更新IP数据库"; @@ -175,13 +299,10 @@ "Updating" = "更新中"; /* No comment provided by engineer. */ -"Upgrade Channel" = "更新通道"; - -/* No comment provided by engineer. */ -"URL is not valid" = "链接不合法"; +"Upload" = "上传"; /* No comment provided by engineer. */ -"Use iCloud" = "使用iCloud"; +"Upload speed" = "上传速率"; /* No comment provided by engineer. */ "Use reload config to try reconnect." = "使用重载配置文件按钮尝试重新连接"; diff --git a/ClashX/Support Files/zh-Hant.lproj/Localizable.strings b/ClashX/Support Files/zh-Hant.lproj/Localizable.strings new file mode 100644 index 000000000..83bf3f97a --- /dev/null +++ b/ClashX/Support Files/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,308 @@ +/* No comment provided by engineer. */ +"Active Connections" = "活動連線"; + +/* No comment provided by engineer. */ +"Add a remote config" = "添加託管配置文件"; + +/* No comment provided by engineer. */ +"Add a remote control config" = "添加遠程控制器配置"; + +/* No comment provided by engineer. */ +"All Clients" = "所有客戶端"; + +/* No comment provided by engineer. */ +"All Hosts" = "所有主機名"; + +/* No comment provided by engineer. */ +"Apply and Quit" = "應用並退出"; + +/* No comment provided by engineer. */ +"Benchmark" = "延遲測速"; + +/* No comment provided by engineer. */ +"Benchmark Finished!" = "延遲測速完成"; + +/* No comment provided by engineer. */ +"Benchmark has begun, please wait." = "延遲測速開始,請稍候"; + +/* No comment provided by engineer. */ +"Benchmark is processing, please wait." = "延遲測速正在進行中,請稍候。"; + +/* No comment provided by engineer. */ +"Cancel" = "取消"; + +/* No comment provided by engineer. */ +"ClashX fail to create ~/.config/clash folder. Please check privileges or manually create folder and restart ClashX." = "ClashX 創建 ~/.config/clash 文件夾失敗,請檢查權限設定或者手動創建文件夾後重啟 ClashX"; + +/* No comment provided by engineer. */ +"ClashX needs to install/update a helper tool with administrator privileges, otherwise ClashX won't be able to configure system proxy." = "ClashX 需要使用管理員權限安裝/更新一個幫助程序,否則 ClashX 將無法設定系統代理。"; + +/* No comment provided by engineer. */ +"ClashX Start Error!" = "ClashX 啟動出錯"; + +/* No comment provided by engineer. */ +"ClashX use a daemon helper to setup your system proxy. Please enable ClashX in the Login Items under the Allow in the Background section and relaunch the app" = "ClashX需要通過後台Daemon進程來設定系統代理,請在\"系統偏好設定->登錄項->允許在後台 中\"允許ClashX"; + +/* No comment provided by engineer. */ +"Click OK to quit the app and apply change." = "確認清理併退出軟件"; + +/* No comment provided by engineer. */ +"Client" = "客戶端"; + +/* No comment provided by engineer. */ +"Close Connection" = "關閉連線"; + +/* No comment provided by engineer. */ +"Config file have been changed" = "配置文件已被修改"; + +/* No comment provided by engineer. */ +"Config loading Fail!" = "配置文件加載失敗"; + +/* No comment provided by engineer. */ +"Connecting" = "連接中"; + +/* No comment provided by engineer. */ +"Copy Shell Command" = "複製終端代理命令"; + +/* No comment provided by engineer. */ +"Copy Shell Command (External)" = "複製終端代理命令(外部IP)"; + +/* No comment provided by engineer. */ +"Custom your GEOIP MMDB download address." = "自定義下載地址"; + +/* No comment provided by engineer. */ +"Date" = "時間"; + +/* No comment provided by engineer. */ +"Details" = "查看詳情"; + +/* No comment provided by engineer. */ +"Direct" = "直連"; + +/* No comment provided by engineer. */ +"Direct Mode" = "直接連接"; + +/* No comment provided by engineer. */ +"Done" = "完成"; + +/* No comment provided by engineer. */ +"Download" = "下載"; + +/* No comment provided by engineer. */ +"Download fail" = "下載失敗"; + +/* No comment provided by engineer. */ +"Download speed" = "下載速率"; + +/* No comment provided by engineer. */ +"Edit in Text Mode" = "編輯配置"; + +/* No comment provided by engineer. */ +"fail" = "失敗"; + +/* No comment provided by engineer. */ +"Fail" = "失敗"; + +/* No comment provided by engineer. */ +"Fail:" = "失敗:"; + +/* No comment provided by engineer. */ +"fails: %@" = "失敗: %@"; + +/* No comment provided by engineer. */ +"Global" = "全局"; + +/* No comment provided by engineer. */ +"Global Mode" = "全局連接"; + +/* No comment provided by engineer. */ +"Host" = "主機名"; + +/* No comment provided by engineer. */ +"Hosts" = "主機名"; + +/* No comment provided by engineer. */ +"hours" = "小時"; + +/* No comment provided by engineer. */ +"iCloud not available" = "iCloud 不可用"; + +/* No comment provided by engineer. */ +"If you can not find ClashX in the settings, you can try reset daemon" = "如果在設定裡沒找到ClashX,可以嘗試重置守護程序"; + +/* No comment provided by engineer. */ +"Install" = "安裝"; + +/* No comment provided by engineer. */ +"Invalid input" = "輸入不合法"; + +/* No comment provided by engineer. */ +"Legacy Install" = "傳統安裝"; + +/* No comment provided by engineer. */ +"Load Balance" = "負載均衡"; + +/* No comment provided by engineer. */ +"Local Clients" = "本地程式"; + +/* No comment provided by engineer. */ +"Need to Restart the ClashX to Take effect, Please start clashX manually" = "需要重啟ClashX生效,請手動啟動ClashX"; + +/* No comment provided by engineer. */ +"Never" = "從未"; + +/* No comment provided by engineer. */ +"Never show again" = "不再提示"; + +/* No comment provided by engineer. */ +"OK" = "確定"; + +/* No comment provided by engineer. */ +"Open Connection Details" = "打開連接查看器"; + +/* No comment provided by engineer. */ +"Open Dashboard" = "打開控制台"; + +/* No comment provided by engineer. */ +"Open Log" = "打開日誌"; + +/* No comment provided by engineer. */ +"Open Menu" = "打開菜單"; + +/* No comment provided by engineer. */ +"Open System Login Item Setting" = "打開系統登錄項設定"; + +/* No comment provided by engineer. */ +"Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services." = "請允許 ClashX 使用定位服務,以獲取您目前連接的 WiFi 名稱,並提供按需暫停服務。"; + +/* No comment provided by engineer. */ +"Ports Open Fail, Please try to restart ClashX" = "端口打開失敗,請嘗試重啟ClashX"; + +/* No comment provided by engineer. */ +"Prelease" = "Prelease"; + +/* No comment provided by engineer. */ +"Proxy Mode" = "出站模式"; + +/* No comment provided by engineer. */ +"Proxy settings are changed by another process. ClashX is no longer the default system proxy." = "代理設定被另一個程式更改。ClashX 不再是默認的系統代理"; + +/* No comment provided by engineer. */ +"Quit" = "退出"; + +/* No comment provided by engineer. */ +"Quit ClashX?" = "退出 ClashX?"; + +/* No comment provided by engineer. */ +"Recent Connections" = "最近連線"; + +/* No comment provided by engineer. */ +"Reduce alerts if notification permission is disabled" = "在通知權限關閉時減少提示彈窗"; + +/* No comment provided by engineer. */ +"Reload Config Fail" = "重新加載配置失敗"; + +/* No comment provided by engineer. */ +"Reload Config Succeed" = "重新加載配置成功"; + +/* No comment provided by engineer. */ +"Remote Config Format Error" = "遠端配置格式錯誤"; + +/* No comment provided by engineer. */ +"Remote Config Update" = "遠程配置更新"; + +/* No comment provided by engineer. */ +"Remote Config Update Fail" = "遠程配置更新失敗"; + +/* No comment provided by engineer. */ +"Requests" = "請求"; + +/* No comment provided by engineer. */ +"Reset Daemon" = "重置守護進程"; + +/* No comment provided by engineer. */ +"ReTest" = "重測"; + +/* No comment provided by engineer. */ +"Rule" = "規則"; + +/* No comment provided by engineer. */ +"Rule Mode" = "規則模式"; + +/* No comment provided by engineer. */ +"Script" = "腳本"; + +/* No comment provided by engineer. */ +"Should be a least 1 hour" = "應該至少 1 小時"; + +/* No comment provided by engineer. */ +"Sources" = "來源"; + +/* No comment provided by engineer. */ +"Stable" = "穩定"; + +/* No comment provided by engineer. */ +"Status" = "狀態"; + +/* No comment provided by engineer. */ +"Succeed!" = "成功!"; + +/* No comment provided by engineer. */ +"Success" = "成功"; + +/* No comment provided by engineer. */ +"Success!" = "成功!"; + +/* No comment provided by engineer. */ +"System Proxy" = "系統代理"; + +/* No comment provided by engineer. */ +"System Proxy Changed" = "系統代理已更改"; + +/* No comment provided by engineer. */ +"Tap to reload config" = "點擊重新加載配置"; + +/* No comment provided by engineer. */ +"Testing" = "測試中"; + +/* No comment provided by engineer. */ +"The active connections will be interrupted." = "活躍的連接將被中斷。"; + +/* No comment provided by engineer. */ +"The remote config name is duplicated" = "遠端配置名稱重複"; + +/* No comment provided by engineer. */ +"The status icon is coverd or hide by other app." = "狀態圖標被其他應用覆蓋或隱藏"; + +/* No comment provided by engineer. */ +"This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly." = "此版本的 ClashX 包含因 clash core 1.0 發布而導致的中斷更改。請檢查您的配置是否無法正常工作"; + +/* No comment provided by engineer. */ +"total: %d, success: %d" = "共計: %d, 成功: %d"; + +/* No comment provided by engineer. */ +"Type" = "類型"; + +/* No comment provided by engineer. */ +"Unknown" = "未知"; + +/* No comment provided by engineer. */ +"Update external resource complete" = "更新外部資源完成"; + +/* No comment provided by engineer. */ +"Update GEOIP Database" = "更新 GEOIP 數據庫"; + +/* No comment provided by engineer. */ +"Update remote config update interval" = "更新遠程配置更新間隔"; + +/* No comment provided by engineer. */ +"Updating" = "更新中"; + +/* No comment provided by engineer. */ +"Upload" = "上傳"; + +/* No comment provided by engineer. */ +"Upload speed" = "上傳速率"; + +/* No comment provided by engineer. */ +"Use reload config to try reconnect." = "使用重新加載配置嘗試重新連接"; diff --git a/ClashX/Vendor/LoginServiceKit/LoginServiceKit.swift b/ClashX/Vendor/LoginServiceKit/LoginServiceKit.swift index c2f72277e..a064b0216 100644 --- a/ClashX/Vendor/LoginServiceKit/LoginServiceKit.swift +++ b/ClashX/Vendor/LoginServiceKit/LoginServiceKit.swift @@ -47,8 +47,9 @@ // import Cocoa +import ServiceManagement -public final class LoginServiceKit: NSObject { +public enum LoginServiceKit { private static var snapshot: (list: LSSharedFileList, items: [LSSharedFileListItem])? { guard let list = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil)?.takeRetainedValue() else { return nil @@ -57,6 +58,9 @@ public final class LoginServiceKit: NSObject { } public static func isExistLoginItems(at path: String = Bundle.main.bundlePath) -> Bool { + if #available(macOS 13.0, *) { + return SMAppService.mainApp.status == .enabled + } return loginItem(at: path) != nil } @@ -65,6 +69,17 @@ public final class LoginServiceKit: NSObject { guard isExistLoginItems(at: path) == false else { return false } + + if #available(macOS 13.0, *) { + do { + try SMAppService.mainApp.register() + return true + } catch let err { + Logger.log("add loginItem error: \(err.localizedDescription)", level: .error) + return false + } + } + guard let (list, _) = snapshot else { return false } @@ -78,6 +93,16 @@ public final class LoginServiceKit: NSObject { guard isExistLoginItems(at: path) == true else { return false } + + if #available(macOS 13.0, *) { + do { + try SMAppService.mainApp.unregister() + return true + } catch let err { + Logger.log("remove loginItem error: \(err.localizedDescription)", level: .error) + return false + } + } guard let (list, items) = snapshot else { return false } diff --git a/ClashX/Vendor/UserDefaultWrapper.swift b/ClashX/Vendor/UserDefaultWrapper.swift index 33c9a5191..0b5aa38d3 100644 --- a/ClashX/Vendor/UserDefaultWrapper.swift +++ b/ClashX/Vendor/UserDefaultWrapper.swift @@ -15,13 +15,13 @@ public struct UserDefault { let key: String let defaultValue: Value var userDefaults: UserDefaults - + public init(_ key: String, defaultValue: Value, userDefaults: UserDefaults = .standard) { self.key = key self.defaultValue = defaultValue self.userDefaults = userDefaults } - + public var wrappedValue: Value { get { return userDefaults.object(forKey: key) as? Value ?? defaultValue @@ -64,7 +64,6 @@ extension UInt64: PropertyListValue {} extension Double: PropertyListValue {} extension Float: PropertyListValue {} - extension Array: PropertyListValue where Element: PropertyListValue {} extension Dictionary: PropertyListValue where Key == String, Value: PropertyListValue {} diff --git a/ClashX/Vendor/Witness/EventStream.swift b/ClashX/Vendor/Witness/EventStream.swift index 1d42fd931..c44169e29 100755 --- a/ClashX/Vendor/Witness/EventStream.swift +++ b/ClashX/Vendor/Witness/EventStream.swift @@ -33,7 +33,7 @@ class EventStream { let paths = unsafeBitCast(eventPaths, to: NSArray.self) var events = [FileEvent]() - for i in 0.. Void)? - - static func create() -> ClashWebViewWindowController { - let win = NSWindow() - win.center() - let wc = ClashWebViewWindowController(window: win) - wc.contentViewController = ClashWebViewContoller() - return wc - } - - override func showWindow(_ sender: Any?) { - super.showWindow(sender) - NSApp.activate(ignoringOtherApps: true) - window?.makeKeyAndOrderFront(self) - window?.delegate = self - } -} - -extension ClashWebViewWindowController: NSWindowDelegate { - func windowWillClose(_ notification: Notification) { - NSApp.setActivationPolicy(.accessory) - onWindowClose?() - if let contentVC = contentViewController as? ClashWebViewContoller, let win = window { - if !win.styleMask.contains(.fullScreen) { - contentVC.lastSize = win.frame.size +enum WebCacheCleaner { + static func clean() { + HTTPCookieStorage.shared.removeCookies(since: Date.distantPast) + Logger.log("[WebCacheCleaner] All cookies deleted") + WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in + records.forEach { record in + WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {}) + Logger.log("[WebCacheCleaner] Record \(record) deleted") } } } } class ClashWebViewContoller: NSViewController { - let webview: CustomWKWebView = CustomWKWebView() - var bridge: WebViewJavascriptBridge? + let webview: CustomWKWebView = .init() + var bridge: JSBridge? let disposeBag = DisposeBag() let minSize = NSSize(width: 920, height: 580) - var lastSize: CGSize? { - set { - if let size = newValue { - UserDefaults.standard.set(NSStringFromSize(size), forKey: "ClashWebViewContoller.lastSize") - } - } - get { - if let str = UserDefaults.standard.value(forKey: "ClashWebViewContoller.lastSize") as? String { - return NSSizeFromString(str) as CGSize - } - return nil - } - } let effectView = NSVisualEffectView() - static func createWindowController() -> NSWindowController { - let sb = NSStoryboard(name: "Main", bundle: Bundle.main) - let vc = sb.instantiateController(withIdentifier: "ClashWebViewContoller") as! ClashWebViewContoller - let wc = NSWindowController(window: NSWindow()) - wc.contentViewController = vc - return wc - } - override func loadView() { view = NSView(frame: NSRect(origin: .zero, size: minSize)) } @@ -83,23 +43,18 @@ class ClashWebViewContoller: NSViewController { webview.navigationDelegate = self webview.customUserAgent = "ClashX Runtime" - - if NSAppKitVersion.current.rawValue > 1500 { - webview.setValue(false, forKey: "drawsBackground") - } else { - webview.setValue(true, forKey: "drawsTransparentBackground") + if #available(macOS 13.3, *) { + webview.isInspectable = true } + webview.setValue(false, forKey: "drawsBackground") + let script = WKUserScript(source: "console.log(\"dashboard loaded\")", injectionTime: .atDocumentStart, forMainFrameOnly: false) + + webview.configuration.userContentController.addUserScript(script) bridge = JsBridgeUtil.initJSbridge(webview: webview, delegate: self) - registerExtenalJSBridgeFunction() webview.configuration.preferences.setValue(true, forKey: "developerExtrasEnabled") - NotificationCenter.default.rx.notification(.configFileChange).bind { - [weak self] _ in - self?.bridge?.callHandler("onConfigChange") - }.disposed(by: disposeBag) - NotificationCenter.default.rx.notification(.reloadDashboard).bind { [weak self] _ in self?.webview.reload() @@ -116,24 +71,12 @@ class ClashWebViewContoller: NSViewController { view.window?.isOpaque = false view.window?.backgroundColor = NSColor.clear - view.window?.styleMask.insert(.closable) - view.window?.styleMask.insert(.resizable) - view.window?.styleMask.insert(.miniaturizable) - if #available(OSX 10.13, *) { - view.window?.toolbar = NSToolbar() - view.window?.toolbar?.showsBaselineSeparator = false - view.wantsLayer = true - view.layer?.cornerRadius = 10 - } + view.window?.toolbar = NSToolbar() + view.window?.toolbar?.showsBaselineSeparator = false + view.wantsLayer = true + view.layer?.cornerRadius = 10 view.window?.minSize = minSize - if let lastSize = lastSize, lastSize != .zero { - view.window?.setContentSize(lastSize) - } - view.window?.center() - if NSApp.activationPolicy() == .accessory { - NSApp.setActivationPolicy(.regular) - } } func setupView() { @@ -148,46 +91,46 @@ class ClashWebViewContoller: NSViewController { } func loadWebRecourses() { + WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeOfflineWebApplicationCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler: {}) // defaults write com.west2online.ClashX webviewUrl "your url" + if let userDefineUrl = UserDefaults.standard.string(forKey: "webviewUrl"), let url = URL(string: userDefineUrl) { + Logger.log("get user define url: \(url)") + webview.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 0)) + return + } let defaultUrl = "http://127.0.0.1:\(ConfigManager.shared.apiPort)/ui/" - let url = UserDefaults.standard.string(forKey: "webviewUrl") ?? defaultUrl - if let url = URL(string: url) { + if let url = URL(string: defaultUrl) { + Logger.log("dashboard url:\(defaultUrl)") webview.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 0)) + return } + Logger.log("load dashboard url fail", level: .error) } - deinit { - NSApp.setActivationPolicy(.accessory) - } -} - -extension ClashWebViewContoller { - func registerExtenalJSBridgeFunction() { - bridge?.registerHandler("setDragAreaHeight") { - [weak self] anydata, responseCallback in - if let height = anydata as? CGFloat { - self?.webview.dragableAreaHeight = height - } - responseCallback?(nil) - } + func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + NSAlert.alert(with: message) + completionHandler() } } extension ClashWebViewContoller: WKUIDelegate, WKNavigationDelegate { - func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {} + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + Logger.log("[dashboard] webview crashed", level: .error) + } func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {} func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + Logger.log("[dashboard] load request \(String(describing: navigationAction.request.url?.absoluteString))", level: .debug) decisionHandler(.allow) } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - Logger.log("\(String(describing: navigation))", level: .debug) + Logger.log("[dashboard] didFinish \(String(describing: navigation))", level: .info) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - Logger.log("\(error)", level: .debug) + Logger.log("[dashboard] \(String(describing: navigation)) error: \(error)", level: .error) } func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { @@ -198,18 +141,27 @@ extension ClashWebViewContoller: WKUIDelegate, WKNavigationDelegate { } } -extension ClashWebViewContoller: WebResourceLoadDelegate {} - class CustomWKWebView: WKWebView { var dragableAreaHeight: CGFloat = 30 let alwaysDragableLeftAreaWidth: CGFloat = 150 - override func mouseDown(with event: NSEvent) { - super.mouseDown(with: event) + private func isInDargArea(with event: NSEvent?) -> Bool { + guard let event = event else { return false } let x = event.locationInWindow.x let y = (window?.frame.size.height ?? 0) - event.locationInWindow.y + return x < alwaysDragableLeftAreaWidth || y < dragableAreaHeight + } - if x < alwaysDragableLeftAreaWidth || y < dragableAreaHeight { + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + if isInDargArea(with: event) { + return true + } + return super.acceptsFirstMouse(for: event) + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + if isInDargArea(with: event) { window?.performDrag(with: event) } } diff --git a/ClashX/ViewControllers/Connections/ConnectionsViewController.swift b/ClashX/ViewControllers/Connections/ConnectionsViewController.swift new file mode 100644 index 000000000..41361a04c --- /dev/null +++ b/ClashX/ViewControllers/Connections/ConnectionsViewController.swift @@ -0,0 +1,220 @@ +// +// ConnectionsViewController.swift +// ClashX +// +// Created by yicheng on 2023/7/5. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import Combine + +@available(macOS 10.15, *) +class ConnectionsViewController: NSViewController { + let viewModel = ConnectionsViewModel() + let leftViewModel = ConnectionLeftPannelViewModel() + lazy var leftTableView = ConnectionLeftPannelView(viewModel: leftViewModel) + let topViewModel = ConnectionTopListViewModel() + lazy var topView = ConnectionTopListView(viewModel: topViewModel) + let detailView = ConnectionDetailInfoView() + + let connectionDetailViewModel = ConnectionDetailViewModel() + + var disposeBag = Set() + var modeCancellable = Set() + var leftWidthConstraint: NSLayoutConstraint? + var topViewBottomConstraint: NSLayoutConstraint? + override func viewDidLoad() { + super.viewDidLoad() + setup() + setupCommonViewModel() + setupAllConnViewModel() + } + + override func loadView() { + view = ConnectionsViewControllerBaseView(frame: NSRect(origin: .zero, size: CGSize(width: 900, height: 600))) + } + + private func setup() { + view.addSubview(leftTableView) + view.makeConstraints { + [$0.widthAnchor.constraint(greaterThanOrEqualToConstant: 900), + $0.heightAnchor.constraint(greaterThanOrEqualToConstant: 600)] + } + + leftWidthConstraint = leftTableView.widthAnchor.constraint(equalToConstant: 200) + leftTableView.makeConstraints { + [$0.leftAnchor.constraint(equalTo: view.leftAnchor), + $0.topAnchor.constraint(equalTo: view.topAnchor), + $0.bottomAnchor.constraint(equalTo: view.bottomAnchor), + leftWidthConstraint!] + } + + (view as! ConnectionsViewControllerBaseView).leftWidthConstraint = leftWidthConstraint + + view.addSubview(topView) + topView.makeConstraints { + [$0.leftAnchor.constraint(equalTo: leftTableView.rightAnchor), + $0.topAnchor.constraint(equalTo: view.topAnchor), + $0.rightAnchor.constraint(equalTo: view.rightAnchor)] + } + topViewBottomConstraint = topView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + detailView.setup(with: connectionDetailViewModel) + } + + private func setupCommonViewModel() { + topViewModel.onSelectedConnection = { [weak self] in + self?.viewModel.selectedConnection = $0 + } + + viewModel.$selectedConnection.sink { [weak self] conn in + self?.connectionDetailViewModel.accept(connection: conn) + }.store(in: &disposeBag) + + leftViewModel.onSelectedFilter = { [weak topViewModel] in + topViewModel?.applicationFilter = $0 + } + + viewModel.$showBottomView.removeDuplicates().sink { [weak self] show in + guard let self else { return } + if show { + view.addSubview(detailView) + topViewBottomConstraint?.isActive = false + detailView.makeConstraints { + [$0.leftAnchor.constraint(equalTo: self.leftTableView.rightAnchor), + $0.heightAnchor.constraint(equalToConstant: 236), + $0.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), + $0.rightAnchor.constraint(equalTo: self.view.rightAnchor), + $0.topAnchor.constraint(equalTo: self.topView.bottomAnchor)] + } + } else { + detailView.removeFromSuperview() + topViewBottomConstraint?.isActive = true + } + }.store(in: &disposeBag) + } + + private func setupAllConnViewModel() { + viewModel.connectionDataDidRefresh.sink { [weak topViewModel] in + topViewModel?.connectionDidUpdate() + }.store(in: &modeCancellable) + + viewModel.$connections.map { Array($0.values) }.sink { [weak self] in + self?.topViewModel.accept(connections: $0) + }.store(in: &modeCancellable) + + viewModel.$applicationMap.map { Array($0.values) }.sink { [weak self] in + self?.leftViewModel.accept(connections: $0) + }.store(in: &modeCancellable) + + viewModel.$sourceIPs.map { Array($0) }.sink { [weak self] in + self?.leftViewModel.accept(sources: $0) + }.store(in: &modeCancellable) + + viewModel.$hosts.map { Array($0) }.sink { [weak self] in + self?.leftViewModel.accept(hosts: $0) + }.store(in: &modeCancellable) + } + + private func setupActiveConnViewModel() { + viewModel.connectionDataDidRefresh.sink { [weak self] in + guard let self else { return } + topViewModel.accept(connections: viewModel.currentConnections) + leftViewModel.accept(apps: viewModel.currentApplications, sources: viewModel.currentSourceIPs, hosts: viewModel.currentHosts) + }.store(in: &modeCancellable) + viewModel.connectionDataDidRefresh.send() + } + + func setActiveMode(enable: Bool) { + modeCancellable.removeAll() + viewModel.activeOnlyMode = enable + if viewModel.activeOnlyMode { + setupActiveConnViewModel() + } else { + setupAllConnViewModel() + } + } +} + +@available(macOS 10.15, *) +extension ConnectionsViewController: DashboardSubViewControllerProtocol { + func actionSearch(string: String) { + topViewModel.textFilter = string + } +} + +class ConnectionsViewControllerBaseView: NSView { + var leftWidthConstraint: NSLayoutConstraint? + enum DragType { + case none + case leftPannel + } + + var dragType = DragType.none + let dragSize: CGFloat = 5.0 + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func hitTest(_ point: NSPoint) -> NSView? { + if dragType == .none { + return super.hitTest(point) + } + return self + } + + override func updateTrackingAreas() { + super.updateTrackingAreas() + trackingAreas.forEach { removeTrackingArea($0) } + addTrackingArea(NSTrackingArea(rect: bounds, + options: [.mouseMoved, + .mouseEnteredAndExited, + .activeAlways], + owner: self)) + } + + override func mouseExited(with event: NSEvent) { + NSCursor.arrow.set() + } + + override func mouseDown(with event: NSEvent) { + update(with: event) + } + + override func mouseUp(with event: NSEvent) { + dragType = .none + } + + override func mouseMoved(with event: NSEvent) { + update(with: event) + } + + override func mouseDragged(with event: NSEvent) { + switch dragType { + case .none: + break + case .leftPannel: + let deltaX = event.deltaX + let target = (leftWidthConstraint?.constant ?? 0) + deltaX + leftWidthConstraint?.constant = min(max(target, 200), 400) + } + } + + func update(with event: NSEvent) { + let locationInView = convert(event.locationInWindow, from: nil) + let currentLeftSize = leftWidthConstraint?.constant ?? 0 + if locationInView.x > currentLeftSize - dragSize && locationInView.x < currentLeftSize + dragSize { + dragType = .leftPannel + NSCursor.resizeLeftRight.set() + return + } + dragType = .none + NSCursor.arrow.set() + } +} diff --git a/ClashX/ViewControllers/Connections/DashboardSubViewControllerProtocol.swift b/ClashX/ViewControllers/Connections/DashboardSubViewControllerProtocol.swift new file mode 100644 index 000000000..151188ec1 --- /dev/null +++ b/ClashX/ViewControllers/Connections/DashboardSubViewControllerProtocol.swift @@ -0,0 +1,13 @@ +// +// DashboardSubViewControllerProtocol.swift +// ClashX +// +// Created by yicheng on 2023/7/14. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit + +protocol DashboardSubViewControllerProtocol: NSViewController { + func actionSearch(string: String) +} diff --git a/ClashX/ViewControllers/Connections/DashboardViewController.swift b/ClashX/ViewControllers/Connections/DashboardViewController.swift new file mode 100644 index 000000000..eb25e9f26 --- /dev/null +++ b/ClashX/ViewControllers/Connections/DashboardViewController.swift @@ -0,0 +1,126 @@ +// +// DashboardViewController.swift +// ClashX +// +// Created by yicheng on 2023/7/14. +// Copyright © 2023 west2online. All rights reserved. +// + +import Cocoa + +enum DashboardContentType: Int, CaseIterable { + case allConnection + case activeConnection + + var title: String { + switch self { + case .allConnection: + return NSLocalizedString("Recent Connections", comment: "") + case .activeConnection: + return NSLocalizedString("Active Connections", comment: "") + } + } +} + +@available(macOS 10.15, *) +class DashboardViewController: NSViewController { + private let toolbar = NSToolbar() + private var segmentControl: NSSegmentedControl! + private let searchField = NSSearchField() + + private let connectionVC = ConnectionsViewController() + + private var currentContentVC: DashboardSubViewControllerProtocol? + + override func viewDidLoad() { + super.viewDidLoad() + segmentControl = NSSegmentedControl(labels: DashboardContentType.allCases.map(\.title), + trackingMode: .selectOne, + target: self, + action: #selector(actionSwitchSegmentControl(sender:))) + segmentControl.selectedSegment = 0 + searchField.delegate = self + setCurrentVC(connectionVC) + } + + override func loadView() { + view = NSView() + } + + override func viewWillAppear() { + super.viewWillAppear() + toolbar.delegate = self + view.window?.toolbar = toolbar + view.window?.backgroundColor = NSColor.clear + if #available(macOS 11.0, *) { + view.window?.toolbarStyle = .unifiedCompact + } else { + view.window?.toolbar?.sizeMode = .small + } + } + + func setCurrentVC(_ vc: DashboardSubViewControllerProtocol) { + currentContentVC?.removeFromParent() + currentContentVC?.view.removeFromSuperview() + addChild(vc) + view.addSubview(vc.view) + vc.view.makeConstraintsToBindToSuperview() + currentContentVC = vc + } + + @objc func actionSwitchSegmentControl(sender: NSSegmentedControl) { + guard let contentType = DashboardContentType(rawValue: sender.selectedSegment) else { return } + switch contentType { + case .allConnection: + connectionVC.setActiveMode(enable: false) + case .activeConnection: + connectionVC.setActiveMode(enable: true) + } + } +} + +@available(macOS 10.15, *) +extension DashboardViewController: NSSearchFieldDelegate { + func controlTextDidChange(_ obj: Notification) { + if let textField = obj.object as? NSTextField { + currentContentVC?.actionSearch(string: textField.stringValue) + } + } + + func searchFieldDidEndSearching(_ sender: NSSearchField) { + currentContentVC?.actionSearch(string: sender.stringValue) + } +} + +extension NSToolbarItem.Identifier { + static let toolbarSearchItem = NSToolbarItem.Identifier("ToolbarSearchItem") + static let toolbarSegmentItem = NSToolbarItem.Identifier("toolbarSegmentItem") +} + +@available(macOS 10.15, *) +extension DashboardViewController: NSToolbarDelegate { + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.toolbarSegmentItem, .flexibleSpace, .toolbarSearchItem] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.toolbarSegmentItem, .flexibleSpace, .toolbarSearchItem] + } + + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + let item = NSToolbarItem(itemIdentifier: itemIdentifier) + if itemIdentifier == .toolbarSearchItem { + item.maxSize = NSSize(width: 200, height: 40) + searchField.sizeToFit() + item.view = searchField + } else if itemIdentifier == .toolbarSegmentItem { + if #available(macOS 11.0, *) { + item.isNavigational = true + } + item.minSize = CGSize(width: 300, height: 34) + item.view = segmentControl + } + + return item + } +} diff --git a/ClashX/ViewControllers/Connections/Requests/ConnectionsReq.swift b/ClashX/ViewControllers/Connections/Requests/ConnectionsReq.swift new file mode 100644 index 000000000..ad6169d43 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Requests/ConnectionsReq.swift @@ -0,0 +1,53 @@ +// +// ConnectionsReq.swift +// ClashX +// +// Created by yicheng on 2023/7/14. +// Copyright © 2023 west2online. All rights reserved. +// + +import Foundation +import Starscream + +@available(macOS 10.15, *) +class ConnectionsReq: WebSocketDelegate { + private var socket: WebSocket? + + let decoder = JSONDecoder() + var onSnapshotUpdate: ((ClashConnectionSnapShot) -> Void)? + init() { + if let url = URL(string: ConfigManager.apiUrl.appending("/connections")) { + socket = WebSocket(url: url) + } + for header in ApiRequest.authHeader() { + socket?.request.setValue(header.value, forHTTPHeaderField: header.name) + } + socket?.delegate = self + decoder.dateDecodingStrategy = .formatted(DateFormatter.js) + } + + func connect() { + socket?.connect() + } + + func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + if let data = text.data(using: .utf8) { + do { + let info = try decoder.decode(ClashConnectionSnapShot.self, from: data) + onSnapshotUpdate?(info) + } catch let err { + Logger.log("decode fail: \(err)", level: .warning) + } + } + } + + func websocketDidConnect(socket: Starscream.WebSocketClient) { + Logger.log("websocketDidConnect") + } + + func websocketDidDisconnect(socket: Starscream.WebSocketClient, error: Error?) { + Logger.log("websocketDidDisconnect: \(String(describing: error))", level: .warning) + } + + func websocketDidReceiveData(socket: Starscream.WebSocketClient, data: Data) {} +} diff --git a/ClashX/ViewControllers/Connections/Requests/StructedLogReq.swift b/ClashX/ViewControllers/Connections/Requests/StructedLogReq.swift new file mode 100644 index 000000000..e7624810d --- /dev/null +++ b/ClashX/ViewControllers/Connections/Requests/StructedLogReq.swift @@ -0,0 +1,147 @@ +// +// StructedLogReq.swift +// ClashX +// +// Created by yicheng on 2023/7/14. +// Copyright © 2023 west2online. All rights reserved. +// + +import Combine +import Foundation +import Starscream + +class StructedLog: Codable { + class Pairs: Codable { + let key: String + let value: String + } + + let time: String + let level: String + let message: String + let fields: [Pairs] + + func convertToConn() -> LogConn? { + let isTCP = message.starts(with: "[TCP]") + let isUDP = message.starts(with: "[UDP]") + if isTCP || isUDP { + let conn = LogConn() + conn.network = isTCP ? "tcp" : "udp" + for field in fields { + switch field.key { + case "lAddr": + conn.localAddr = field.value + case "rAddr": + conn.remoteAddr = field.value + case "mode": + conn.mode = field.value + case "rule": + conn.rule = field.value + case "proxy": + conn.proxy = field.value + case "rulePayload": + conn.rulePayload = field.value + case "error": + conn.error = field.value + default: + break + } + } + return conn + } + return nil + } +} + +class LogConn { + let time = Date() + var localAddr: String = "" + var remoteAddr: String = "" + var mode: String = "" + var rule: String = "" + var proxy: String = "" + var rulePayload: String = "" + var error: String = "" + var network: String = "" + + @available(macOS 10.15, *) + func toConn() -> ClashConnectionSnapShot.Connection { + let sourceInfos = localAddr.split(separator: ":") + let remoteInfos = remoteAddr.split(separator: ":") + let metaData = ClashConnectionSnapShot.MetaData(network: network, + type: "log", + sourceIP: String(sourceInfos.first ?? ""), + destinationIP: String(remoteInfos.first ?? ""), + sourcePort: String(sourceInfos.last ?? ""), + destinationPort: String(remoteInfos.last ?? ""), + host: String(remoteInfos.first ?? ""), + dnsMode: "", + specialProxy: nil, + processPath: "") + + let conn = ClashConnectionSnapShot.Connection(id: UUID().uuidString, chains: [proxy], meta: metaData, upload: 0, download: 0, start: time, rule: rule, rulePayload: rulePayload) + if !error.isEmpty { + conn.status = .fail + conn.error = error + } else { + conn.status = .finished + } + return conn + } +} + +@available(macOS 10.15, *) +class StructedLogReq: WebSocketDelegate { + /* + {"time":"22:44:32","level":"warn","message":"[TCP] dial failed", + "fields":[{"key":"error","value":"dial tcp4 1.2.4.6:80: i/o timeout"}, + {"key":"proxy","value":"Domestic"}, + {"key":"lAddr","value":"127.0.0.1:49790"}, + {"key":"rAddr","value":"1.2.4.6:80"}, + {"key":"rule","value":"GeoIP"}, + {"key":"rulePayload","value":"CN"}] + } + {"time":"22:42:09","level":"debug","message":"[TUN] hijack udp dns","fields":[{"key":"addr","value":"198.18.0.2:53"}]} + */ + let logLevel = ClashLogLevel.info + private var socket: WebSocket? + + let decoder = JSONDecoder() + + let onLogUpdate = PassthroughSubject() + init(level: ClashLogLevel = .warning) { + if let url = URL(string: ConfigManager.apiUrl.appending("/logs?format=structured&level=\(logLevel.rawValue)")) { + socket = WebSocket(url: url) + } + for header in ApiRequest.authHeader() { + socket?.request.setValue(header.value, forHTTPHeaderField: header.name) + } + socket?.delegate = self + decoder.dateDecodingStrategy = .formatted(DateFormatter.js) + } + + func connect() { + socket?.connect() + } + + func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + if let data = text.data(using: .utf8) { + do { + let info = try decoder.decode(StructedLog.self, from: data) + onLogUpdate.send(info) + } catch let err { + Logger.log("decode fail: \(err)", level: .warning) + } + } + } + + func websocketDidConnect(socket: Starscream.WebSocketClient) { + Logger.log("websocketDidConnect") + } + + func websocketDidDisconnect(socket: Starscream.WebSocketClient, error: Error?) { + Logger.log("websocketDidDisconnect: \(String(describing: error))", level: .warning) + } + + func websocketDidReceiveData(socket: Starscream.WebSocketClient, data: Data) {} +} diff --git a/ClashX/ViewControllers/Connections/ViewModels/ConnectionDetailViewModel.swift b/ClashX/ViewControllers/Connections/ViewModels/ConnectionDetailViewModel.swift new file mode 100644 index 000000000..3da6db5e6 --- /dev/null +++ b/ClashX/ViewControllers/Connections/ViewModels/ConnectionDetailViewModel.swift @@ -0,0 +1,93 @@ +// +// ConnectionDetailViewModel.swift +// ClashX +// +// Created by yicheng on 2023/7/8. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import Combine + +@available(macOS 10.15, *) +class ConnectionDetailViewModel { + @Published var processName = "" + @Published var processImage: NSImage? + @Published var remoteHost = "" + + @Published var entry = "" + @Published var networkType = "" + @Published var totalUpload = "" + @Published var totalDownload = "" + @Published var maxUpload = "" + @Published var maxDownload = "" + @Published var currentUpload = "" + @Published var currentDownload = "" + @Published var rule = "" + @Published var chain = "" + @Published var sourceIP = "" + @Published var destination = "" + @Published var applicationPath: String? = "" + @Published var otherText = "" + @Published var showCloseButton = false + + private var uuid = "" + var cancellable = Set() + + func accept(connection: ClashConnectionSnapShot.Connection?) { + cancellable.removeAll() + guard let connection else { return } + if let pid = connection.metadata.pid { + processName = "\(connection.metadata.processName ?? NSLocalizedString("Unknown", comment: "")) (\(pid))" + } else { + processName = connection.metadata.processName ?? NSLocalizedString("Unknown", comment: "") + } + uuid = connection.id + showCloseButton = connection.status == .connecting + processImage = connection.metadata.processImage + applicationPath = connection.metadata.processPath + let area = clash_getCountryForIp(connection.metadata.destinationIP.goStringBuffer()).toString() + let areaString = "\(flag(from: area))\(area)" + if connection.metadata.host.isEmpty { + remoteHost = "\(connection.metadata.destinationIP):\(connection.metadata.destinationPort) \(areaString)" + } else { + remoteHost = "\(connection.metadata.host):\(connection.metadata.destinationPort) \(areaString)" + } + + entry = connection.metadata.type + networkType = connection.metadata.network + + connection.$download.map { SpeedUtils.getNetString(for: $0) }.weakAssign(to: \.totalDownload, on: self).store(in: &cancellable) + connection.$upload.map { SpeedUtils.getNetString(for: $0) }.weakAssign(to: \.totalUpload, on: self).store(in: &cancellable) + + connection.$maxUploadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.maxUpload, on: self).store(in: &cancellable) + connection.$maxDownloadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.maxDownload, on: self).store(in: &cancellable) + + connection.$uploadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.currentUpload, on: self).store(in: &cancellable) + connection.$downloadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.currentDownload, on: self).store(in: &cancellable) + + rule = connection.rule + "\n" + connection.rulePayload + chain = connection.chains.joined(separator: "\n") + sourceIP = connection.metadata.sourceIP.appending(":").appending(connection.metadata.sourcePort) + destination = connection.metadata.destinationIP.appending(":").appending(connection.metadata.destinationPort) + if let error = connection.error { + otherText = error + } else { + otherText = "" + } + } + + func flag(from country: String) -> String { + if country.isEmpty { return "" } + let base: UInt32 = 127397 + var s = "" + for v in country.uppercased().unicodeScalars { + s.unicodeScalars.append(UnicodeScalar(base + v.value)!) + } + return s + } + + func closeConnection() { + ApiRequest.closeConnection(uuid) + } +} diff --git a/ClashX/ViewControllers/Connections/ViewModels/ConnectionLeftPannelViewModel.swift b/ClashX/ViewControllers/Connections/ViewModels/ConnectionLeftPannelViewModel.swift new file mode 100644 index 000000000..2bd28c2ee --- /dev/null +++ b/ClashX/ViewControllers/Connections/ViewModels/ConnectionLeftPannelViewModel.swift @@ -0,0 +1,110 @@ +// +// ConnectionLeftPannelViewModel.swift +// ClashX +// +// Created by miniLV on 2023-07-10. +// Copyright © 2023 west2online. All rights reserved. +// + +import Combine + +@available(macOS 10.15, *) +class ConnectionLeftPannelViewModel { + enum Section: Int, CaseIterable { + case all + case local + case remote + case hosts + } + + private(set) var currentSections = [Section.all, Section.local, Section.remote] + private(set) var localApplications = [ConnectionApplication]() + private(set) var sources = [String]() + private(set) var hosts = [String]() + private(set) var isHostMode = false + var onReloadTable: ((IndexPath) -> Void)? + var onSelectedFilter: ((ConnectionFilter?) -> Void)? + var selectedFilter: ConnectionFilter? + + func accept(connections new: [ConnectionApplication]) { + var dupSet = Set() + localApplications = new.filter { dupSet.insert($0.path ?? $0.pid).inserted } + .sorted(by: { $0.name ?? "" < $1.name ?? "" }) + if !isHostMode { + onReloadTable?(getSelectedIndexPath()) + } + } + + func accept(sources new: [String]) { + sources = new.sorted() + if !isHostMode { + onReloadTable?(getSelectedIndexPath()) + } + } + + func accept(hosts new: [String]) { + hosts = new.sorted() + if isHostMode { + onReloadTable?(getSelectedIndexPath()) + } + } + + func accept(apps: [ConnectionApplication], sources: [String], hosts: [String]) { + var dupSet = Set() + localApplications = apps.filter { dupSet.insert($0.path ?? $0.pid).inserted } + .sorted(by: { $0.name ?? "" < $1.name ?? "" }) + self.sources = sources.sorted() + self.hosts = hosts.sorted() + onReloadTable?(getSelectedIndexPath()) + } + + func setHostMode(enable: Bool) { + isHostMode = enable + selectedFilter = nil + onSelectedFilter?(nil) + currentSections = enable ? [.all, .hosts] : [.all, .local, .remote] + onReloadTable?(getSelectedIndexPath()) + } + + func getSelectedIndexPath() -> IndexPath { + switch selectedFilter { + case .none: + break + case let .application(path): + if let idx = localApplications.firstIndex(where: { ($0.path ?? $0.pid) == path }) { + return IndexPath(item: idx, section: 1) + } + case let .source(ip): + if let idx = sources.firstIndex(where: { $0 == ip }) { + return IndexPath(item: idx, section: 2) + } + case let .hosts(name): + if let idx = hosts.firstIndex(where: { $0 == name }) { + return IndexPath(item: idx, section: 1) + } + } + return IndexPath(item: 0, section: 0) + } + + func setSelect(indexPath: IndexPath) { + if indexPath.item < 0 || indexPath.section < 0 { + selectedFilter = nil + onSelectedFilter?(nil) + return + } + + let type = currentSections[indexPath.section] + switch type { + case Section.local: + let app = localApplications[indexPath.item] + selectedFilter = .application(path: app.path ?? app.pid) + case Section.remote: + selectedFilter = .source(ip: sources[indexPath.item]) + case .hosts: + selectedFilter = .hosts(name: hosts[indexPath.item]) + case .all: + selectedFilter = nil + } + onSelectedFilter?(selectedFilter) + } +} diff --git a/ClashX/ViewControllers/Connections/ViewModels/ConnectionTopListViewModel.swift b/ClashX/ViewControllers/Connections/ViewModels/ConnectionTopListViewModel.swift new file mode 100644 index 000000000..dd5e8b7bc --- /dev/null +++ b/ClashX/ViewControllers/Connections/ViewModels/ConnectionTopListViewModel.swift @@ -0,0 +1,120 @@ +// +// ConnectionTopListViewModel.swift +// ClashX +// +// Created by yicheng on 2023/7/8. +// Copyright © 2023 west2online. All rights reserved. +// + +import Combine + +@available(macOS 10.15, *) +class ConnectionTopListViewModel { + private var fullConnections = [ClashConnectionSnapShot.Connection]() + private(set) var connections = [ClashConnectionSnapShot.Connection]() + private var selectedUUIDs = [String]() + private var updateDebounceDate = Date() + + var onReloadTable: (() -> Void)? + var onSelectedConnection: ((ClashConnectionSnapShot.Connection?) -> Void)? + var applicationFilter: ConnectionFilter? { + didSet { + updateData() + } + } + + var textFilter: String? { + didSet { + updateData() + } + } + + var currentSortDescriptor: NSSortDescriptor? { + didSet { + updateData() + } + } + + func accept(connections new: [ClashConnectionSnapShot.Connection]) { + fullConnections = new + updateData() + } + + func connectionDidUpdate() { + if let key = currentSortDescriptor?.key, ConnectionColume.isDynamicSort(for: key) { + updateData(applyDebounce: true) + } + } + + func currentSelection() -> IndexSet { + let indexs = selectedUUIDs.compactMap { uuid in connections.firstIndex(where: { $0.id == uuid }) } + return IndexSet(indexs) + } + + func closeConnection(for indexs: IndexSet) { + for idx in indexs { + let conn = connections[idx] + ApiRequest.closeConnection(conn.id) + } + } + + private func updateData(applyDebounce: Bool = false) { + let current = Date() + if applyDebounce, current.timeIntervalSince(updateDebounceDate) < 0.2 { + return + } + updateDebounceDate = current + connections = fullConnections + + switch applicationFilter { + case .none: + break + case let .application(pathOrPid): + connections = connections.filter { conn in + conn.metadata.processPath == pathOrPid || conn.metadata.pid == pathOrPid + } + + case let .source(ip): + connections = connections.filter { conn in + conn.metadata.sourceIP == ip + } + case let .hosts(name): + connections = connections.filter { conn in + conn.metadata.displayHost == name + } + } + + if let textFilter = textFilter?.lowercased(), !textFilter.isEmpty { + connections = connections.filter { conn in + conn.metadata.displayHost.contains(textFilter) || + conn.metadata.network.contains(textFilter) || + conn.chains.joined().lowercased().contains(textFilter) || + conn.metadata.processName?.lowercased().contains(textFilter) ?? false || + conn.rule.lowercased().contains(textFilter) + } + } + + connections = (connections as NSArray).sortedArray(using: [currentSortDescriptor].compactMap { $0 }) as! [ClashConnectionSnapShot.Connection] + onReloadTable?() + } + + func setSelect(row: IndexSet) { + selectedUUIDs = row.map { connections[$0].id } + if selectedUUIDs.count == 1, let idx = row.first { + onSelectedConnection?(connections[idx]) + } else { + onSelectedConnection?(nil) + } + } + + func sortSortDescriptor(for columnType: ConnectionColume) -> NSSortDescriptor? { + if let keypath = columnType.compareKeyPath { + let sort = NSSortDescriptor(key: keypath, ascending: true) + if columnType == .date { + currentSortDescriptor = sort + } + return sort + } + return nil + } +} diff --git a/ClashX/ViewControllers/Connections/ViewModels/ConnectionsViewModel.swift b/ClashX/ViewControllers/Connections/ViewModels/ConnectionsViewModel.swift new file mode 100644 index 000000000..6e3686f99 --- /dev/null +++ b/ClashX/ViewControllers/Connections/ViewModels/ConnectionsViewModel.swift @@ -0,0 +1,190 @@ +// +// ConnectionsViewModel.swift +// ClashX +// +// Created by yicheng on 2023/7/5. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import Combine +import Foundation + +struct ConnectionApplication { + let pid: String + let image: NSImage? + let name: String? + let path: String? +} + +@available(macOS 10.15, *) +class ConnectionsViewModel { + @Published private(set) var applicationMap = [String: ConnectionApplication]() + @Published private(set) var connections = [String: ClashConnectionSnapShot.Connection]() + @Published var selectedConnection: ClashConnectionSnapShot.Connection? { + didSet { + showBottomView = selectedConnection != nil + } + } + + @Published private(set) var sourceIPs = Set() + @Published private(set) var hosts = Set() + @Published var showBottomView = false + let connectionDataDidRefresh = PassthroughSubject() + var activeOnlyMode = false { + didSet { + if activeOnlyMode, let currentSnapShot { + updateForActiveMode(snapShot: currentSnapShot) + } + } + } + + private let unknownApplicationPlaceHolder = ConnectionApplication(pid: "-1", image: nil, name: NSLocalizedString("Unknown", comment: ""), path: "") + + private var cancellable = Set() + private(set) var currentApplications = [ConnectionApplication]() + private(set) var currentSourceIPs = [String]() + private(set) var currentHosts = [String]() + private(set) var currentConnections = [ClashConnectionSnapShot.Connection]() + private var currentSnapShot: ClashConnectionSnapShot? + private let req = ConnectionsReq() + private let logReq = StructedLogReq() + private var verifyConnList = [LogConn]() + init() { + req.connect() + logReq.connect() + req.onSnapshotUpdate = { + [weak self] snap in + self?.update(snapShot: snap) + } + logReq.onLogUpdate.compactMap { $0.convertToConn() }.sink { [weak self] conn in + self?.verifyConnList.append(conn) + }.store(in: &cancellable) + } + + func update(snapShot: ClashConnectionSnapShot) { + currentSnapShot = snapShot + defer { + connectionDataDidRefresh.send() + } + let keys = Set(snapShot.connections.map(\.id)) + for key in connections.keys where !keys.contains(key) { + if let conn = connections[key] { + if conn.status == .connecting { + conn.status = .finished + } + conn.uploadSpeed = 0 + conn.downloadSpeed = 0 + } + } + let lAddrs = Set(snapShot.connections.map { $0.metadata.sourceIP.appending(":").appending($0.metadata.sourcePort) }) + + for logCon in verifyConnList where !lAddrs.contains(logCon.localAddr) { + snapShot.connections.append(logCon.toConn()) + } + var processMap: [String: String]? + for conn in snapShot.connections { + if let oldConn = connections[conn.id] { + oldConn.uploadSpeed = conn.upload - oldConn.upload + oldConn.downloadSpeed = conn.download - oldConn.download + oldConn.upload = conn.upload + oldConn.download = conn.download + } else { + if processMap == nil { + processMap = getProcessList() + } + let key = conn.metadata.sourceIP.appending(conn.metadata.sourcePort) + if let pid = processMap![key], + let info = getProgressInfo(pid: pid) { + conn.metadata.pid = pid + conn.metadata.processPath = info.path ?? "" + conn.metadata.processName = info.name + conn.metadata.processImage = info.image + } else if !conn.metadata.processPath.isEmpty { + conn.metadata.processName = conn.metadata.processPath.components(separatedBy: "/").last + conn.metadata.processImage = NSWorkspace.shared.icon(forFile: conn.metadata.processPath) + } else { + if applicationMap["-1"] == nil { + applicationMap["-1"] = unknownApplicationPlaceHolder + } + conn.metadata.pid = unknownApplicationPlaceHolder.pid + conn.metadata.processName = unknownApplicationPlaceHolder.name + } + + connections[conn.id] = conn + if !sourceIPs.contains(conn.metadata.sourceIP) { + sourceIPs.insert(conn.metadata.sourceIP) + } + if !hosts.contains(conn.metadata.displayHost) { + hosts.insert(conn.metadata.displayHost) + } + } + } + + if activeOnlyMode { + updateForActiveMode(snapShot: snapShot) + } + + verifyConnList.removeAll() + } + + private func updateForActiveMode(snapShot: ClashConnectionSnapShot) { + currentConnections = snapShot.connections.compactMap { [weak self] in self?.connections[$0.id] } + currentHosts = Array(Set(currentConnections.map(\.metadata.displayHost))) + currentSourceIPs = Array(Set(currentConnections.map(\.metadata.sourceIP))) + currentApplications = Array(Set(currentConnections.compactMap(\.metadata.pid)).compactMap { [weak self] in self?.applicationMap[$0] }) + } + + private func getProcessList() -> [String: String] { + let tableString: String = clash_getProggressInfo().toString().trimmingCharacters(in: .whitespacesAndNewlines) + if tableString.isEmpty { return [:] } + let processList = tableString.components(separatedBy: "\n") + var map = [String: String]() + for process in processList { + let infos = process.components(separatedBy: " ") + // fmt.Sprintf("%s %d %d\n", srcIP, srcPort, pid) + let srcIp = infos[0] + let srcPort = infos[1] + let pid = infos[2] + map[srcIp.appending(srcPort)] = pid + } + return map + } + + private func getProcessPath(pid: Int32) -> String? { + let pathBuffer = UnsafeMutablePointer.allocate(capacity: Int(MAXPATHLEN)) + defer { + pathBuffer.deallocate() + } + let pathLength = proc_pidpath(pid, pathBuffer, UInt32(MAXPATHLEN)) + if pathLength > 0 { + let path = String(cString: pathBuffer) + return path + } + return nil + } + + private func getProgressInfo(pid: String) -> ConnectionApplication? { + if let info = applicationMap[pid] { + return info + } + guard let pidValue = Int32(pid) else { return nil } + + if let application = NSRunningApplication(processIdentifier: pidValue) { + let info = ConnectionApplication(pid: pid, + image: application.icon, + name: application.localizedName, + path: application.executableURL?.absoluteString) + applicationMap[pid] = info + return info + } + + guard let path = getProcessPath(pid: pidValue) else { return nil } + let info = ConnectionApplication(pid: pid, + image: NSWorkspace.shared.icon(forFile: path), + name: path.components(separatedBy: "/").last, + path: path) + applicationMap[pid] = info + return info + } +} diff --git a/ClashX/ViewControllers/Connections/Views/Base.lproj/ConnectionDetailInfoGeneralView.xib b/ClashX/ViewControllers/Connections/Views/Base.lproj/ConnectionDetailInfoGeneralView.xib new file mode 100644 index 000000000..975b16f2b --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/Base.lproj/ConnectionDetailInfoGeneralView.xib @@ -0,0 +1,713 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ClashX/ViewControllers/Connections/Views/Cell/ConnectionCellProtocol.swift b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionCellProtocol.swift new file mode 100644 index 000000000..5e2be30da --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionCellProtocol.swift @@ -0,0 +1,14 @@ +// +// ConnectionCellProtocol.swift +// ClashX +// +// Created by yicheng on 2023/7/6. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit + +@available(macOS 10.15, *) +protocol ConnectionCellProtocol: NSView { + func setup(with connection: ClashConnectionSnapShot.Connection, type: ConnectionColume) +} diff --git a/ClashX/ViewControllers/Connections/Views/Cell/ConnectionLeftTextCellView.swift b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionLeftTextCellView.swift new file mode 100644 index 000000000..082c46df1 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionLeftTextCellView.swift @@ -0,0 +1,113 @@ +// +// ConnectionLeftTextCellView.swift +// ClashX +// +// Created by miniLV on 2023-07-10. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit + +@available(macOS 10.15, *) +class ConnectionApplicationCellView: NSView { + let imageView = NSImageView() + let nameLabel = NSTextField(labelWithString: "") + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + func setupUI() { + addSubview(nameLabel) + addSubview(imageView) + nameLabel.font = NSFont.systemFont(ofSize: 13) + imageView.makeConstraints { + [$0.heightAnchor.constraint(equalToConstant: 23), + $0.widthAnchor.constraint(equalTo: $0.heightAnchor), + $0.centerYAnchor.constraint(equalTo: centerYAnchor), + $0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6)] + } + nameLabel.makeConstraints { + [ + $0.centerYAnchor.constraint(equalTo: centerYAnchor), + $0.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 5), + $0.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) + ] + } + nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + nameLabel.cell?.truncatesLastVisibleLine = true + } + + func setup(with connection: ConnectionApplication) { + nameLabel.stringValue = connection.name ?? NSLocalizedString("Unknown", comment: "") + imageView.image = connection.image + } +} + +class ConnectionLeftTextCellView: NSView { + let nameLabel = NSTextField(labelWithString: "") + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + func setupUI() { + addSubview(nameLabel) + nameLabel.font = NSFont.systemFont(ofSize: 13) + nameLabel.makeConstraints { + [ + $0.centerYAnchor.constraint(equalTo: centerYAnchor), + $0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6), + $0.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) + ] + } + nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + nameLabel.cell?.truncatesLastVisibleLine = true + } + + func setup(with text: String) { + nameLabel.stringValue = text + } +} + +class ApplicationClientSectionCell: NSTableCellView { + let titleLabel = NSTextField(labelWithString: "") + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + func setupUI() { + addSubview(titleLabel) + titleLabel.font = NSFont.systemFont(ofSize: 10) + titleLabel.textColor = NSColor.secondaryLabelColor + titleLabel.makeConstraints { + [ + $0.centerYAnchor.constraint(equalTo: centerYAnchor), + $0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0), + $0.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) + ] + } + titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + titleLabel.cell?.truncatesLastVisibleLine = true + } + + func setup(with title: String) { + titleLabel.stringValue = title + } +} diff --git a/ClashX/ViewControllers/Connections/Views/Cell/ConnectionProxyClientCellView.swift b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionProxyClientCellView.swift new file mode 100644 index 000000000..854db6837 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionProxyClientCellView.swift @@ -0,0 +1,50 @@ +// +// ConnectionProxyClientCellView.swift +// ClashX +// +// Created by yicheng on 2023/7/6. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit + +@available(macOS 10.15, *) +class ConnectionProxyClientCellView: NSView, ConnectionCellProtocol { + let imageView = NSImageView() + let nameLabel = NSTextField(labelWithString: "") + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + func setupUI() { + addSubview(nameLabel) + addSubview(imageView) + nameLabel.font = NSFont.systemFont(ofSize: 12) + imageView.makeConstraints { + [$0.heightAnchor.constraint(equalToConstant: 18), + $0.widthAnchor.constraint(equalTo: $0.heightAnchor), + $0.centerYAnchor.constraint(equalTo: centerYAnchor), + $0.leadingAnchor.constraint(equalTo: leadingAnchor)] + } + nameLabel.makeConstraints { + [ + $0.centerYAnchor.constraint(equalTo: centerYAnchor), + $0.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 4), + $0.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) + ] + } + nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + nameLabel.cell?.truncatesLastVisibleLine = true + } + + func setup(with connection: ClashConnectionSnapShot.Connection, type: ConnectionColume) { + nameLabel.stringValue = connection.metadata.processName ?? NSLocalizedString("Unknown", comment: "") + imageView.image = connection.metadata.processImage + } +} diff --git a/ClashX/ViewControllers/Connections/Views/Cell/ConnectionStatusIconCellView.swift b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionStatusIconCellView.swift new file mode 100644 index 000000000..e4b479535 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionStatusIconCellView.swift @@ -0,0 +1,49 @@ +// +// ConnectionStatusIconCellView.swift +// ClashX +// +// Created by yicheng on 2023/7/6. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import Combine + +@available(macOS 10.15, *) +class ConnectionStatusIconCellView: NSView, ConnectionCellProtocol { + let imageView = NSImageView() + var cancellable = Set() + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + func setupUI() { + addSubview(imageView) + imageView.makeConstraints { + [$0.heightAnchor.constraint(equalToConstant: 18), + $0.widthAnchor.constraint(equalTo: $0.heightAnchor), + $0.centerYAnchor.constraint(equalTo: centerYAnchor), + $0.leadingAnchor.constraint(equalTo: leadingAnchor)] + } + } + + func setup(with connection: ClashConnectionSnapShot.Connection, type: ConnectionColume) { + cancellable.removeAll() + connection + .$status + .map(\.image) + .weakAssign(to: \.image, on: imageView) + .store(in: &cancellable) + } + + override func prepareForReuse() { + super.prepareForReuse() + cancellable.removeAll() + } +} diff --git a/ClashX/ViewControllers/Connections/Views/Cell/ConnectionTextCellView.swift b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionTextCellView.swift new file mode 100644 index 000000000..e377be807 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/Cell/ConnectionTextCellView.swift @@ -0,0 +1,66 @@ +// +// ConnectionTextCellView.swift +// ClashX +// +// Created by yicheng on 2023/7/6. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import Combine + +@available(macOS 10.15, *) +class ConnectionTextCellView: NSView, ConnectionCellProtocol { + let label = NSTextField(labelWithString: "") + var cancellable = Set() + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + func setupUI() { + clipsToBounds = true + addSubview(label) + label.font = NSFont.systemFont(ofSize: 12) + label.makeConstraints { + [$0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0), + $0.centerYAnchor.constraint(equalTo: centerYAnchor)] + } + } + + func setup(with connection: ClashConnectionSnapShot.Connection, type: ConnectionColume) { + cancellable.removeAll() + switch type { + case .upload: + connection.$upload.map { SpeedUtils.getNetString(for: $0) }.weakAssign(to: \.stringValue, on: label).store(in: &cancellable) + case .download: + connection.$download.map { SpeedUtils.getNetString(for: $0) }.weakAssign(to: \.stringValue, on: label).store(in: &cancellable) + case .currentUpload: + connection.$uploadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.stringValue, on: label).store(in: &cancellable) + case .currentDownload: + connection.$downloadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.stringValue, on: label).store(in: &cancellable) + case .status: + connection.$status.map(\.title).weakAssign(to: \.stringValue, on: label).store(in: &cancellable) + case .statusIcon, .process: + return + case .rule: + label.stringValue = connection.chains.joined(separator: "/") + case .date: + label.stringValue = DateFormatter.simple.string(from: connection.start) + case .url: + label.stringValue = connection.metadata.displayHost + case .type: + label.stringValue = connection.metadata.network + } + } + + override func prepareForReuse() { + super.prepareForReuse() + cancellable.removeAll() + } +} diff --git a/ClashX/ViewControllers/Connections/Views/ConnectionColume.swift b/ClashX/ViewControllers/Connections/Views/ConnectionColume.swift new file mode 100644 index 000000000..53833cd46 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/ConnectionColume.swift @@ -0,0 +1,86 @@ +// +// ConnectionColume.swift +// ClashX +// +// Created by yicheng on 2023/7/6. +// Copyright © 2023 west2online. All rights reserved. +// + +enum ConnectionFilter { + case application(path: String) + case source(ip: String) + case hosts(name: String) +} + +@available(macOS 10.15, *) +enum ConnectionColume: String, CaseIterable { + case statusIcon + case process + case status + case date + case url + case rule + case currentUpload + case currentDownload + case upload + case download + case type + + var columeTitle: String { + switch self { + case .statusIcon: return "" + case .process: return NSLocalizedString("Client", comment: "") + case .status: return NSLocalizedString("Status", comment: "") + case .rule: return NSLocalizedString("Rule", comment: "") + case .url: return NSLocalizedString("Host", comment: "") + case .date: return NSLocalizedString("Date", comment: "") + case .upload: return NSLocalizedString("Upload", comment: "") + case .download: return NSLocalizedString("Download", comment: "") + case .currentUpload: return NSLocalizedString("Upload speed", comment: "") + case .currentDownload: return NSLocalizedString("Download speed", comment: "") + case .type: return NSLocalizedString("Type", comment: "") + } + } + + var compareKeyPath: String? { + switch self { + case .statusIcon, .status: return "status" + case .process: return "metadata.processName" + case .rule: return "rule" + case .url: return "metadata.displayHost" + case .date: return "start" + case .upload: return "upload" + case .download: return "download" + case .currentUpload: return "uploadSpeed" + case .currentDownload: return "downloadSpeed" + case .type: return "metadata.network" + } + } + + static func isDynamicSort(for keypath: String) -> Bool { + return keypath == "upload" || keypath == "download" || keypath == "uploadSpeed" || keypath == "downloadSpeed" || keypath == "done" + } + + var minWidth: CGFloat { + switch self { + case .statusIcon: return 16 + case .status: return 30 + default: return 60 + } + } + + var width: CGFloat { + switch self { + case .upload, .download, .currentUpload, .currentDownload: return 80 + case .status: return 50 + default: return 100 + } + } + + var maxWidth: CGFloat { + switch self { + case .statusIcon: return 16 + default: return CGFloat.greatestFiniteMagnitude + } + } +} diff --git a/ClashX/ViewControllers/Connections/Views/ConnectionDetailInfoGeneralView.swift b/ClashX/ViewControllers/Connections/Views/ConnectionDetailInfoGeneralView.swift new file mode 100644 index 000000000..f6abcac6f --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/ConnectionDetailInfoGeneralView.swift @@ -0,0 +1,31 @@ +// +// ConnectionDetailInfoGeneralView.swift +// ClashX +// +// Created by yicheng on 2023/7/8. +// Copyright © 2023 west2online. All rights reserved. +// + +import Cocoa + +class ConnectionDetailInfoGeneralView: NSView, NibLoadable { + @IBOutlet var entryLabel: NSTextField! + @IBOutlet var networkTypeLabel: NSTextField! + @IBOutlet var totalUploadLabel: NSTextField! + @IBOutlet var totalDownloadLabel: NSTextField! + @IBOutlet var maxUploadLabel: NSTextField! + @IBOutlet var maxDownloadLabel: NSTextField! + @IBOutlet var currentUploadLabel: NSTextField! + @IBOutlet var currentDownloadLabel: NSTextField! + + @IBOutlet var ruleLabel: NSTextField! + @IBOutlet var proxyChainLabel: NSTextField! + @IBOutlet var otherTextView: NSTextView! + @IBOutlet var sourceIpLabel: NSTextField! + @IBOutlet var destLabel: NSTextField! + override func awakeFromNib() { + super.awakeFromNib() + otherTextView.backgroundColor = NSColor.clear + otherTextView.font = NSFont.systemFont(ofSize: 10) + } +} diff --git a/ClashX/ViewControllers/Connections/Views/ConnectionDetailInfoView.swift b/ClashX/ViewControllers/Connections/Views/ConnectionDetailInfoView.swift new file mode 100644 index 000000000..41fb47f7e --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/ConnectionDetailInfoView.swift @@ -0,0 +1,155 @@ +// +// ConnectionDetailInfoView.swift +// ClashX +// +// Created by yicheng on 2023/7/5. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import Combine + +@available(macOS 10.15, *) +class ConnectionDetailInfoView: NSView { + private let logoView = NSImageView() + private let processNameLabel = NSTextField(labelWithString: "") + private let hostLabel = NSTextField(labelWithString: "") + private let generalView = ConnectionDetailInfoGeneralView.createFromNib() + private let containerView = NSView() + private var cancelable = Set() + private let closeButton = NSButton() + private var viewModel: ConnectionDetailViewModel? + init() { + super.init(frame: .zero) + wantsLayer = true + updateColor() + setupSubviews() + } + + override func viewDidChangeEffectiveAppearance() { + super.viewDidChangeEffectiveAppearance() + updateColor() + } + + func updateColor() { + if #available(macOS 11.0, *) { + effectiveAppearance.performAsCurrentDrawingAppearance { + layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor + } + } else { + let pervious = NSAppearance.current + NSAppearance.current = effectiveAppearance + layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor + NSAppearance.current = pervious + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupSubviews() { + addSubview(logoView) + logoView.makeConstraints { + [$0.topAnchor.constraint(equalTo: topAnchor, constant: 16), + $0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + $0.widthAnchor.constraint(equalTo: $0.heightAnchor), + $0.widthAnchor.constraint(equalToConstant: 36)] + } + let nameStackView = NSStackView() + nameStackView.orientation = .vertical + nameStackView.addArrangedSubview(processNameLabel) + nameStackView.addArrangedSubview(hostLabel) + nameStackView.alignment = .left + nameStackView.spacing = 2 + addSubview(nameStackView) + nameStackView.makeConstraints { + [$0.centerYAnchor.constraint(equalTo: logoView.centerYAnchor), + $0.leadingAnchor.constraint(equalTo: logoView.trailingAnchor, constant: 10)] + } + + processNameLabel.font = NSFont.systemFont(ofSize: 17, weight: .bold) + hostLabel.font = NSFont.systemFont(ofSize: 12) + /* + let segmentControl = NSSegmentedControl(labels: ["General", "Event"], trackingMode: .selectOne, target: self, action: #selector(actionSelectSegment(sender: ))) + addSubview(segmentControl) + segmentControl.makeConstraints { + [$0.leftAnchor.constraint(equalTo: logoView.centerXAnchor), + $0.topAnchor.constraint(equalTo: nameStackView.bottomAnchor, constant: 12)] + } + segmentControl.selectedSegment = 0 + */ + closeButton.title = NSLocalizedString("Close Connection", comment: "") + closeButton.bezelStyle = .regularSquare + closeButton.target = self + closeButton.action = #selector(actionCloseConn) + addSubview(closeButton) + closeButton.makeConstraints { [ + $0.centerYAnchor.constraint(equalTo: nameStackView.centerYAnchor), + $0.rightAnchor.constraint(equalTo: rightAnchor, constant: -12) + ] } + + let separator = NSView() + separator.wantsLayer = true + separator.layer?.backgroundColor = NSColor.separatorColor.cgColor + addSubview(separator) + separator.makeConstraints { + [$0.heightAnchor.constraint(equalToConstant: 1), + $0.leftAnchor.constraint(equalTo: logoView.centerXAnchor), + $0.rightAnchor.constraint(equalTo: rightAnchor), + $0.centerYAnchor.constraint(equalTo: nameStackView.bottomAnchor, constant: 12)] + } + + addSubview(containerView) + containerView.makeConstraints { + [$0.leftAnchor.constraint(equalTo: separator.leftAnchor), + $0.rightAnchor.constraint(equalTo: rightAnchor), + $0.bottomAnchor.constraint(equalTo: bottomAnchor), + $0.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: 16)] + } + + addGeneralView() + } + + func setup(with viewModel: ConnectionDetailViewModel) { + cancelable.removeAll() + self.viewModel = viewModel + viewModel.$processName.weakAssign(to: \.stringValue, on: processNameLabel).store(in: &cancelable) + viewModel.$applicationPath.weakAssign(to: \.toolTip, on: processNameLabel).store(in: &cancelable) + viewModel.$applicationPath.weakAssign(to: \.toolTip, on: logoView).store(in: &cancelable) + viewModel.$remoteHost.weakAssign(to: \.stringValue, on: hostLabel).store(in: &cancelable) + viewModel.$processImage.weakAssign(to: \.image, on: logoView).store(in: &cancelable) + viewModel.$entry.weakAssign(to: \.stringValue, on: generalView.entryLabel).store(in: &cancelable) + viewModel.$networkType.weakAssign(to: \.stringValue, on: generalView.networkTypeLabel).store(in: &cancelable) + + viewModel.$totalUpload.weakAssign(to: \.stringValue, on: generalView.totalUploadLabel).store(in: &cancelable) + viewModel.$totalDownload.weakAssign(to: \.stringValue, on: generalView.totalDownloadLabel).store(in: &cancelable) + + viewModel.$maxUpload.weakAssign(to: \.stringValue, on: generalView.maxUploadLabel).store(in: &cancelable) + viewModel.$maxDownload.weakAssign(to: \.stringValue, on: generalView.maxDownloadLabel).store(in: &cancelable) + + viewModel.$currentUpload.weakAssign(to: \.stringValue, on: generalView.currentUploadLabel).store(in: &cancelable) + viewModel.$currentDownload.weakAssign(to: \.stringValue, on: generalView.currentDownloadLabel).store(in: &cancelable) + + viewModel.$rule.weakAssign(to: \.stringValue, on: generalView.ruleLabel).store(in: &cancelable) + viewModel.$chain.weakAssign(to: \.stringValue, on: generalView.proxyChainLabel).store(in: &cancelable) + viewModel.$sourceIP.weakAssign(to: \.stringValue, on: generalView.sourceIpLabel).store(in: &cancelable) + + viewModel.$destination.weakAssign(to: \.stringValue, on: generalView.destLabel).store(in: &cancelable) + viewModel.$otherText.weakAssign(to: \.string, on: generalView.otherTextView).store(in: &cancelable) + + viewModel.$showCloseButton.map { !$0 }.weakAssign(to: \.isHidden, on: closeButton).store(in: &cancelable) + } + + @objc func actionSelectSegment(sender: NSSegmentedControl?) {} + @objc func actionCloseConn() { + viewModel?.closeConnection() + } + + func addGeneralView() { + containerView.subviews.forEach { $0.removeFromSuperview() } + containerView.addSubview(generalView) + generalView.makeConstraintsToBindToSuperview() + } +} diff --git a/ClashX/ViewControllers/Connections/Views/ConnectionLeftPannelView.swift b/ClashX/ViewControllers/Connections/Views/ConnectionLeftPannelView.swift new file mode 100644 index 000000000..2d8a9d4d8 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/ConnectionLeftPannelView.swift @@ -0,0 +1,194 @@ +// +// ConnectionLeftPannelView.swift +// ClashX +// +// Created by yicheng on 2023/7/5. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit + +private extension NSUserInterfaceItemIdentifier { + static let mainColumn = NSUserInterfaceItemIdentifier("mainColumn") + static let localApplication = NSUserInterfaceItemIdentifier("localApplication") + static let remoteApplication = NSUserInterfaceItemIdentifier("remoteApplication") + static let hosts = NSUserInterfaceItemIdentifier("hosts") + static let all = NSUserInterfaceItemIdentifier("all") +} + +@available(macOS 10.15, *) +class ConnectionLeftPannelView: NSView { + let viewModel: ConnectionLeftPannelViewModel + let columnIdentifier = NSUserInterfaceItemIdentifier(rawValue: "column") + let effectView = NSVisualEffectView() + + private let tableView: SectionedTableView = { + let table = SectionedTableView() + table.columnAutoresizingStyle = .uniformColumnAutoresizingStyle + table.backgroundColor = NSColor.clear + table.allowsColumnSelection = false + table.usesAutomaticRowHeights = true + table.translatesAutoresizingMaskIntoConstraints = false + return table + }() + + init(viewModel: ConnectionLeftPannelViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + setupSubviews() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupSubviews() { + addSubview(effectView) + effectView.makeConstraintsToBindToSuperview() + + let segmentControl = NSSegmentedControl(labels: [NSLocalizedString("Client", comment: ""), NSLocalizedString("Host", comment: "")], + trackingMode: .selectOne, + target: self, + action: #selector(actionSelectSegment(sender:))) + addSubview(segmentControl) + segmentControl.makeConstraints { [ + $0.leftAnchor.constraint(equalTo: leftAnchor, constant: 12), + $0.rightAnchor.constraint(equalTo: rightAnchor, constant: -12), + $0.heightAnchor.constraint(equalToConstant: 20), + $0.topAnchor.constraint(equalTo: topAnchor, constant: 12) + ] } + segmentControl.selectedSegment = 0 + + let v = NSScrollView() + v.drawsBackground = false + v.backgroundColor = .clear + v.contentView.documentView = tableView + addSubview(v) + v.makeConstraints { [ + $0.leftAnchor.constraint(equalTo: leftAnchor), + $0.rightAnchor.constraint(equalTo: rightAnchor), + $0.bottomAnchor.constraint(equalTo: bottomAnchor), + $0.topAnchor.constraint(equalTo: segmentControl.bottomAnchor, constant: 2) + ] } + v.hasHorizontalScroller = false + + let column = NSTableColumn(identifier: .mainColumn) + column.minWidth = 60 + column.maxWidth = .greatestFiniteMagnitude + tableView.addTableColumn(column) + tableView.backgroundColor = .clear + tableView.headerView = nil + tableView.intercellSpacing = .zero + tableView.sectionDatasource = self + tableView.allowsEmptySelection = false + tableView.sizeLastColumnToFit() + tableView.reloadData() + + viewModel.onReloadTable = { [weak self] in + guard let self else { return } + tableView.reloadData() + tableView.selectRow(at: $0) + } + } + + @objc func actionSelectSegment(sender: NSSegmentedControl?) { + viewModel.setHostMode(enable: sender?.selectedSegment == 1) + } +} + +// MARK: - section map to cell logic. + +@available(macOS 10.15, *) +private extension ConnectionLeftPannelView { + func getIdentifier(section: Int) -> NSUserInterfaceItemIdentifier { + var identifier = NSUserInterfaceItemIdentifier("") + switch viewModel.currentSections[section] { + case .local: + identifier = .localApplication + case .remote: + identifier = .remoteApplication + case .hosts: + identifier = .hosts + case .all: + identifier = .all + } + return identifier + } +} + +@available(macOS 10.15, *) +extension ConnectionLeftPannelView: TableViewSectionDataSource { + func numberOfSectionsInTableView(tableView: NSTableView) -> Int { + return viewModel.currentSections.count + } + + func tableView(tableView: NSTableView, numberOfRowsInSection section: Int) -> Int { + switch viewModel.currentSections[section] { + case .local: + return viewModel.localApplications.count + case .remote: + return viewModel.sources.count + case .hosts: + return viewModel.hosts.count + case .all: + return 1 + } + } + + func tableView(tableView: NSTableView, viewForHeaderInSection section: Int) -> NSView? { + switch viewModel.currentSections[section] { + case .local: + let sectionView = ApplicationClientSectionCell() + sectionView.setup(with: NSLocalizedString("Local Clients", comment: "")) + return sectionView + case .remote: + let sectionView = ApplicationClientSectionCell() + sectionView.setup(with: NSLocalizedString("Sources", comment: "")) + return sectionView + case .all: + let sectionView = ApplicationClientSectionCell() + sectionView.setup(with: NSLocalizedString("Requests", comment: "")) + return sectionView + case .hosts: + let sectionView = ApplicationClientSectionCell() + sectionView.setup(with: NSLocalizedString("Hosts", comment: "")) + return sectionView + } + } + + func tableView(tableView: NSTableView, viewForRowAt indexPath: IndexPath, column: NSTableColumn) -> NSView? { + let type = viewModel.currentSections[indexPath.section] + let identifier = getIdentifier(section: indexPath.section) + var view = tableView.makeView(withIdentifier: identifier, owner: self) + if view == nil { + switch type { + case .local: + view = ConnectionApplicationCellView() + case .remote, .all, .hosts: + view = ConnectionLeftTextCellView() + } + view?.identifier = identifier + } + + switch type { + case .local: + (view as! ConnectionApplicationCellView).setup(with: viewModel.localApplications[indexPath.item]) + case .remote: + (view as! ConnectionLeftTextCellView).setup(with: viewModel.sources[indexPath.item]) + case .all: + (view as! ConnectionLeftTextCellView).setup(with: viewModel.isHostMode ? NSLocalizedString("All Hosts", comment: "") : NSLocalizedString("All Clients", comment: "")) + case .hosts: + (view as! ConnectionLeftTextCellView).setup(with: viewModel.hosts[indexPath.item]) + } + return view + } + + func tableView(tableView: NSTableView, didSelectRowAtIndexPath indexPath: IndexPath) { + viewModel.setSelect(indexPath: indexPath) + } + + func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + return 36 + } +} diff --git a/ClashX/ViewControllers/Connections/Views/ConnectionTopListView.swift b/ClashX/ViewControllers/Connections/Views/ConnectionTopListView.swift new file mode 100644 index 000000000..db9e10b8b --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/ConnectionTopListView.swift @@ -0,0 +1,131 @@ +// +// ConnectionTopListView.swift +// ClashX +// +// Created by yicheng on 2023/7/5. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit + +@available(macOS 10.15, *) +class ConnectionTopListView: NSView { + private let viewModel: ConnectionTopListViewModel + + private let tableView: NSTableView = { + let table = NSTableView() + table.allowsColumnSelection = false + return table + }() + + let closeConnectionMenuItem = NSMenuItem() + + init(viewModel: ConnectionTopListViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + setupSubviews() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupSubviews() { + let v = NSScrollView() + v.contentView.documentView = tableView + addSubview(v) + v.makeConstraintsToBindToSuperview() + v.hasVerticalScroller = true + v.hasHorizontalScroller = true + + for columnType in ConnectionColume.allCases { + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: columnType.rawValue)) + column.title = columnType.columeTitle + column.minWidth = columnType.minWidth + column.maxWidth = columnType.maxWidth + column.width = columnType.width + column.sortDescriptorPrototype = viewModel.sortSortDescriptor(for: columnType) + tableView.addTableColumn(column) + } + tableView.autosaveName = className.appending("tableAutoSave") + tableView.autosaveTableColumns = true + tableView.sortDescriptors = [viewModel.currentSortDescriptor].compactMap { $0 } + tableView.usesAlternatingRowBackgroundColors = true + tableView.delegate = self + tableView.allowsMultipleSelection = true + tableView.dataSource = self + tableView.sizeLastColumnToFit() + tableView.reloadData() + closeConnectionMenuItem.title = NSLocalizedString("Close Connection", comment: "") + closeConnectionMenuItem.target = self + closeConnectionMenuItem.action = #selector(actionCloseConnection) + tableView.menu = NSMenu() + tableView.menu?.autoenablesItems = false + tableView.menu?.addItem(closeConnectionMenuItem) + tableView.menu?.delegate = self + + viewModel.onReloadTable = { [weak self] in + guard let self else { return } + tableView.reloadData() + tableView.selectRowIndexes(viewModel.currentSelection(), byExtendingSelection: false) + } + } + + @objc func actionCloseConnection() { + if tableView.selectedRowIndexes.contains(tableView.clickedRow) { + viewModel.closeConnection(for: tableView.selectedRowIndexes) + } else { + viewModel.closeConnection(for: [tableView.clickedRow]) + } + } +} + +@available(macOS 10.15, *) +extension ConnectionTopListView: NSMenuDelegate { + func menuNeedsUpdate(_ menu: NSMenu) { + closeConnectionMenuItem.isEnabled = !tableView.selectedRowIndexes.isEmpty + } +} + +@available(macOS 10.15, *) +extension ConnectionTopListView: NSTableViewDelegate { + func tableViewSelectionDidChange(_ notification: Notification) { + viewModel.setSelect(row: tableView.selectedRowIndexes) + } + + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + return 28 + } + + func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) { + viewModel.currentSortDescriptor = tableView.sortDescriptors.first + tableView.sortDescriptors = [viewModel.currentSortDescriptor].compactMap { $0 } + } +} + +@available(macOS 10.15, *) +extension ConnectionTopListView: NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return viewModel.connections.count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + guard let tableColumn, let type = ConnectionColume(rawValue: tableColumn.identifier.rawValue) else { return nil } + var view = tableView.makeView(withIdentifier: tableColumn.identifier, owner: self) as? ConnectionCellProtocol + if view == nil { + switch type { + case .process: + view = ConnectionProxyClientCellView() + case .statusIcon: + view = ConnectionStatusIconCellView() + default: + view = ConnectionTextCellView() + } + view?.identifier = tableColumn.identifier + } + let c = viewModel.connections[row] + view?.setup(with: c, type: type) + return view + } +} diff --git a/ClashX/ViewControllers/Connections/Views/SectionedTableView.swift b/ClashX/ViewControllers/Connections/Views/SectionedTableView.swift new file mode 100644 index 000000000..61a43ade8 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/SectionedTableView.swift @@ -0,0 +1,147 @@ +// +// SectionedTableView.swift +// ClashX +// +// Created by yicheng on 2023/7/13. +// Copyright © 2023 Marcin Krzyzanowski. All rights reserved. +// + +import Cocoa + +protocol TableViewSectionDataSource: NSObject { + func numberOfSectionsInTableView(tableView: NSTableView) -> Int + func tableView(tableView: NSTableView, numberOfRowsInSection section: Int) -> Int + func tableView(tableView: NSTableView, viewForHeaderInSection section: Int) -> NSView? + func tableView(tableView: NSTableView, viewForRowAt indexPath: IndexPath, column: NSTableColumn) -> NSView? + func tableView(tableView: NSTableView, didSelectRowAtIndexPath indexPath: IndexPath) + func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat +} + +class SectionedTableView: NSTableView { + weak var sectionDatasource: TableViewSectionDataSource? + private var sectionHeaders = [Int: NSView]() + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + dataSource = self + delegate = self + } + + override func reloadData() { + for section in 0 ..< (sectionDatasource?.numberOfSectionsInTableView(tableView: self) ?? 0) { + if let header = sectionDatasource?.tableView(tableView: self, viewForHeaderInSection: section) { + sectionHeaders[section] = header + } + } + super.reloadData() + } + + private func sectionForRow(row: Int, counts: [Int]) -> (section: Int?, row: Int?) { + var c = counts[0] + for section in 0 ..< counts.count { + if section > 0 { + c += counts[section] + } + if (row >= c - counts[section]) && row < c { + return (section: section, row: row - (c - counts[section])) + } + } + return (section: nil, row: nil) + } + + private func sectionForRow(row: Int) -> (section: Int, row: Int) { + if let dataSource = sectionDatasource { + let numberOfSections = dataSource.numberOfSectionsInTableView(tableView: self) + var counts = [Int](repeating: 0, count: numberOfSections) + + for section in 0 ..< numberOfSections { + counts[section] = dataSource.tableView(tableView: self, numberOfRowsInSection: section) + ((sectionHeaders[section] != nil) ? 1 : 0) + } + + let result = sectionForRow(row: row, counts: counts) + return (section: result.section ?? 0, row: result.row ?? 0) + } + + assertionFailure("Invalid datasource") + return (section: 0, row: 0) + } + + func selectRow(at indexPath: IndexPath) { + var count = 0 + for section in 0 ... indexPath.section { + if sectionHeaders[section] != nil { + count += 1 + } + if section != indexPath.section { + count += sectionDatasource?.tableView(tableView: self, numberOfRowsInSection: section) ?? 0 + } + } + count += indexPath.item + + selectRowIndexes(IndexSet(integer: count), byExtendingSelection: false) + } +} + +extension SectionedTableView: NSTableViewDataSource, NSTableViewDelegate { + func numberOfRows(in tableView: NSTableView) -> Int { + var total = 0 + + if let dataSource = sectionDatasource { + for section in 0 ..< dataSource.numberOfSectionsInTableView(tableView: tableView) { + total += dataSource.tableView(tableView: tableView, numberOfRowsInSection: section) + if sectionHeaders[section] != nil { + total += 1 + } + } + } + + return total + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + guard let dataSource = sectionDatasource, let tableColumn else { return nil } + let (section, sectionRow) = sectionForRow(row: row) + + if let headerView = sectionHeaders[section] { + if sectionRow == 0 { + return headerView + } + return dataSource.tableView(tableView: tableView, viewForRowAt: IndexPath(item: sectionRow - 1, section: section), column: tableColumn) + } + return dataSource.tableView(tableView: tableView, viewForRowAt: IndexPath(item: sectionRow, section: section), column: tableColumn) + } + + func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { + let (_, sectionRow) = sectionForRow(row: row) + if sectionRow == 0 { + return false + } + return true + } + + func tableViewSelectionDidChange(_ notification: Notification) { + guard let dataSource = sectionDatasource else { return } + let (section, sectionRow) = sectionForRow(row: selectedRow) + + if sectionHeaders[section] != nil { + if sectionRow == 0 { + return + } + dataSource.tableView(tableView: self, didSelectRowAtIndexPath: IndexPath(item: sectionRow - 1, section: section)) + return + } + dataSource.tableView(tableView: self, didSelectRowAtIndexPath: IndexPath(item: sectionRow, section: section)) + } + + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + return sectionDatasource?.tableView(tableView: tableView, heightOfRow: row) ?? 0 + } +} diff --git a/ClashX/ViewControllers/Connections/Views/zh-Hans.lproj/ConnectionDetailInfoGeneralView.strings b/ClashX/ViewControllers/Connections/Views/zh-Hans.lproj/ConnectionDetailInfoGeneralView.strings new file mode 100644 index 000000000..377b86fe6 --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/zh-Hans.lproj/ConnectionDetailInfoGeneralView.strings @@ -0,0 +1,53 @@ +/* Class = "NSTextFieldCell"; title = "NetworkType"; ObjectID = "6p6-n8-rBg"; */ +"6p6-n8-rBg.title" = "网络类型"; + +/* Class = "NSBox"; title = "Current Speed"; ObjectID = "7fZ-OX-aWF"; */ +"7fZ-OX-aWF.title" = "实时速率"; + +/* Class = "NSTextFieldCell"; title = "Source"; ObjectID = "9Id-dT-XYP"; */ +"9Id-dT-XYP.title" = "来源"; + +/* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "CGD-ut-PXk"; */ +"CGD-ut-PXk.title" = "上传"; + +/* Class = "NSBox"; title = "Rule"; ObjectID = "CiP-Ib-BPd"; */ +"CiP-Ib-BPd.title" = "规则"; + +/* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "GhE-tw-D8L"; */ +"GhE-tw-D8L.title" = "上传"; + +/* Class = "NSTextFieldCell"; title = "Entry"; ObjectID = "KQt-5l-lyc"; */ +"KQt-5l-lyc.title" = "入口"; + +/* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "KsS-nl-D2n"; */ +"KsS-nl-D2n.title" = "上传"; + +/* Class = "NSBox"; title = "Proxy Chain"; ObjectID = "MIz-sg-4Rx"; */ +"MIz-sg-4Rx.title" = "代理链"; + +/* Class = "NSBox"; title = "Max Speed"; ObjectID = "NBq-b1-RLL"; */ +"NBq-b1-RLL.title" = "最高速率"; + +/* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "ZEc-E9-AXS"; */ +"ZEc-E9-AXS.title" = "下载"; + +/* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "iem-RP-B0u"; */ +"iem-RP-B0u.title" = "下载"; + +/* Class = "NSBox"; title = "General"; ObjectID = "leq-MF-MFL"; */ +"leq-MF-MFL.title" = "通用"; + +/* Class = "NSTextFieldCell"; title = "Dest."; ObjectID = "m5m-4U-xAi"; */ +"m5m-4U-xAi.title" = "去向"; + +/* Class = "NSBox"; title = "Other"; ObjectID = "nG8-0D-7W1"; */ +"nG8-0D-7W1.title" = "其他"; + +/* Class = "NSBox"; title = "Address"; ObjectID = "obG-yt-Gi8"; */ +"obG-yt-Gi8.title" = "地址"; + +/* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "tes-yR-PKh"; */ +"tes-yR-PKh.title" = "下载"; + +/* Class = "NSBox"; title = "Total"; ObjectID = "xB0-fx-J0y"; */ +"xB0-fx-J0y.title" = "总计"; diff --git a/ClashX/ViewControllers/Connections/Views/zh-Hant.lproj/ConnectionDetailInfoGeneralView.strings b/ClashX/ViewControllers/Connections/Views/zh-Hant.lproj/ConnectionDetailInfoGeneralView.strings new file mode 100644 index 000000000..9509734db --- /dev/null +++ b/ClashX/ViewControllers/Connections/Views/zh-Hant.lproj/ConnectionDetailInfoGeneralView.strings @@ -0,0 +1,53 @@ +/* Class = "NSTextFieldCell"; title = "NetworkType"; ObjectID = "6p6-n8-rBg"; */ +"6p6-n8-rBg.title" = "網絡類型"; + +/* Class = "NSBox"; title = "Current Speed"; ObjectID = "7fZ-OX-aWF"; */ +"7fZ-OX-aWF.title" = "實時速率"; + +/* Class = "NSTextFieldCell"; title = "Source"; ObjectID = "9Id-dT-XYP"; */ +"9Id-dT-XYP.title" = "來源"; + +/* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "CGD-ut-PXk"; */ +"CGD-ut-PXk.title" = "上傳"; + +/* Class = "NSBox"; title = "Rule"; ObjectID = "CiP-Ib-BPd"; */ +"CiP-Ib-BPd.title" = "規則"; + +/* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "GhE-tw-D8L"; */ +"GhE-tw-D8L.title" = "上傳"; + +/* Class = "NSTextFieldCell"; title = "Entry"; ObjectID = "KQt-5l-lyc"; */ +"KQt-5l-lyc.title" = "入口"; + +/* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "KsS-nl-D2n"; */ +"KsS-nl-D2n.title" = "上傳"; + +/* Class = "NSBox"; title = "Proxy Chain"; ObjectID = "MIz-sg-4Rx"; */ +"MIz-sg-4Rx.title" = "代理鏈"; + +/* Class = "NSBox"; title = "Max Speed"; ObjectID = "NBq-b1-RLL"; */ +"NBq-b1-RLL.title" = "最高速率"; + +/* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "ZEc-E9-AXS"; */ +"ZEc-E9-AXS.title" = "下載"; + +/* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "iem-RP-B0u"; */ +"iem-RP-B0u.title" = "下載"; + +/* Class = "NSBox"; title = "General"; ObjectID = "leq-MF-MFL"; */ +"leq-MF-MFL.title" = "通用"; + +/* Class = "NSTextFieldCell"; title = "Dest."; ObjectID = "m5m-4U-xAi"; */ +"m5m-4U-xAi.title" = "去向"; + +/* Class = "NSBox"; title = "Other"; ObjectID = "nG8-0D-7W1"; */ +"nG8-0D-7W1.title" = "其他"; + +/* Class = "NSBox"; title = "Address"; ObjectID = "obG-yt-Gi8"; */ +"obG-yt-Gi8.title" = "地址"; + +/* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "tes-yR-PKh"; */ +"tes-yR-PKh.title" = "下載"; + +/* Class = "NSBox"; title = "Total"; ObjectID = "xB0-fx-J0y"; */ +"xB0-fx-J0y.title" = "總計"; diff --git a/ClashX/ViewControllers/ExternalControlViewController.swift b/ClashX/ViewControllers/ExternalControlViewController.swift index 48b1b72d2..5cf8f1f45 100644 --- a/ClashX/ViewControllers/ExternalControlViewController.swift +++ b/ClashX/ViewControllers/ExternalControlViewController.swift @@ -101,6 +101,7 @@ class ExternalControlAddView: NSView { setupView() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -137,11 +138,11 @@ class ExternalControlAddView: NSView { nameField.leadingAnchor.constraint(equalTo: urlTextField.leadingAnchor), nameLabel.centerYAnchor.constraint(equalTo: nameField.centerYAnchor), nameLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor), - nameField.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 5), + nameField.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 5) ]) } func isVaild() -> Bool { - return urlTextField.stringValue.isUrlVaild() && nameLabel.stringValue.count > 0 + return urlTextField.stringValue.isUrlVaild() && !nameLabel.stringValue.isEmpty } } diff --git a/ClashX/ViewControllers/RemoteConfigViewController.swift b/ClashX/ViewControllers/RemoteConfigViewController.swift index bd06df039..b8603df7a 100644 --- a/ClashX/ViewControllers/RemoteConfigViewController.swift +++ b/ClashX/ViewControllers/RemoteConfigViewController.swift @@ -219,24 +219,24 @@ class RemoteConfigAddView: NSView, NibLoadable { /// Get the config name /// - Returns: return (name, isUserInput) func getConfigName() -> (String, Bool) { - if configNameTextField.stringValue.count > 0 { + if !configNameTextField.stringValue.isEmpty { return (configNameTextField.stringValue, true) } return (configNameTextField.placeholderString ?? "", false) } func isVaild() -> Bool { - return urlTextField.stringValue.isUrlVaild() && getConfigName().0.count > 0 + return urlTextField.stringValue.isUrlVaild() && !getConfigName().0.isEmpty } func setUrl(string: String, name: String? = nil, defaultName: String?) { urlTextField.stringValue = string - if let name = name, name.count > 0 { + if let name = name, !name.isEmpty { configNameTextField.stringValue = name } - if let defaultName = defaultName, defaultName.count > 0 { + if let defaultName = defaultName, !defaultName.isEmpty { configNameTextField.placeholderString = defaultName } diff --git a/ClashX/ViewControllers/Settings/DebugSettingViewController.swift b/ClashX/ViewControllers/Settings/DebugSettingViewController.swift new file mode 100644 index 000000000..ba02e56d4 --- /dev/null +++ b/ClashX/ViewControllers/Settings/DebugSettingViewController.swift @@ -0,0 +1,82 @@ +// +// DebugSettingViewController.swift +// ClashX Pro +// +// Created by yicheng on 2023/5/25. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import RxSwift + +class DebugSettingViewController: NSViewController { + @IBOutlet var useBuiltinApiButton: NSButton! + @IBOutlet var revertProxyButton: NSButton! + @IBOutlet var updateChannelPopButton: NSPopUpButton! + var disposeBag = DisposeBag() + override func viewDidLoad() { + super.viewDidLoad() + useBuiltinApiButton.state = Settings.builtInApiMode ? .on : .off + revertProxyButton.state = Settings.disableRestoreProxy ? .off : .on + AutoUpgardeManager.shared.addChannelMenuItem(updateChannelPopButton) + } + + @IBAction func actionUnInstallProxyHelper(_ sender: Any) { + PrivilegedHelperManager.shared.removeInstallHelper() + } + + @IBAction func actionOpenLogFolder(_ sender: Any) { + NSWorkspace.shared.openFile(Logger.shared.logFolder()) + } + + @IBAction func actionOpenLocalConfig(_ sender: Any) { + NSWorkspace.shared.openFile(kConfigFolderPath) + } + + @IBAction func actionOpenIcloudConfig(_ sender: Any) { + if ICloudManager.shared.icloudAvailable { + ICloudManager.shared.getUrl { + url in + if let url = url { + NSWorkspace.shared.open(url) + } + } + } else { + NSAlert.alert(with: NSLocalizedString("iCloud not available", comment: "")) + } + } + + @IBAction func actionResetUserDefault(_ sender: Any) { + guard let domain = Bundle.main.bundleIdentifier else { return } + NSAlert.alert(with: NSLocalizedString("Click OK to quit the app and apply change.", comment: "")) + UserDefaults.standard.removePersistentDomain(forName: domain) + UserDefaults.standard.synchronize() + NSApplication.shared.terminate(self) + } + + @IBAction func actionSetUseApiMode(_ sender: Any) { + let alert = NSAlert() + alert.informativeText = NSLocalizedString("Need to Restart the ClashX to Take effect, Please start clashX manually", comment: "") + alert.addButton(withTitle: NSLocalizedString("Apply and Quit", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + if alert.runModal() == .alertFirstButtonReturn { + Settings.builtInApiMode = !Settings.builtInApiMode + NSApp.terminate(nil) + } else { + useBuiltinApiButton.state = Settings.builtInApiMode ? .on : .off + } + } + + @IBAction func actionUpdateGeoipDb(_ sender: Any) { + ClashResourceManager.updateGeoIP() + } + + @IBAction func actionRevertProxy(_ sender: Any) { + Settings.disableRestoreProxy.toggle() + revertProxyButton.state = Settings.disableRestoreProxy ? .off : .on + } + + @IBAction func actionOpenCrashLogFolder(_ sender: Any) { + NSWorkspace.shared.open(URL(fileURLWithPath: "\(NSHomeDirectory())/Library/Logs/DiagnosticReports", isDirectory: true)) + } +} diff --git a/ClashX/ViewControllers/Settings/GeneralSettingViewController.swift b/ClashX/ViewControllers/Settings/GeneralSettingViewController.swift new file mode 100644 index 000000000..ee62ff637 --- /dev/null +++ b/ClashX/ViewControllers/Settings/GeneralSettingViewController.swift @@ -0,0 +1,139 @@ +// +// GeneralSettingViewController.swift +// ClashX Pro +// +// Created by yicheng on 2022/11/20. +// Copyright © 2022 west2online. All rights reserved. +// + +import Cocoa +import RxSwift + +class GeneralSettingViewController: NSViewController { + @IBOutlet var ignoreListTextView: NSTextView! + @IBOutlet var launchAtLoginButton: NSButton! + + @IBOutlet var reduceNotificationsButton: NSButton! + @IBOutlet var useiCloudButton: NSButton! + + @IBOutlet var allowApiLanUsageSwitcher: NSButton! + @IBOutlet var proxyPortTextField: NSTextField! + @IBOutlet var apiPortTextField: NSTextField! + @IBOutlet var ssidSuspendTextField: NSTextView! + + @IBOutlet var apiSecretTextField: NSTextField! + + @IBOutlet var apiSecretOverrideButton: NSButton! + + @IBOutlet var ipv6Button: NSButton! + @IBOutlet var speedTestUrlField: NSTextField! + + var disposeBag = DisposeBag() + override func viewDidLoad() { + super.viewDidLoad() + speedTestUrlField.stringValue = Settings.benchMarkUrl + speedTestUrlField.placeholderString = Settings.defaultBenchmarkUrl + ignoreListTextView.string = Settings.proxyIgnoreList.joined(separator: ",") + ignoreListTextView.rx + .string.debounce(.milliseconds(500), scheduler: MainScheduler.instance) + .map { $0.components(separatedBy: ",").filter { !$0.isEmpty } } + .subscribe { arr in + Settings.proxyIgnoreList = arr + }.disposed(by: disposeBag) + + ssidSuspendTextField.string = Settings.disableSSIDList.joined(separator: ",") + ssidSuspendTextField.rx + .string.debounce(.milliseconds(500), scheduler: MainScheduler.instance) + .map { $0.components(separatedBy: ",").filter { !$0.isEmpty } } + .subscribe { arr in + Settings.disableSSIDList = arr + SSIDSuspendTool.shared.update() + }.disposed(by: disposeBag) + + LaunchAtLogin.shared.isEnableVirable + .map { $0 ? .on : .off } + .bind(to: launchAtLoginButton.rx.state) + .disposed(by: disposeBag) + launchAtLoginButton.rx.state.map { $0 == .on }.subscribe { + LaunchAtLogin.shared.isEnabled = $0 + }.disposed(by: disposeBag) + + ICloudManager.shared.useiCloud + .map { $0 ? .on : .off } + .bind(to: useiCloudButton.rx.state) + .disposed(by: disposeBag) + useiCloudButton.rx.state.map { $0 == .on }.subscribe { + ICloudManager.shared.userEnableiCloud = $0 + }.disposed(by: disposeBag) + reduceNotificationsButton.toolTip = NSLocalizedString("Reduce alerts if notification permission is disabled", comment: "") + reduceNotificationsButton.state = Settings.disableNoti ? .on : .off + reduceNotificationsButton.rx.state.map { $0 == .on }.subscribe { + Settings.disableNoti = $0 + }.disposed(by: disposeBag) + + ipv6Button.state = Settings.enableIPV6 ? .on : .off + ipv6Button.rx.state.map { $0 == .on }.subscribe { + Settings.enableIPV6 = $0 + }.disposed(by: disposeBag) + + if Settings.proxyPort > 0 { + proxyPortTextField.stringValue = "\(Settings.proxyPort)" + } else { + proxyPortTextField.stringValue = "\(ConfigManager.shared.currentConfig?.mixedPort ?? 0)" + } + if Settings.apiPort > 0 { + apiPortTextField.stringValue = "\(Settings.apiPort)" + } else { + apiPortTextField.stringValue = ConfigManager.shared.apiPort + } + + apiSecretTextField.stringValue = Settings.apiSecret + apiSecretTextField.rx.text.compactMap { $0 }.bind { + Settings.apiSecret = $0 + }.disposed(by: disposeBag) + + apiSecretOverrideButton.state = Settings.overrideConfigSecret ? .on : .off + apiSecretOverrideButton.rx.state.bind { state in + Settings.overrideConfigSecret = state == .on + }.disposed(by: disposeBag) + + proxyPortTextField.rx.text + .compactMap { $0 } + .compactMap { Int($0) } + .bind { + Settings.proxyPort = $0 + }.disposed(by: disposeBag) + + apiPortTextField.rx.text + .compactMap { $0 } + .compactMap { Int($0) } + .bind { + Settings.apiPort = $0 + }.disposed(by: disposeBag) + allowApiLanUsageSwitcher.state = Settings.apiPortAllowLan ? .on : .off + allowApiLanUsageSwitcher.rx.state.bind { state in + Settings.apiPortAllowLan = state == .on + }.disposed(by: disposeBag) + } + + override func viewDidAppear() { + super.viewDidAppear() + view.window?.makeFirstResponder(nil) + } + + override func viewWillDisappear() { + super.viewWillDisappear() + let url = speedTestUrlField.stringValue + if url.isUrlVaild() || url.isEmpty { + Settings.benchMarkUrl = url + } + SSIDSuspendTool.shared.showNoticeOnNotPermission = true + SSIDSuspendTool.shared.requestPermissionIfNeed() + SSIDSuspendTool.shared.update() + } + + @IBAction func actionResetIgnoreList(_ sender: Any) { + ignoreListTextView.string = Settings.proxyIgnoreListDefaultValue.joined(separator: ",") + Settings.proxyIgnoreList = Settings.proxyIgnoreListDefaultValue + } +} diff --git a/ClashX/ViewControllers/Settings/GlobalShortCutViewController.swift b/ClashX/ViewControllers/Settings/GlobalShortCutViewController.swift new file mode 100644 index 000000000..f630b148f --- /dev/null +++ b/ClashX/ViewControllers/Settings/GlobalShortCutViewController.swift @@ -0,0 +1,121 @@ +// +// GlobalShortCutViewController.swift +// ClashX Pro +// +// Created by yicheng on 2023/5/26. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import KeyboardShortcuts + +extension KeyboardShortcuts.Name { + static let toggleSystemProxyMode = Self("shortCut.toggleSystemProxyMode") + static let copyShellCommand = Self("shortCut.copyShellCommand") + static let copyExternalShellCommand = Self("shortCut.copyExternalShellCommand") + + static let modeDirect = Self("shortCut.modeDirect") + static let modeRule = Self("shortCut.modeRule") + static let modeGlobal = Self("shortCut.modeGlobal") + + static let log = Self("shortCut.log") + static let dashboard = Self("shortCut.dashboard") + static let openMenu = Self("shortCut.openMenu") + static let nativeDashboard = Self("shortCut.nativeDashboard") +} + +enum KeyboardShortCutManager { + static func setup() { + KeyboardShortcuts.onKeyUp(for: .toggleSystemProxyMode) { + AppDelegate.shared.actionSetSystemProxy(nil) + } + + KeyboardShortcuts.onKeyUp(for: .copyShellCommand) { + AppDelegate.shared.actionCopyExportCommand(AppDelegate.shared.copyExportCommandMenuItem) + } + + KeyboardShortcuts.onKeyUp(for: .copyExternalShellCommand) { + AppDelegate.shared.actionCopyExportCommand(AppDelegate.shared.copyExportCommandExternalMenuItem) + } + + KeyboardShortcuts.onKeyUp(for: .modeDirect) { + AppDelegate.shared.switchProxyMode(mode: .direct) + } + + KeyboardShortcuts.onKeyUp(for: .modeRule) { + AppDelegate.shared.switchProxyMode(mode: .rule) + } + + KeyboardShortcuts.onKeyUp(for: .modeGlobal) { + AppDelegate.shared.switchProxyMode(mode: .global) + } + + KeyboardShortcuts.onKeyUp(for: .log) { + AppDelegate.shared.actionShowLog(nil) + } + + KeyboardShortcuts.onKeyUp(for: .dashboard) { + AppDelegate.shared.actionDashboard(nil) + } + + KeyboardShortcuts.onKeyUp(for: .openMenu) { + AppDelegate.shared.statusItem.button?.performClick(nil) + } + if #available(macOS 10.15, *) { + KeyboardShortcuts.onKeyUp(for: .nativeDashboard) { + ClashWindowController.create().showWindow(self) + } + } + } +} + +class GlobalShortCutViewController: NSViewController { + @IBOutlet var proxyBox: NSBox! + @IBOutlet var modeBoxView: NSView! + @IBOutlet var otherBoxView: NSView! + + override func viewDidLoad() { + super.viewDidLoad() + let systemProxy = getRecoder(for: .toggleSystemProxyMode) + let copyShellCommand = getRecoder(for: .copyShellCommand) + let copyShellCommandExternal = getRecoder(for: .copyExternalShellCommand) + addGridView(in: proxyBox.contentView!, with: [ + [NSTextField(labelWithString: NSLocalizedString("System Proxy", comment: "")), systemProxy], + [NSTextField(labelWithString: NSLocalizedString("Copy Shell Command", comment: "")), copyShellCommand], + [NSTextField(labelWithString: NSLocalizedString("Copy Shell Command (External)", comment: "")), copyShellCommandExternal] + ]) + + addGridView(in: modeBoxView, with: [ + [NSTextField(labelWithString: NSLocalizedString("Direct Mode", comment: "")), getRecoder(for: .modeDirect)], + [NSTextField(labelWithString: NSLocalizedString("Rule Mode", comment: "")), getRecoder(for: .modeRule)], + [NSTextField(labelWithString: NSLocalizedString("Global Mode", comment: "")), getRecoder(for: .modeGlobal)] + ]) + + var otherItems: [[NSView]] = [ + [NSTextField(labelWithString: NSLocalizedString("Open Menu", comment: "")), getRecoder(for: .openMenu)], + [NSTextField(labelWithString: NSLocalizedString("Open Log", comment: "")), getRecoder(for: .log)], + [NSTextField(labelWithString: NSLocalizedString("Open Dashboard", comment: "")), getRecoder(for: .dashboard)] + ] + if #available(macOS 10.15, *) { + otherItems.append([NSTextField(labelWithString: NSLocalizedString("Open Connection Details", comment: "")), getRecoder(for: .nativeDashboard)]) + } + addGridView(in: otherBoxView, with: otherItems) + } + + private func getRecoder(for name: KeyboardShortcuts.Name) -> KeyboardShortcuts.RecorderCocoa { + let view = KeyboardShortcuts.RecorderCocoa(for: name) + view.setContentCompressionResistancePriority(.required, for: .vertical) + return view + } + + private func addGridView(in superView: NSView, with views: [[NSView]]) { + let gridView = NSGridView(views: views) + gridView.rowSpacing = 10 + superView.addSubview(gridView) + gridView.makeConstraintsToBindToSuperview(NSEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)) + gridView.setContentHuggingPriority(.required, for: .vertical) + gridView.setContentCompressionResistancePriority(.required, for: .vertical) + gridView.xPlacement = .trailing + gridView.column(at: 0).xPlacement = .leading + } +} diff --git a/ClashX/ViewControllers/Settings/SettingTabViewController.swift b/ClashX/ViewControllers/Settings/SettingTabViewController.swift new file mode 100644 index 000000000..a93155573 --- /dev/null +++ b/ClashX/ViewControllers/Settings/SettingTabViewController.swift @@ -0,0 +1,23 @@ +// +// SettingTabViewController.swift +// ClashX Pro +// +// Created by yicheng on 2022/11/20. +// Copyright © 2022 west2online. All rights reserved. +// + +import Cocoa + +class SettingTabViewController: NSTabViewController, NibLoadable { + override func viewDidLoad() { + super.viewDidLoad() + tabStyle = .toolbar + if #unavailable(macOS 10.11) { + tabStyle = .segmentedControlOnTop + tabViewItems.forEach { item in + item.image = nil + } + } + NSApp.activate(ignoringOtherApps: true) + } +} diff --git a/ClashX/Views/MenuItemBaseView.swift b/ClashX/Views/MenuItemBaseView.swift index d69e9f1a2..96698d108 100644 --- a/ClashX/Views/MenuItemBaseView.swift +++ b/ClashX/Views/MenuItemBaseView.swift @@ -15,7 +15,13 @@ class MenuItemBaseView: NSView { // MARK: Public - var isHighlighted: Bool = false + var isHighlighted: Bool = false { + didSet { + if isHighlighted != oldValue { + setNeedsDisplay() + } + } + } let effectView: NSVisualEffectView = { let effectView = NSVisualEffectView() @@ -56,6 +62,7 @@ class MenuItemBaseView: NSView { setupView() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -127,7 +134,7 @@ class MenuItemBaseView: NSView { override func viewDidMoveToSuperview() { super.viewDidMoveToSuperview() guard autolayout else { return } - if #available(macOS 10.15, *) {} else { + if #unavailable(macOS 10.15) { if let view = superview { view.autoresizingMask = [.width] } diff --git a/ClashX/Views/NormalMenuItemView.swift b/ClashX/Views/NormalMenuItemView.swift new file mode 100644 index 000000000..f3e14e37f --- /dev/null +++ b/ClashX/Views/NormalMenuItemView.swift @@ -0,0 +1,63 @@ +// +// NormalMenuItemView.swift +// ClashX +// +// Created by yicheng on 2023/6/25. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit + +@available(macOS 11.0, *) +class NormalMenuItemView: MenuItemBaseView { + let label: NSTextField + private let arrowLabel: NSControl = { + let image = NSImage(named: NSImage.goForwardTemplateName)!.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .small))! + return NSImageView(image: image) + }() + + init(_ title: String, rightArrow: Bool) { + label = NSTextField(labelWithString: title) + label.font = type(of: self).labelFont + label.sizeToFit() + let rect = NSRect(x: 0, y: 0, width: label.bounds.width + 40 + arrowLabel.bounds.width, height: 20) + super.init(frame: rect, autolayout: false) + addSubview(label) + label.frame = NSRect(x: 20, y: 0, width: label.bounds.width, height: 20) + label.textColor = NSColor.labelColor + if rightArrow { + addSubview(arrowLabel) + } + } + + override func layoutSubtreeIfNeeded() { + super.layoutSubtreeIfNeeded() + arrowLabel.frame = NSRect(x: bounds.width - arrowLabel.bounds.width - 12, y: 0, width: arrowLabel.bounds.width, height: 20) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var cells: [NSCell?] { + return [label.cell, arrowLabel.cell] + } + + override var labels: [NSTextField] { + return [label] + } +} + +@available(macOS 11.0, *) +extension NormalMenuItemView: ProxyGroupMenuHighlightDelegate { + func highlight(item: NSMenuItem?) { + if enclosingMenuItem?.hasSubmenu == true, let item = item { + if enclosingMenuItem?.submenu?.items.contains(item) == true { + isHighlighted = true + return + } + } + isHighlighted = item == enclosingMenuItem + } +} diff --git a/ClashX/Views/ProxyDelayHistoryMenu.swift b/ClashX/Views/ProxyDelayHistoryMenu.swift index c80e43754..8367e6b68 100644 --- a/ClashX/Views/ProxyDelayHistoryMenu.swift +++ b/ClashX/Views/ProxyDelayHistoryMenu.swift @@ -18,6 +18,7 @@ class ProxyDelayHistoryMenu: NSMenu { NotificationCenter.default.addObserver(self, selector: #selector(proxyInfoDidUpdate(note:)), name: .proxyUpdate(for: proxy.name), object: nil) } + @available(*, unavailable) required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -35,7 +36,7 @@ class ProxyDelayHistoryMenu: NSMenu { let historys = Array(proxy.history.reversed()) let change = Changeset(previous: currentHistory, current: historys, identifier: { $0.time }) currentHistory = historys - if change.moves.count == 0 && change.mutations.count == 0 { + if change.moves.isEmpty && change.mutations.isEmpty { change.removals.reversed().forEach { idx in removeItem(at: idx) } diff --git a/ClashX/Views/ProxyGroupMenu.swift b/ClashX/Views/ProxyGroupMenu.swift index 597b7fde7..8a75ec5e9 100644 --- a/ClashX/Views/ProxyGroupMenu.swift +++ b/ClashX/Views/ProxyGroupMenu.swift @@ -7,7 +7,7 @@ // import AppKit -@objc protocol ProxyGroupMenuHighlightDelegate: class { +@objc protocol ProxyGroupMenuHighlightDelegate: AnyObject { func highlight(item: NSMenuItem?) } diff --git a/ClashX/Views/ProxyGroupMenuItemView.swift b/ClashX/Views/ProxyGroupMenuItemView.swift index 6ef707488..28917b4c1 100644 --- a/ClashX/Views/ProxyGroupMenuItemView.swift +++ b/ClashX/Views/ProxyGroupMenuItemView.swift @@ -31,7 +31,7 @@ class ProxyGroupMenuItemView: MenuItemBaseView { return [groupNameLabel.cell, selectProxyLabel.cell, arrowLabel.cell] } - init(group: ClashProxyName, targetProxy: ClashProxyName, hasLeftPadding: Bool, observeUpdate:Bool = true) { + init(group: ClashProxyName, targetProxy: ClashProxyName, hasLeftPadding: Bool, observeUpdate: Bool = true) { groupNameLabel = VibrancyTextField(labelWithString: group) selectProxyLabel = VibrancyTextField(labelWithString: targetProxy) super.init(autolayout: true) @@ -67,7 +67,11 @@ class ProxyGroupMenuItemView: MenuItemBaseView { selectProxyLabel.leftAnchor.constraint(greaterThanOrEqualTo: groupNameLabel.rightAnchor, constant: 20).isActive = true // max - effectView.widthAnchor.constraint(lessThanOrEqualToConstant: 330).isActive = true + if #available(macOS 14, *) { + selectProxyLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 200).isActive = true + } else { + effectView.widthAnchor.constraint(lessThanOrEqualToConstant: 330).isActive = true + } // font & color groupNameLabel.font = type(of: self).labelFont selectProxyLabel.font = type(of: self).labelFont @@ -91,6 +95,7 @@ class ProxyGroupMenuItemView: MenuItemBaseView { } } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift b/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift index b66e14eae..6b2fafc5b 100644 --- a/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift +++ b/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift @@ -37,6 +37,7 @@ class ProxyGroupSpeedTestMenuItem: NSMenuItem { } } + @available(*, unavailable) required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -47,12 +48,12 @@ class ProxyGroupSpeedTestMenuItem: NSMenuItem { ApiRequest.getMergedProxyData { [weak self] proxyResp in guard let self = self else { return } var providers = Set() - self.proxyGroup.all?.compactMap{ + self.proxyGroup.all?.compactMap { proxyResp?.proxiesMap[$0]?.enclosingProvider?.name - }.forEach{ + }.forEach { providers.insert($0) } - providers.forEach{ + providers.forEach { ApiRequest.healthCheck(proxy: $0) } } @@ -66,7 +67,7 @@ extension ProxyGroupSpeedTestMenuItem: ProxyGroupMenuHighlightDelegate { } } -fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView { +private class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView { private let label: NSTextField init(_ title: String) { @@ -80,6 +81,7 @@ fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView { label.textColor = NSColor.labelColor } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -141,7 +143,7 @@ fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView { self.label.stringValue = menu.title menu.isEnabled = true self.setNeedsDisplay() - if providers.count > 0 { + if !providers.isEmpty { MenuItemFactory.refreshExistingMenuItems() } } diff --git a/ClashX/Views/ProxyItemView.swift b/ClashX/Views/ProxyItemView.swift index 3a8857a23..d207e3eab 100644 --- a/ClashX/Views/ProxyItemView.swift +++ b/ClashX/Views/ProxyItemView.swift @@ -75,7 +75,7 @@ class ProxyItemView: MenuItemBaseView { switch delay { case 0: delayLabel.layer?.backgroundColor = CGColor.fail - case 0..<300: + case 0 ..< 300: delayLabel.layer?.backgroundColor = CGColor.good default: delayLabel.layer?.backgroundColor = CGColor.meduim @@ -101,6 +101,7 @@ class ProxyItemView: MenuItemBaseView { } } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -114,7 +115,7 @@ class ProxyItemView: MenuItemBaseView { } } -fileprivate extension CGColor { +private extension CGColor { static let good = CGColor(red: 30.0 / 255, green: 181.0 / 255, blue: 30.0 / 255, alpha: 1) static let meduim = CGColor(red: 1, green: 135.0 / 255, blue: 0, alpha: 1) static let fail = CGColor(red: 218.0 / 255, green: 0.0, blue: 3.0 / 255, alpha: 1) diff --git a/ClashX/Views/ProxyMenuItem.swift b/ClashX/Views/ProxyMenuItem.swift index 18bf61a00..2095fd478 100644 --- a/ClashX/Views/ProxyMenuItem.swift +++ b/ClashX/Views/ProxyMenuItem.swift @@ -46,6 +46,7 @@ class ProxyMenuItem: NSMenuItem { } } + @available(*, unavailable) required init(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -71,7 +72,11 @@ class ProxyMenuItem: NSMenuItem { assertionFailure() return } - updateDelay(info.history.last?.delayDisplay, rawValue: info.history.last?.delay) + if info.alive == false { + updateDelay(NSLocalizedString("fail", comment: ""), rawValue: 0) + } else { + updateDelay(info.history.last?.delayDisplay, rawValue: info.history.last?.delay) + } } @objc private func proxyGroupInfoUpdate(note: Notification) { @@ -108,7 +113,7 @@ extension ProxyMenuItem { func getAttributedTitle(name: String, delay: String?) -> NSAttributedString { let paragraph = NSMutableParagraphStyle() paragraph.tabStops = [ - NSTextTab(textAlignment: .right, location: 65 + maxProxyNameLength, options: [:]), + NSTextTab(textAlignment: .right, location: 65 + maxProxyNameLength, options: [:]) ] let proxyName = name.replacingOccurrences(of: "\t", with: " ") let str: String @@ -122,16 +127,16 @@ extension ProxyMenuItem { string: str, attributes: [ NSAttributedString.Key.paragraphStyle: paragraph, - NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 14), + NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 14) ] ) let hackAttr = [NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 15)] - attributed.addAttributes(hackAttr, range: NSRange(name.utf16.count.. StatusItemView { + var topLevelObjects: NSArray? + if Bundle.main.loadNibNamed("StatusItemView", owner: self, topLevelObjects: &topLevelObjects) { + let view = (topLevelObjects!.first(where: { $0 is NSView }) as? StatusItemView)! + view.setupView() + view.imageView.image = StatusItemTool.menuImage + + if let button = statusItem?.button { + button.addSubview(view) + button.imagePosition = .imageOverlaps + } else { + Logger.log("button = nil") + AppDelegate.shared.openConfigFolder(self) + } + view.updateViewStatus(enableProxy: false) + return view + } + return NSView() as! StatusItemView + } + + func setupView() { + uploadSpeedLabel.font = StatusItemTool.font + downloadSpeedLabel.font = StatusItemTool.font + + uploadSpeedLabel.textColor = NSColor.labelColor + downloadSpeedLabel.textColor = NSColor.labelColor + } + + func updateSize(width: CGFloat) { + frame = CGRect(x: 0, y: 0, width: width, height: 22) + } + + func updateViewStatus(enableProxy: Bool) { + if enableProxy { + imageView.contentTintColor = NSColor.labelColor + } else { + imageView.contentTintColor = NSColor.labelColor.withSystemEffect(.disabled) + } + } + + func updateSpeedLabel(up: Int, down: Int) { + guard !speedContainerView.isHidden else { return } + if up != self.up { + uploadSpeedLabel.stringValue = SpeedUtils.getSpeedString(for: up) + self.up = up + } + if down != self.down { + downloadSpeedLabel.stringValue = SpeedUtils.getSpeedString(for: down) + self.down = down + } + } + + func showSpeedContainer(show: Bool) { + speedContainerView.isHidden = !show + } +} diff --git a/ClashX/Views/StatusItemView.xib b/ClashX/Views/StatusItem/StatusItemView.xib similarity index 98% rename from ClashX/Views/StatusItemView.xib rename to ClashX/Views/StatusItem/StatusItemView.xib index dc1593434..77be4e983 100644 --- a/ClashX/Views/StatusItemView.xib +++ b/ClashX/Views/StatusItem/StatusItemView.xib @@ -1,8 +1,8 @@ - + - + diff --git a/ClashX/Views/StatusItem/StatusItemViewProtocol.swift b/ClashX/Views/StatusItem/StatusItemViewProtocol.swift new file mode 100644 index 000000000..6695cbad8 --- /dev/null +++ b/ClashX/Views/StatusItem/StatusItemViewProtocol.swift @@ -0,0 +1,16 @@ +// +// StatusItemViewProtocol.swift +// ClashX Pro +// +// Created by yicheng on 2023/3/1. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit + +protocol StatusItemViewProtocol: AnyObject { + func updateViewStatus(enableProxy: Bool) + func updateSpeedLabel(up: Int, down: Int) + func showSpeedContainer(show: Bool) + func updateSize(width: CGFloat) +} diff --git a/ClashX/Views/StatusItemView.swift b/ClashX/Views/StatusItemView.swift deleted file mode 100644 index 7b9e3bdc9..000000000 --- a/ClashX/Views/StatusItemView.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// StatusItemView.swift -// ClashX -// -// Created by CYC on 2018/6/23. -// Copyright © 2018年 yichengchen. All rights reserved. -// - -import AppKit -import Foundation -import RxCocoa -import RxSwift - -class StatusItemView: NSView { - @IBOutlet var imageView: NSImageView! - - @IBOutlet var uploadSpeedLabel: NSTextField! - @IBOutlet var downloadSpeedLabel: NSTextField! - @IBOutlet var speedContainerView: NSView! - - weak var statusItem: NSStatusItem? - - lazy var menuImage: NSImage = { - let customImagePath = (NSHomeDirectory() as NSString).appendingPathComponent("/.config/clash/menuImage.png") - if let image = NSImage(contentsOfFile: customImagePath) { - return image - } - if let imagePath = Bundle.main.path(forResource: "menu_icon@2x", ofType: "png"), - let image = NSImage(contentsOfFile: imagePath) { - return image - } - return NSImage() - }() - - static func create(statusItem: NSStatusItem?) -> StatusItemView { - var topLevelObjects: NSArray? - if Bundle.main.loadNibNamed("StatusItemView", owner: self, topLevelObjects: &topLevelObjects) { - let view = (topLevelObjects!.first(where: { $0 is NSView }) as? StatusItemView)! - view.statusItem = statusItem - view.setupView() - return view - } - return NSView() as! StatusItemView - } - - func setupView() { - let fontSize: CGFloat = 9 - let font: NSFont - if let fontName = UserDefaults.standard.string(forKey: "kStatusMenuFontName"), - let f = NSFont(name: fontName, size: fontSize) { - font = f - } else { - font = NSFont.menuBarFont(ofSize: fontSize) - } - uploadSpeedLabel.font = font - downloadSpeedLabel.font = font - - uploadSpeedLabel.textColor = NSColor.black - downloadSpeedLabel.textColor = NSColor.black - } - - func updateViewStatus(enableProxy: Bool) { - let selectedColor = NSColor.red - let unselectedColor: NSColor - if #available(OSX 10.14, *) { - unselectedColor = selectedColor.withSystemEffect(.disabled) - } else { - unselectedColor = selectedColor.withAlphaComponent(0.5) - } - - imageView.image = menuImage.tint(color: enableProxy ? selectedColor : unselectedColor) - updateStatusItemView() - } - - func getSpeedString(for byte: Int) -> String { - let kb = byte / 1024 - if kb < 1024 { - return "\(kb)KB/s" - } else { - let mb = Double(kb) / 1024.0 - if mb >= 100 { - if mb >= 1000 { - return String(format: "%.1fGB/s", mb/1024) - } - return String(format: "%.1fMB/s", mb) - } else { - return String(format: "%.2fMB/s", mb) - } - } - } - - - func updateSpeedLabel(up: Int, down: Int) { - guard !speedContainerView.isHidden else { return } - let finalUpStr = getSpeedString(for: up) - let finalDownStr = getSpeedString(for: down) - - if downloadSpeedLabel.stringValue == finalDownStr && uploadSpeedLabel.stringValue == finalUpStr { - return - } - downloadSpeedLabel.stringValue = finalDownStr - uploadSpeedLabel.stringValue = finalUpStr - updateStatusItemView() - } - - func showSpeedContainer(show: Bool) { - speedContainerView.isHidden = !show - updateStatusItemView() - } - - func updateStatusItemView() { - statusItem?.updateImage(withView: self) - } -} - -extension NSStatusItem { - func updateImage(withView view: NSView) { - if let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds) { - view.cacheDisplay(in: view.bounds, to: rep) - let img = NSImage(size: view.bounds.size) - img.addRepresentation(rep) - img.isTemplate = true - image = img - } - } -} diff --git a/ClashX/goClash/UIHelper.h b/ClashX/goClash/UIHelper.h new file mode 100644 index 000000000..bc6c34eb8 --- /dev/null +++ b/ClashX/goClash/UIHelper.h @@ -0,0 +1,23 @@ +#import + +typedef void (^NStringCallback)(NSString *,NSString *); +typedef void (^IntCallback)(int64_t,int64_t); +extern NStringCallback logCallback; +extern IntCallback trafficCallback; +void clash_setLogBlock(NStringCallback block); + +void clash_setTrafficBlock(IntCallback block); + +static inline void sendLogToUI(char *s, char *level) { + @autoreleasepool { + if (logCallback) { + logCallback([NSString stringWithUTF8String:s], [NSString stringWithUTF8String:level]); + } + } +} + +static inline void sendTrafficToUI(int64_t up, int64_t down) { + if (trafficCallback) { + trafficCallback(up, down); + } +} diff --git a/ClashX/goClash/UIHelper.m b/ClashX/goClash/UIHelper.m new file mode 100644 index 000000000..eaf4e3f8e --- /dev/null +++ b/ClashX/goClash/UIHelper.m @@ -0,0 +1,10 @@ +#import "UIHelper.h" +NStringCallback logCallback; +IntCallback trafficCallback; +void clash_setLogBlock(NStringCallback block) { + logCallback = [block copy]; +} + +void clash_setTrafficBlock(IntCallback block) { + trafficCallback = [block copy]; +} \ No newline at end of file diff --git a/ClashX/goClash/build.sh b/ClashX/goClash/build.sh index 469ac2b5b..62b8d0618 100755 --- a/ClashX/goClash/build.sh +++ b/ClashX/goClash/build.sh @@ -1,2 +1 @@ -rm -f *.h *.a -python3 build_clash.py +python3 build_clash_universal.py diff --git a/ClashX/goClash/build_clash.py b/ClashX/goClash/build_clash.py deleted file mode 100644 index df5a7e344..000000000 --- a/ClashX/goClash/build_clash.py +++ /dev/null @@ -1,55 +0,0 @@ -import subprocess -import datetime -import plistlib -import os - -def get_version(): - with open('./go.mod') as file: - for line in file.readlines(): - if "clash" in line and "ClashX" not in line: - return line.split("-")[-1].strip()[:6] - return "unknown" - -def get_full_version(): - with open('./go.mod') as file: - for line in file.readlines(): - if "clash" in line and "ClashX" not in line: - return line.split(" ")[-1].strip() - -def build_clash(version): - build_time = datetime.datetime.now().strftime("%Y-%m-%d-%H%M") - command = f"""CGO_CFLAGS=-mmacosx-version-min=10.12 \ -CGO_LDFLAGS=-mmacosx-version-min=10.12 \ -go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version={version}" \ --X "github.com/Dreamacro/clash/constant.BuildTime={build_time}"' \ --buildmode=c-archive -o goClash.a """ - subprocess.check_output(command, shell=True) - - -def write_to_info(version): - path = "../info.plist" - - with open(path, 'rb') as f: - contents = plistlib.load(f) - - if not contents: - exit(-1) - - contents["coreVersion"] = version - with open(path, 'wb') as f: - plistlib.dump(contents, f, sort_keys=False) - - -def run(): - version = get_version() - print("current clash version:", version) - build_clash(version) - print("build static library complete!") - if os.environ.get("CI", False) or os.environ.get("GITHUB_ACTIONS", False): - print("writing info.plist") - write_to_info(version) - print("done") - - -if __name__ == "__main__": - run() diff --git a/ClashX/goClash/build_clash_universal.py b/ClashX/goClash/build_clash_universal.py index 143503ada..2a153f163 100644 --- a/ClashX/goClash/build_clash_universal.py +++ b/ClashX/goClash/build_clash_universal.py @@ -11,22 +11,20 @@ def get_version(): return line.split("-")[-1].strip()[:6] return "unknown" +go_bin = "go" def build_clash(version,build_time,arch): - clang = f"{os.getcwd()}/clangWrap.sh" command = f""" -go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version={version}" \ +{go_bin} build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version={version}" \ -X "github.com/Dreamacro/clash/constant.BuildTime={build_time}"' \ -buildmode=c-archive -o goClash_{arch}.a """ envs = os.environ.copy() envs.update({ - "CC":clang, - "CXX":clang, "GOOS":"darwin", "GOARCH":arch, "CGO_ENABLED":"1", - "CGO_LDFLAGS":"-mmacosx-version-min=10.12", - "CGO_CFLAGS":"-mmacosx-version-min=10.12", + "CGO_LDFLAGS":"-mmacosx-version-min=10.14", + "CGO_CFLAGS":"-mmacosx-version-min=10.14", }) subprocess.check_output(command, shell=True,env=envs) @@ -61,8 +59,7 @@ def run(): print("current clash version:", version) build_time = datetime.datetime.now().strftime("%Y-%m-%d-%H%M") print("clean existing") - subprocess.check_output("rm -f *.h *.a", shell=True) - + subprocess.check_output("rm -f *Clash*.h *.a", shell=True) print("create arm64") build_clash(version,build_time,"arm64") print("create amd64") diff --git a/ClashX/goClash/clangWrap.sh b/ClashX/goClash/clangWrap.sh deleted file mode 100755 index f4e21431d..000000000 --- a/ClashX/goClash/clangWrap.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -SDK=macosx -PLATFORM=macosx -if [ "$GOARCH" == "arm64" ]; then - CLANGARCH="arm64" -else - CLANGARCH="x86_64" -fi - -SDK_PATH=`xcrun --sdk $SDK --show-sdk-path` -CLANG=`xcrun --sdk $SDK --find clang` -exec "$CLANG" -arch $CLANGARCH -isysroot "$SDK_PATH" -mmacosx-version-min=10.12 "$@" diff --git a/ClashX/goClash/go.mod b/ClashX/goClash/go.mod index 099c0fe0e..45a7c6663 100644 --- a/ClashX/goClash/go.mod +++ b/ClashX/goClash/go.mod @@ -1,34 +1,46 @@ module github.com/yichengchen/clashX/ClashX +go 1.21 + +toolchain go1.21.0 + require ( - github.com/Dreamacro/clash v1.10.1-0.20220320033218-8c9e0b388437 - github.com/oschwald/geoip2-golang v1.6.1 + github.com/Dreamacro/clash v1.18.1-0.20230911035213-d034a408be42 + github.com/oschwald/geoip2-golang v1.9.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 ) require ( - github.com/Dreamacro/go-shadowsocks2 v0.1.7 // indirect - github.com/go-chi/chi/v5 v5.0.7 // indirect - github.com/go-chi/cors v1.2.0 // indirect - github.com/go-chi/render v1.0.1 // indirect - github.com/gofrs/uuid v4.2.0+incompatible // indirect + github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect + github.com/go-chi/cors v1.2.1 // indirect + github.com/go-chi/render v1.0.3 // indirect + github.com/gofrs/uuid/v5 v5.0.0 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/insomniacslk/dhcp v0.0.0-20220119180841-3c283ff8b7dd // indirect - github.com/miekg/dns v1.1.47 // indirect - github.com/oschwald/maxminddb-golang v1.8.0 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect - github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect - go.etcd.io/bbolt v1.3.6 // indirect - go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect - golang.org/x/mod v0.4.2 // indirect - golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/miekg/dns v1.1.55 // indirect + github.com/oschwald/maxminddb-golang v1.11.0 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/samber/lo v1.38.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect + github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 // indirect + github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect + go.etcd.io/bbolt v1.3.7 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect + golang.org/x/tools v0.6.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) - -go 1.18 diff --git a/ClashX/goClash/go.sum b/ClashX/goClash/go.sum index a4971c581..2250a336a 100644 --- a/ClashX/goClash/go.sum +++ b/ClashX/goClash/go.sum @@ -1,137 +1,87 @@ -github.com/Dreamacro/clash v1.10.1-0.20220320033218-8c9e0b388437 h1:vdg/f0ZmHliM88+UAkdqS7brXOAUQXBv1+c620XRxk4= -github.com/Dreamacro/clash v1.10.1-0.20220320033218-8c9e0b388437/go.mod h1:Qre43BqAGIKpvRMvLB0cflDohlH2FmN/RrQZOlhi1jY= -github.com/Dreamacro/go-shadowsocks2 v0.1.7 h1:8CtbE1HoPPMfrQZGXmlluq6dO2lL31W6WRRE8fabc4Q= -github.com/Dreamacro/go-shadowsocks2 v0.1.7/go.mod h1:8p5G4cAj5ZlXwUR+Ww63gfSikr8kvw8uw3TDwLAJpUc= +github.com/Dreamacro/clash v1.18.1-0.20230911035213-d034a408be42 h1:1gOoRDxOn2wF5u+ku5rfpX5z0vuw1lS4NW0aGvjXnZE= +github.com/Dreamacro/clash v1.18.1-0.20230911035213-d034a408be42/go.mod h1:r//xe/2pA3Zl+3fjIiI/o6RjIVd+z87drCD58dpRnFg= +github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 h1:JFnwKplz9hj8ubqYjm8HkgZS1Rvz9yW+u/XCNNTxr0k= +github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158/go.mod h1:QvmEZ/h6KXszPOr2wUFl7Zn3hfFNYdfbXwPVDTyZs6k= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= -github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= -github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE= -github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= -github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= +github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= -github.com/insomniacslk/dhcp v0.0.0-20220119180841-3c283ff8b7dd h1:efcJu2Vzz6DoSq245deWNzTz6l/gsqdphm3FjmI88/g= -github.com/insomniacslk/dhcp v0.0.0-20220119180841-3c283ff8b7dd/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= -github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= -github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= -github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= -github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= -github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= -github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= -github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= -github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= -github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/miekg/dns v1.1.47 h1:J9bWiXbqMbnZPcY8Qi2E3EWIBsIm6MZzzJB9VRg5gL8= -github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/oschwald/geoip2-golang v1.6.1 h1:GKxT3yaWWNXSb7vj6D7eoJBns+lGYgx08QO0UcNm0YY= -github.com/oschwald/geoip2-golang v1.6.1/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s= -github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk= -github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis= +github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d h1:Ka64cclWedOkGzm9M2/XYuwJUdmWRUozmsxW0PyKA3A= +github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc= +github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y= +github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0= +github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= -github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 h1:F9xjJm4IH8VjcqG4ujciOF+GIM4mjPkHhWLLzOghPtM= +github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01/go.mod h1:cAAsePK2e15YDAMJNyOpGYEWNe4sIghTY7gpz4cX/Ik= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ClashX/goClash/main.go b/ClashX/goClash/main.go index 8e62c8b9b..63473f7fb 100644 --- a/ClashX/goClash/main.go +++ b/ClashX/goClash/main.go @@ -1,7 +1,15 @@ package main +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation +#import +#import "UIHelper.h" +*/ +import "C" + import ( - "C" + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -10,16 +18,23 @@ import ( "path/filepath" "strconv" "strings" + "time" + "unsafe" + "github.com/Dreamacro/clash/component/mmdb" "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/hub/executor" "github.com/Dreamacro/clash/hub/route" "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/tunnel/statistic" "github.com/oschwald/geoip2-golang" "github.com/phayes/freeport" ) +var secretOverride string = "" +var enableIPV6 bool = false + func isAddrValid(addr string) bool { if addr != "" { comps := strings.Split(addr, ":") @@ -76,41 +91,60 @@ func readConfig(path string) ([]byte, error) { return data, err } - -func parseDefaultConfigThenStart(checkPort, allowLan bool) (*config.Config, error) { +func getRawCfg() (*config.RawConfig, error) { buf, err := readConfig(constant.Path.Config()) if err != nil { return nil, err } - rawCfg, err := config.UnmarshalRawConfig(buf) + return config.UnmarshalRawConfig(buf) +} + +func parseDefaultConfigThenStart(checkPort, allowLan, ipv6 bool, proxyPort uint32, externalController string) (*config.Config, error) { + rawCfg, err := getRawCfg() if err != nil { return nil, err } - if rawCfg.MixedPort == 0 { - if rawCfg.Port > 0 { - rawCfg.MixedPort = rawCfg.Port + if proxyPort > 0 { + rawCfg.MixedPort = int(proxyPort) + if rawCfg.Port == rawCfg.MixedPort { rawCfg.Port = 0 - } else if rawCfg.SocksPort > 0 { - rawCfg.MixedPort = rawCfg.SocksPort - rawCfg.SocksPort = 0 - } else { - rawCfg.MixedPort = 7890 } - if rawCfg.SocksPort == rawCfg.MixedPort { rawCfg.SocksPort = 0 } + } else { + if rawCfg.MixedPort == 0 { + if rawCfg.Port > 0 { + rawCfg.MixedPort = rawCfg.Port + rawCfg.Port = 0 + } else if rawCfg.SocksPort > 0 { + rawCfg.MixedPort = rawCfg.SocksPort + rawCfg.SocksPort = 0 + } else { + rawCfg.MixedPort = 7890 + } - if rawCfg.Port == rawCfg.MixedPort { - rawCfg.Port = 0 - } + if rawCfg.SocksPort == rawCfg.MixedPort { + rawCfg.SocksPort = 0 + } + if rawCfg.Port == rawCfg.MixedPort { + rawCfg.Port = 0 + } + } + } + if secretOverride != "" { + rawCfg.Secret = secretOverride } - rawCfg.ExternalUI = "" rawCfg.Profile.StoreSelected = false + enableIPV6 = ipv6 + rawCfg.IPv6 = ipv6 + if len(externalController) > 0 { + rawCfg.ExternalController = externalController + } if checkPort { if !isAddrValid(rawCfg.ExternalController) { port, err := freeport.GetFreePort() @@ -153,9 +187,56 @@ func verifyClashConfig(content *C.char) *C.char { return C.CString("success") } +//export clashSetupLogger +func clashSetupLogger() { + sub := log.Subscribe() + go func() { + for elm := range sub { + log := elm.(log.Event) + cs := C.CString(log.Payload) + cl := C.CString(log.Type()) + C.sendLogToUI(cs, cl) + C.free(unsafe.Pointer(cs)) + C.free(unsafe.Pointer(cl)) + } + }() +} + +//export clashSetupTraffic +func clashSetupTraffic() { + go func() { + tick := time.NewTicker(time.Second) + defer tick.Stop() + t := statistic.DefaultManager + buf := &bytes.Buffer{} + for range tick.C { + buf.Reset() + up, down := t.Now() + C.sendTrafficToUI(C.longlong(up), C.longlong(down)) + } + }() +} + +//export clash_checkSecret +func clash_checkSecret() *C.char { + cfg, err := getRawCfg() + if err != nil { + return C.CString("") + } + if cfg.Secret != "" { + return C.CString(cfg.Secret) + } + return C.CString("") +} + +//export clash_setSecret +func clash_setSecret(secret *C.char) { + secretOverride = C.GoString(secret) +} + //export run -func run(checkConfig, allowLan bool) *C.char { - cfg, err := parseDefaultConfigThenStart(checkConfig, allowLan) +func run(checkConfig, allowLan, ipv6 bool, portOverride uint32, externalController *C.char) *C.char { + cfg, err := parseDefaultConfigThenStart(checkConfig, allowLan, ipv6, portOverride, C.GoString(externalController)) if err != nil { return C.CString(err.Error()) } @@ -184,6 +265,7 @@ func clashUpdateConfig(path *C.char) *C.char { if err != nil { return C.CString(err.Error()) } + cfg.General.IPv6 = enableIPV6 executor.ApplyConfig(cfg, false) return C.CString("success") } @@ -214,5 +296,27 @@ func verifyGEOIPDataBase() bool { return true } +//export clash_getCountryForIp +func clash_getCountryForIp(ip *C.char) *C.char { + record, _ := mmdb.Instance().Country(net.ParseIP(C.GoString(ip))) + if record != nil { + return C.CString(record.Country.IsoCode) + } + return C.CString("") +} + +//export clash_closeAllConnections +func clash_closeAllConnections() { + snapshot := statistic.DefaultManager.Snapshot() + for _, c := range snapshot.Connections { + c.Close() + } +} + +//export clash_getProggressInfo +func clash_getProggressInfo() *C.char { + return C.CString(GetTcpNetList() + GetUDpList()) +} + func main() { } diff --git a/ClashX/goClash/proccess.go b/ClashX/goClash/proccess.go new file mode 100644 index 000000000..b3e6b29bd --- /dev/null +++ b/ClashX/goClash/proccess.go @@ -0,0 +1,108 @@ +package main + +import ( + "encoding/binary" + "fmt" + "net/netip" + "strconv" + "strings" + "syscall" + "unsafe" +) + +const ( + procpidpathinfo = 0xb + procpidpathinfosize = 1024 + proccallnumpidinfo = 0x2 +) + +var structSize = func() int { + value, _ := syscall.Sysctl("kern.osrelease") + major, _, _ := strings.Cut(value, ".") + n, _ := strconv.ParseInt(major, 10, 64) + switch true { + case n >= 22: + return 408 + default: + // from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n + // size/offset are round up (aligned) to 8 bytes in darwin + // rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) + + // 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n)) + return 384 + } +}() + +func GetTcpNetList() string { + value, err := syscall.Sysctl("net.inet.tcp.pcblist_n") + if err != nil { + return "" + } + + buf := []byte(value) + itemSize := structSize + // tcp + // rup8(sizeof(xtcpcb_n)) + itemSize += 208 + + result := "" + for i := 24; i+itemSize <= len(buf); i += itemSize { + // offset of xinpcb_n and xsocket_n + inp, so := i, i+104 + srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20]) + // xinpcb_n.inp_vflag + flag := buf[inp+44] + + var srcIP netip.Addr + switch { + case flag&0x1 > 0: + // ipv4 + srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80])) + case flag&0x2 > 0: + // ipv6 + srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80])) + default: + continue + } + pid := readNativeUint32(buf[so+68 : so+72]) + result += fmt.Sprintf("%s %d %d\n", srcIP, srcPort, pid) + } + return result +} + +func GetUDpList() string { + value, err := syscall.Sysctl("net.inet.udp.pcblist_n") + if err != nil { + return "" + } + + buf := []byte(value) + itemSize := structSize + result := "" + + for i := 24; i+itemSize <= len(buf); i += itemSize { + // offset of xinpcb_n and xsocket_n + inp, so := i, i+104 + srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20]) + // xinpcb_n.inp_vflag + flag := buf[inp+44] + var srcIP netip.Addr + switch { + case flag&0x1 > 0: + // ipv4 + srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80])) + case flag&0x2 > 0: + // ipv6 + srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80])) + default: + continue + } + + pid := readNativeUint32(buf[so+68 : so+72]) + result += fmt.Sprintf("%s %d %d\n", srcIP, srcPort, pid) + } + return result +} + +func readNativeUint32(b []byte) uint32 { + return *(*uint32)(unsafe.Pointer(&b[0])) +} diff --git a/ClashX/goClash/upgrade_core.py b/ClashX/goClash/upgrade_core.py index e126b8ef0..b20fef28e 100644 --- a/ClashX/goClash/upgrade_core.py +++ b/ClashX/goClash/upgrade_core.py @@ -1,8 +1,5 @@ import subprocess -import os -import re -from build_clash import get_full_version -from build_clash import build_clash +from build_clash_universal import run def upgrade_version(current_version): @@ -12,6 +9,12 @@ def upgrade_version(current_version): file.write(string) +def get_full_version(): + with open('./go.mod') as file: + for line in file.readlines(): + if "clash" in line and "ClashX" not in line: + return line.split(" ")[-1].strip() + def install(): subprocess.check_output("go mod download", shell=True) subprocess.check_output("go mod tidy", shell=True) @@ -25,4 +28,4 @@ def install(): install() new_version = get_full_version() print("new version:", new_version, ",start building") - build_clash(new_version) + run() diff --git a/ClashX/zh-Hans.lproj/Main.strings b/ClashX/zh-Hans.lproj/Main.strings index a733fd28a..f19202567 100644 --- a/ClashX/zh-Hans.lproj/Main.strings +++ b/ClashX/zh-Hans.lproj/Main.strings @@ -67,16 +67,13 @@ /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "id7-f0-u56"; */ "id7-f0-u56.title" = "Text Cell"; -/* Class = "NSMenuItem"; title = "Show current proxy in menu"; ObjectID = "j9o-36-NTd"; */ -"j9o-36-NTd.title" = "快捷展示策略组策略"; - /* Class = "NSMenuItem"; title = "API Connect Error"; ObjectID = "jGT-1M-xJu"; */ "jGT-1M-xJu.title" = "API Connect Error"; /* Class = "NSMenuItem"; title = "Copy shell command"; ObjectID = "Jmb-PK-rMW"; */ "Jmb-PK-rMW.title" = "复制终端代理命令"; -/* Class = "NSMenuItem"; title = "Config"; ObjectID = "JMV-Dy-CI0"; */ +/* Class = "NSMenuItem"; title = "Configs"; ObjectID = "JMV-Dy-CI0"; */ "JMV-Dy-CI0.title" = "配置"; /* Class = "NSMenuItem"; title = "WARNING"; ObjectID = "ko2-Ir-DxA"; */ @@ -94,9 +91,6 @@ /* Class = "NSMenu"; title = "Help"; ObjectID = "ogW-pn-jeR"; */ "ogW-pn-jeR.title" = "帮助"; -/* Class = "NSMenuItem"; title = "Experimental"; ObjectID = "OLP-Uv-at6"; */ -"OLP-Uv-at6.title" = "试验性功能"; - /* Class = "NSMenuItem"; title = "Check Update"; ObjectID = "p0T-J8-Emx"; */ "p0T-J8-Emx.title" = "检查更新"; @@ -109,13 +103,7 @@ /* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "RCv-zz-HKW"; */ "RCv-zz-HKW.title" = "Text Cell"; -/* Class = "NSMenuItem"; title = "Set benchmark url"; ObjectID = "rls-O1-mpQ"; */ -"rls-O1-mpQ.title" = "设置延迟测速链接"; - -/* Class = "NSMenu"; title = "Experimental"; ObjectID = "sbS-Fj-gxn"; */ -"sbS-Fj-gxn.title" = "试验性功能"; - -/* Class = "NSMenu"; title = "Config"; ObjectID = "tck-zU-JKQ"; */ +/* Class = "NSMenu"; title = "Configs"; ObjectID = "tck-zU-JKQ"; */ "tck-zU-JKQ.title" = "配置"; /* Class = "NSTextFieldCell"; title = "Configs"; ObjectID = "tL1-bl-LXd"; */ @@ -130,9 +118,6 @@ /* Class = "NSMenu"; title = "Log level"; ObjectID = "wqo-3T-4qO"; */ "wqo-3T-4qO.title" = "日志等级"; -/* Class = "NSMenuItem"; title = "Use built in api"; ObjectID = "xG5-B4-mlw"; */ -"xG5-B4-mlw.title" = "使用内置接口"; - /* Class = "NSMenuItem"; title = "Dashboard"; ObjectID = "XG6-2M-PNi"; */ "XG6-2M-PNi.title" = "控制台"; @@ -178,10 +163,10 @@ /* Class = "NSTableColumn"; headerCell.title = "Api Url"; ObjectID = "yO6-uZ-IRv"; */ "yO6-uZ-IRv.headerCell.title" = "Api Url"; -/* Class = "NSMenu"; title = "Remote Controller"; ObjectID = "1He-Eq-fSy"; */ +/* Class = "NSMenu"; title = "Remote controller"; ObjectID = "1He-Eq-fSy"; */ "1He-Eq-fSy.title" = "远程控制器"; -/* Class = "NSMenuItem"; title = "Remote Controller"; ObjectID = "BRR-WK-aeP"; */ +/* Class = "NSMenuItem"; title = "Remote controller"; ObjectID = "BRR-WK-aeP"; */ "BRR-WK-aeP.title" = "远程控制器"; /* Class = "NSMenuItem"; title = " Manage"; ObjectID = "hlb-KQ-Fdr"; */ @@ -190,5 +175,149 @@ /* Class = "NSTextFieldCell"; title = "This allows you to control the clash core running in the different machine"; ObjectID = "WkL-aX-66E"; */ "WkL-aX-66E.title" = "远程控制器允许你控制其他设备上的 Clash 状态。"; -/* Class = "NSMenuItem"; title = "Set update Interval"; ObjectID = "h1H-7k-9HS"; */ +/* Class = "NSMenuItem"; title = "Set update interval"; ObjectID = "h1H-7k-9HS"; */ "h1H-7k-9HS.title" = "设置更新间隔"; + +/* Class = "NSTabViewController"; title = "Settings"; ObjectID = "LAj-p8-9gd"; */ +"LAj-p8-9gd.title" = "ClashX 设置"; + +/* Class = "NSTabViewItem"; label = "General"; ObjectID = "Ltt-Vq-Hh1"; */ +"Ltt-Vq-Hh1.label" = "通用"; + +/* Class = "NSTextFieldCell"; title = "Bypass proxy settings for these Hosts & Domains"; ObjectID = "NPu-V9-f3r"; */ +"NPu-V9-f3r.title" = "忽略这些主机与域的代理设置"; + +/* Class = "NSButtonCell"; title = "Launch at login"; ObjectID = "dV6-4Z-2SO"; */ +"dV6-4Z-2SO.title" = "开机启动"; + +/* Class = "NSViewController"; title = "General"; ObjectID = "kma-mp-ncL"; */ +"kma-mp-ncL.title" = "通用"; + +/* Class = "NSMenuItem"; title = "Settings"; ObjectID = "krh-QF-pqZ"; */ +"krh-QF-pqZ.title" = "更多设置"; + +/* Class = "NSButtonCell"; title = "Use iCloud to store config files"; ObjectID = "p7q-KN-kIv"; */ +"p7q-KN-kIv.title" = "将配置文件存储在iCloud中"; + +/* Class = "NSButtonCell"; title = "Reduce notifications"; ObjectID = "jsL-HC-6ne"; */ +"jsL-HC-6ne.title" = "减少通知"; + +/* Class = "NSTextFieldCell"; title = "Separated by commas(,)"; ObjectID = "sfe-wu-UXp"; */ +"sfe-wu-UXp.title" = "使用英文逗号(,)分隔"; + +/* Class = "NSButtonCell"; title = "Allow control from lan"; ObjectID = "E8B-e5-K0A"; */ +"E8B-e5-K0A.title" = "允许局域网控制(不推荐)"; + +/* Class = "NSBox"; title = "Clash Settings (restart app to take effect)"; ObjectID = "bcR-rG-52F"; */ +"bcR-rG-52F.title" = "Clash设置(重启应用生效)"; + +/* Class = "NSTextFieldCell"; title = "Api Port:"; ObjectID = "mbn-tK-UQa"; */ +"mbn-tK-UQa.title" = "Api 端口"; + +/* Class = "NSTextFieldCell"; title = "Proxy Port:"; ObjectID = "uUA-LS-Hu8"; */ +"uUA-LS-Hu8.title" = "代理端口"; + +/* Class = "NSButtonCell"; title = "Reset"; ObjectID = "yXh-2Y-aTS"; */ +"yXh-2Y-aTS.title" = "重置"; + +/* Class = "NSBox"; title = "Box"; ObjectID = "Gnh-m8-PAz"; */ +"Gnh-m8-PAz.title" = "Box"; + +/* Class = "NSTextFieldCell"; title = "SSID Suspend"; ObjectID = "F1s-SF-dqX"; */ +"F1s-SF-dqX.title" = "在特定 WiFi SSID 下自动暂停"; + +/* Class = "NSTextFieldCell"; title = "Separated by commas(,)"; ObjectID = "xnL-ma-vFo"; */ +"xnL-ma-vFo.title" = "使用英文逗号(,)分隔"; + +/* Class = "NSTabViewItem"; label = "Debug"; ObjectID = "3Om-yT-A83"; */ +"3Om-yT-A83.label" = "调试"; + +/* Class = "NSBox"; title = "Debug Setting"; ObjectID = "NLT-FZ-48V"; */ +"NLT-FZ-48V.title" = "调试设置"; + +/* Class = "NSViewController"; title = "Debug"; ObjectID = "kdV-Em-qBi"; */ +"kdV-Em-qBi.title" = "调试"; + +/* Class = "NSBox"; title = "Mode"; ObjectID = "1d5-NL-UsJ"; */ +"1d5-NL-UsJ.title" = "代理模式"; + +/* Class = "NSBox"; title = "Other"; ObjectID = "AMT-oc-r8A"; */ +"AMT-oc-r8A.title" = "其他"; + +/* Class = "NSBox"; title = "Proxy"; ObjectID = "BFE-Qq-B2H"; */ +"BFE-Qq-B2H.title" = "代理设置"; + +/* Class = "NSViewController"; title = "Global ShortCut"; ObjectID = "wKZ-TE-sf8"; */ +"wKZ-TE-sf8.title" = "全局快捷键"; + +/* Class = "NSTextFieldCell"; title = "Please ensure to address any potential shortcut conflicts. Global shortcuts take precedence over regular shortcuts."; ObjectID = "GGx-F2-7kE"; */ +"GGx-F2-7kE.title" = "请务必注意处理可能存在的快捷键冲突,全局快捷键将优先于普通快捷键。"; + +/* Class = "NSButtonCell"; title = "Uninstall Proxy Helper"; ObjectID = "AY0-nP-cGT"; */ +"AY0-nP-cGT.title" = "移除助手程序"; + +/* Class = "NSTextFieldCell"; title = "Proxy Helper"; ObjectID = "aSG-9A-eeG"; */ +"aSG-9A-eeG.title" = "助手程序"; + +/* Class = "NSButtonCell"; title = "Open Log Folder"; ObjectID = "afj-4G-usr"; */ +"afj-4G-usr.title" = "打开日志文件夹"; + +/* Class = "NSTextFieldCell"; title = "Log"; ObjectID = "c01-0L-1SQ"; */ +"c01-0L-1SQ.title" = "日志"; + +/* Class = "NSButtonCell"; title = "Override Config Setting"; ObjectID = "LW4-cA-3bB"; */ +"LW4-cA-3bB.title" = "覆盖配置文件设置"; + +/* Class = "NSTextFieldCell"; title = "Api Secret:"; ObjectID = "ckH-Er-PfX"; */ +"ckH-Er-PfX.title" = "Api秘钥"; + +/* Class = "NSButtonCell"; title = "iCloud"; ObjectID = "URV-fZ-bJf"; */ +"URV-fZ-bJf.title" = "iCloud"; + +/* Class = "NSButtonCell"; title = "Local"; ObjectID = "YF4-uZ-A0M"; */ +"YF4-uZ-A0M.title" = "本地"; + +/* Class = "NSTextFieldCell"; title = "Config Folder"; ObjectID = "ZA9-qc-wi4"; */ +"ZA9-qc-wi4.title" = "配置文件夹"; + +/* Class = "NSButtonCell"; title = "Reset"; ObjectID = "5ws-55-f1g"; */ +"5ws-55-f1g.title" = "重置"; + +/* Class = "NSTextFieldCell"; title = "App Setting"; ObjectID = "PF0-Gd-XbR"; */ +"PF0-Gd-XbR.title" = "应用配置"; + +/* Class = "NSMenuItem"; title = "Connection Details"; ObjectID = "v4s-jd-g1N"; */ +"v4s-jd-g1N.title" = "连接查看器"; + +/* Class = "NSButtonCell"; title = "Use Built-in clash api"; ObjectID = "IET-Bf-hGj"; */ +"IET-Bf-hGj.title" = "使用Clash内置Api"; + +/* Class = "NSButtonCell"; title = "Revert to previous proxy on proxy disabled"; ObjectID = "JOF-yU-YHc"; */ +"JOF-yU-YHc.title" = "取消代理时还原更早的系统代理设置"; + +/* Class = "NSTextFieldCell"; title = "Update Channel"; ObjectID = "NLt-OM-k6i"; */ +"NLt-OM-k6i.title" = "更新通道"; + +/* Class = "NSTextFieldCell"; title = "Geo-IP Datebase"; ObjectID = "OMy-zu-iho"; */ +"OMy-zu-iho.title" = "Geo-IP数据库"; + +/* Class = "NSTextFieldCell"; title = "Proxy Benchmark Url:"; ObjectID = "e0M-xf-ovR"; */ +"e0M-xf-ovR.title" = "代理延迟测试地址"; + +/* Class = "NSButtonCell"; title = "Update"; ObjectID = "pHl-C4-fNt"; */ +"pHl-C4-fNt.title" = "更新"; + +/* Class = "NSMenuItem"; title = "Update external resources"; ObjectID = "9g1-lW-mA8"; */ +"9g1-lW-mA8.title" = "更新外部资源"; + +/* Class = "NSButtonCell"; title = "Enable IPv6"; ObjectID = "KRm-2U-T4s"; */ +"KRm-2U-T4s.title" = "启用IPv6"; + +/* Class = "NSMenuItem"; title = "Quit ClashX"; ObjectID = "TsN-g8-OjG"; */ +"TsN-g8-OjG.title" = "退出 ClashX"; + +/* Class = "NSTextFieldCell"; title = "Crash Log Folder"; ObjectID = "LFb-xY-CNV"; */ +"LFb-xY-CNV.title" = "崩溃日志文件夹"; + +/* Class = "NSButtonCell"; title = "Open"; ObjectID = "Wci-1w-zlx"; */ +"Wci-1w-zlx.title" = "打开"; diff --git a/ClashX/zh-Hant.lproj/Main.strings b/ClashX/zh-Hant.lproj/Main.strings new file mode 100644 index 000000000..556a07ab7 --- /dev/null +++ b/ClashX/zh-Hant.lproj/Main.strings @@ -0,0 +1,323 @@ +/* Class = "NSMenu"; title = "Configs"; ObjectID = "tck-zU-JKQ"; */ +"tck-zU-JKQ.title" = "配置"; + +/* Class = "NSTextFieldCell"; title = "Configs"; ObjectID = "tL1-bl-LXd"; */ +"tL1-bl-LXd.title" = "託管的配置文件:"; + +/* Class = "NSMenu"; title = "API Connect Error"; ObjectID = "UU2-uE-YB4"; */ +"UU2-uE-YB4.title" = "API Connect Error"; + +/* Class = "NSMenuItem"; title = "Allow connect from Lan"; ObjectID = "Vz8-7n-vx6"; */ +"Vz8-7n-vx6.title" = "允許局域網連接"; + +/* Class = "NSMenu"; title = "Log level"; ObjectID = "wqo-3T-4qO"; */ +"wqo-3T-4qO.title" = "日誌等級"; + +/* Class = "NSMenuItem"; title = "Dashboard"; ObjectID = "XG6-2M-PNi"; */ +"XG6-2M-PNi.title" = "控制台"; + +/* Class = "NSMenuItem"; title = "ERROR"; ObjectID = "0iu-lB-eZN"; */ +"0iu-lB-eZN.title" = "錯誤"; + +/* Class = "NSButtonCell"; title = "Update"; ObjectID = "2Rx-ih-aGW"; */ +"2Rx-ih-aGW.title" = "更新"; + +/* Class = "NSMenuItem"; title = "Log level"; ObjectID = "3Da-fL-Mzr"; */ +"3Da-fL-Mzr.title" = "日誌等級"; + +/* Class = "NSButtonCell"; title = "Add"; ObjectID = "51K-nB-xLS"; */ +"51K-nB-xLS.title" = "添加"; + +/* Class = "NSViewController"; title = "Remote Configs"; ObjectID = "6WI-Hi-v9j"; */ +"6WI-Hi-v9j.title" = "託管的配置文件"; + +/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "7G6-oO-vNs"; */ +"7G6-oO-vNs.title" = "文本單元格"; + +/* Class = "NSMenuItem"; title = "Rule"; ObjectID = "89n-bD-JHk"; */ +"89n-bD-JHk.title" = "規則判斷"; + +/* Class = "NSMenuItem"; title = "Set as system proxy"; ObjectID = "8se-yr-wmp"; */ +"8se-yr-wmp.title" = "設定為系統代理"; + +/* Class = "NSMenuItem"; title = "Ports"; ObjectID = "9i0-LH-x04"; */ +"9i0-LH-x04.title" = "端口"; + +/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ +"AYu-sK-qS6.title" = "主菜單"; + +/* Class = "NSMenu"; title = "Remote config"; ObjectID = "az2-wz-yyy"; */ +"az2-wz-yyy.title" = "託管配置"; + +/* Class = "NSMenuItem"; title = "Start at login"; ObjectID = "B1J-XB-BiZ"; */ +"B1J-XB-BiZ.title" = "開機啟動"; + +/* Class = "NSTableColumn"; headerCell.title = "Url"; ObjectID = "C79-J5-30z"; */ +"C79-J5-30z.headerCell.title" = "鏈接"; + +/* Class = "NSMenuItem"; title = "Benchmark"; ObjectID = "COu-UX-bww"; */ +"COu-UX-bww.title" = "延遲測速"; + +/* Class = "NSMenuItem"; title = "INFO"; ObjectID = "d8V-h0-0zF"; */ +"d8V-h0-0zF.title" = "INFO"; + +/* Class = "NSMenuItem"; title = "Help"; ObjectID = "Dd9-2F-FVY"; */ +"Dd9-2F-FVY.title" = "幫助"; + +/* Class = "NSMenuItem"; title = "SILENT"; ObjectID = "dVr-Xp-C0C"; */ +"dVr-Xp-C0C.title" = "SILENT"; + +/* Class = "NSMenuItem"; title = "Open config folder"; ObjectID = "DwE-WX-ETZ"; */ +"DwE-WX-ETZ.title" = "打開配置文件夾"; + +/* Class = "NSMenuItem"; title = "Manage"; ObjectID = "Dwg-Qb-2AU"; */ +"Dwg-Qb-2AU.title" = "管理"; + +/* Class = "NSMenuItem"; title = "Remote config"; ObjectID = "h1C-R6-Y9w"; */ +"h1C-R6-Y9w.title" = "託管配置"; + +/* Class = "NSMenuItem"; title = "About"; ObjectID = "hUb-k9-TEf"; */ +"hUb-k9-TEf.title" = "關於"; + +/* Class = "NSMenuItem"; title = "Update"; ObjectID = "I2P-Wd-Ns7"; */ +"I2P-Wd-Ns7.title" = "更新"; + +/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "id7-f0-u56"; */ +"id7-f0-u56.title" = "Text Cell"; + +/* Class = "NSMenuItem"; title = "API Connect Error"; ObjectID = "jGT-1M-xJu"; */ +"jGT-1M-xJu.title" = "API Connect Error"; + +/* Class = "NSMenuItem"; title = "Copy shell command"; ObjectID = "Jmb-PK-rMW"; */ +"Jmb-PK-rMW.title" = "複製終端代理命令"; + +/* Class = "NSMenuItem"; title = "Configs"; ObjectID = "JMV-Dy-CI0"; */ +"JMV-Dy-CI0.title" = "配置"; + +/* Class = "NSMenuItem"; title = "WARNING"; ObjectID = "ko2-Ir-DxA"; */ +"ko2-Ir-DxA.title" = "WARNING"; + +/* Class = "NSTableColumn"; headerCell.title = "Config Name"; ObjectID = "lRE-Xa-euB"; */ +"lRE-Xa-euB.headerCell.title" = "配置文件名稱"; + +/* Class = "NSMenuItem"; title = "Direct"; ObjectID = "Np6-Pm-Lo3"; */ +"Np6-Pm-Lo3.title" = "直接連接"; + +/* Class = "NSMenuItem"; title = "Quit"; ObjectID = "NXU-86-Eem"; */ +"NXU-86-Eem.title" = "退出"; + +/* Class = "NSMenu"; title = "Help"; ObjectID = "ogW-pn-jeR"; */ +"ogW-pn-jeR.title" = "幫助"; + +/* Class = "NSMenuItem"; title = "Check Update"; ObjectID = "p0T-J8-Emx"; */ +"p0T-J8-Emx.title" = "檢查更新"; + +/* Class = "NSMenuItem"; title = "Reload config"; ObjectID = "q3G-VH-eyy"; */ +"q3G-VH-eyy.title" = "重載配置文件"; + +/* Class = "NSMenuItem"; title = "Auto Update"; ObjectID = "r8s-OI-tgf"; */ +"r8s-OI-tgf.title" = "自動更新"; + +/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "RCv-zz-HKW"; */ +"RCv-zz-HKW.title" = "Text Cell"; + +/* Class = "NSMenu"; title = "Remote controller"; ObjectID = "1He-Eq-fSy"; */ +"1He-Eq-fSy.title" = "遙控"; + +/* Class = "NSBox"; title = "Mode"; ObjectID = "1d5-NL-UsJ"; */ +"1d5-NL-UsJ.title" = "模式"; + +/* Class = "NSTabViewItem"; label = "Debug"; ObjectID = "3Om-yT-A83"; */ +"3Om-yT-A83.label" = "調試"; + +/* Class = "NSTableColumn"; headerCell.title = "Api Secret"; ObjectID = "5hn-k8-CWe"; */ +"5hn-k8-CWe.headerCell.title" = "Api Secret"; + +/* Class = "NSMenuItem"; title = "Copy shell command (External IP)"; ObjectID = "7wl-vK-5JO"; */ +"7wl-vK-5JO.title" = "複製shell命令(外網IP)"; + +/* Class = "NSTextFieldCell"; title = "External Controls"; ObjectID = "9gE-NX-2wJ"; */ +"9gE-NX-2wJ.title" = "外部控制"; + +/* Class = "NSBox"; title = "Other"; ObjectID = "AMT-oc-r8A"; */ +"AMT-oc-r8A.title" = "其他"; + +/* Class = "NSButtonCell"; title = "Uninstall Proxy Helper"; ObjectID = "AY0-nP-cGT"; */ +"AY0-nP-cGT.title" = "卸載代理程式"; + +/* Class = "NSButtonCell"; title = "Delete"; ObjectID = "B2w-4r-5Kh"; */ +"B2w-4r-5Kh.title" = "刪除"; + +/* Class = "NSBox"; title = "Proxy"; ObjectID = "BFE-Qq-B2H"; */ +"BFE-Qq-B2H.title" = "代理"; + +/* Class = "NSMenuItem"; title = "Remote controller"; ObjectID = "BRR-WK-aeP"; */ +"BRR-WK-aeP.title" = "遙控"; + +/* Class = "NSButtonCell"; title = "Allow control from lan"; ObjectID = "E8B-e5-K0A"; */ +"E8B-e5-K0A.title" = "允許局域網設備"; + +/* Class = "NSTextFieldCell"; title = "SSID Suspend"; ObjectID = "F1s-SF-dqX"; */ +"F1s-SF-dqX.title" = "SSID 掛起"; + +/* Class = "NSTextFieldCell"; title = "Please ensure to address any potential shortcut conflicts. Global shortcuts take precedence over regular shortcuts."; ObjectID = "GGx-F2-7kE"; */ +"GGx-F2-7kE.title" = "請確保解決任何潛在的快捷方式衝突,全局快捷方式優先於常規快捷方式"; + +/* Class = "NSBox"; title = "Box"; ObjectID = "Gnh-m8-PAz"; */ +"Gnh-m8-PAz.title" = "盒子"; + +/* Class = "NSTabViewController"; title = "Settings"; ObjectID = "LAj-p8-9gd"; */ +"LAj-p8-9gd.title" = "設定"; + +/* Class = "NSTabViewItem"; label = "General"; ObjectID = "Ltt-Vq-Hh1"; */ +"Ltt-Vq-Hh1.label" = "一般設定"; + +/* Class = "NSBox"; title = "Debug Setting"; ObjectID = "NLT-FZ-48V"; */ +"NLT-FZ-48V.title" = "調試設定"; + +/* Class = "NSTextFieldCell"; title = "Bypass proxy settings for these Hosts & Domains"; ObjectID = "NPu-V9-f3r"; */ +"NPu-V9-f3r.title" = "繞過這些主機和域的代理設定"; + +/* Class = "NSTextFieldCell"; title = "This allows you to control the clash core running in the different machine"; ObjectID = "WkL-aX-66E"; */ +"WkL-aX-66E.title" = "這允許你控制在不同機器上運行的衝突核心"; + +/* Class = "NSMenuItem"; title = "DEBUG"; ObjectID = "XIR-Go-fWA"; */ +"XIR-Go-fWA.title" = "調試"; + +/* Class = "NSMenuItem"; title = "Show network indicator"; ObjectID = "YIO-Vj-64f"; */ +"YIO-Vj-64f.title" = "顯示網絡指示器"; + +/* Class = "NSButtonCell"; title = "Add"; ObjectID = "ZcF-10-jsl"; */ +"ZcF-10-jsl.title" = "添加"; + +/* Class = "NSTextFieldCell"; title = "Proxy Helper"; ObjectID = "aSG-9A-eeG"; */ +"aSG-9A-eeG.title" = "代理助手"; + +/* Class = "NSButtonCell"; title = "Open Log Folder"; ObjectID = "afj-4G-usr"; */ +"afj-4G-usr.title" = "打開日誌文件夾"; + +/* Class = "NSBox"; title = "Clash Settings (restart app to take effect)"; ObjectID = "bcR-rG-52F"; */ +"bcR-rG-52F.title" = "Clash設定"; + +/* Class = "NSTextFieldCell"; title = "Log"; ObjectID = "c01-0L-1SQ"; */ +"c01-0L-1SQ.title" = "日誌"; + +/* Class = "NSButtonCell"; title = "Launch at login"; ObjectID = "dV6-4Z-2SO"; */ +"dV6-4Z-2SO.title" = "登錄時啟動"; + +/* Class = "NSMenuItem"; title = "Set update interval"; ObjectID = "h1H-7k-9HS"; */ +"h1H-7k-9HS.title" = "設定更新間隔"; + +/* Class = "NSMenuItem"; title = " Manage"; ObjectID = "hlb-KQ-Fdr"; */ +"hlb-KQ-Fdr.title" = "管理"; + +/* Class = "NSButtonCell"; title = "Reduce notifications"; ObjectID = "jsL-HC-6ne"; */ +"jsL-HC-6ne.title" = "減少通知"; + +/* Class = "NSViewController"; title = "Debug"; ObjectID = "kdV-Em-qBi"; */ +"kdV-Em-qBi.title" = "調試"; + +/* Class = "NSViewController"; title = "General"; ObjectID = "kma-mp-ncL"; */ +"kma-mp-ncL.title" = "一般設定"; + +/* Class = "NSMenuItem"; title = "Settings"; ObjectID = "krh-QF-pqZ"; */ +"krh-QF-pqZ.title" = "設定"; + +/* Class = "NSTextFieldCell"; title = "Api Port:"; ObjectID = "mbn-tK-UQa"; */ +"mbn-tK-UQa.title" = "API 端口"; + +/* Class = "NSButtonCell"; title = "Use iCloud to store config files"; ObjectID = "p7q-KN-kIv"; */ +"p7q-KN-kIv.title" = "使用iCloud 存儲配置文件"; + +/* Class = "NSViewController"; title = "External Manager"; ObjectID = "s6y-wL-pnr"; */ +"s6y-wL-pnr.title" = "外部管理"; + +/* Class = "NSTextFieldCell"; title = "Separated by commas(,)"; ObjectID = "sfe-wu-UXp"; */ +"sfe-wu-UXp.title" = "以逗號(,)分隔"; + +/* Class = "NSTextFieldCell"; title = "Proxy Port:"; ObjectID = "uUA-LS-Hu8"; */ +"uUA-LS-Hu8.title" = "代理端口"; + +/* Class = "NSViewController"; title = "Global ShortCut"; ObjectID = "wKZ-TE-sf8"; */ +"wKZ-TE-sf8.title" = "全局快捷方式"; + +/* Class = "NSTextFieldCell"; title = "Separated by commas(,)"; ObjectID = "xnL-ma-vFo"; */ +"xnL-ma-vFo.title" = "以逗號(,)分隔"; + +/* Class = "NSTableColumn"; headerCell.title = "Update Time"; ObjectID = "xoc-hs-9qa"; */ +"xoc-hs-9qa.headerCell.title" = "更新時間"; + +/* Class = "NSMenuItem"; title = "Show Log"; ObjectID = "xxZ-9l-69m"; */ +"xxZ-9l-69m.title" = "顯示日誌"; + +/* Class = "NSButtonCell"; title = "Delete"; ObjectID = "yGD-AG-oYU"; */ +"yGD-AG-oYU.title" = "刪除"; + +/* Class = "NSTableColumn"; headerCell.title = "Api Url"; ObjectID = "yO6-uZ-IRv"; */ +"yO6-uZ-IRv.headerCell.title" = "API 網址"; + +/* Class = "NSButtonCell"; title = "Reset"; ObjectID = "yXh-2Y-aTS"; */ +"yXh-2Y-aTS.title" = "重置"; + +/* Class = "NSMenuItem"; title = "Global"; ObjectID = "yiM-U4-MNg"; */ +"yiM-U4-MNg.title" = "全局"; + +/* Class = "NSTextFieldCell"; title = "Core Version"; ObjectID = "zwo-q5-k5N"; */ +"zwo-q5-k5N.title" = "核心版本"; + +/* Class = "NSButtonCell"; title = "Override Config Setting"; ObjectID = "LW4-cA-3bB"; */ +"LW4-cA-3bB.title" = "覆蓋配置文件設置"; + +/* Class = "NSTextFieldCell"; title = "Api Secret:"; ObjectID = "ckH-Er-PfX"; */ +"ckH-Er-PfX.title" = "API 秘鑰"; + +/* Class = "NSButtonCell"; title = "iCloud"; ObjectID = "URV-fZ-bJf"; */ +"URV-fZ-bJf.title" = "iCloud"; + +/* Class = "NSButtonCell"; title = "Local"; ObjectID = "YF4-uZ-A0M"; */ +"YF4-uZ-A0M.title" = "本地"; + +/* Class = "NSTextFieldCell"; title = "Config Folder"; ObjectID = "ZA9-qc-wi4"; */ +"ZA9-qc-wi4.title" = "配置文件夾"; + +/* Class = "NSButtonCell"; title = "Reset"; ObjectID = "5ws-55-f1g"; */ +"5ws-55-f1g.title" = "重置"; + +/* Class = "NSTextFieldCell"; title = "App Setting"; ObjectID = "PF0-Gd-XbR"; */ +"PF0-Gd-XbR.title" = "應用配置"; + +/* Class = "NSMenuItem"; title = "Connection Details"; ObjectID = "v4s-jd-g1N"; */ +"v4s-jd-g1N.title" = "連接詳情"; + +/* Class = "NSButtonCell"; title = "Use Built-in clash api"; ObjectID = "IET-Bf-hGj"; */ +"IET-Bf-hGj.title" = "使用內建 Clash API"; + +/* Class = "NSButtonCell"; title = "Revert to previous proxy on proxy disabled"; ObjectID = "JOF-yU-YHc"; */ +"JOF-yU-YHc.title" = "停用代理后恢复先前的代理设置"; + +/* Class = "NSTextFieldCell"; title = "Update Channel"; ObjectID = "NLt-OM-k6i"; */ +"NLt-OM-k6i.title" = "更新通道"; + +/* Class = "NSTextFieldCell"; title = "Geo-IP Datebase"; ObjectID = "OMy-zu-iho"; */ +"OMy-zu-iho.title" = "Geo-IP 数据库"; + +/* Class = "NSTextFieldCell"; title = "Proxy Benchmark Url:"; ObjectID = "e0M-xf-ovR"; */ +"e0M-xf-ovR.title" = "代理延遲測試地址"; + +/* Class = "NSButtonCell"; title = "Update"; ObjectID = "pHl-C4-fNt"; */ +"pHl-C4-fNt.title" = "更新"; + +/* Class = "NSMenuItem"; title = "Update external resources"; ObjectID = "9g1-lW-mA8"; */ +"9g1-lW-mA8.title" = "更新外部資源"; + +/* Class = "NSButtonCell"; title = "Enable IPv6"; ObjectID = "KRm-2U-T4s"; */ +"KRm-2U-T4s.title" = "啟用IPv6"; + +/* Class = "NSMenuItem"; title = "Quit ClashX"; ObjectID = "TsN-g8-OjG"; */ +"TsN-g8-OjG.title" = "退出 ClashX"; + +/* Class = "NSTextFieldCell"; title = "Crash Log Folder"; ObjectID = "LFb-xY-CNV"; */ +"LFb-xY-CNV.title" = "崩潰日誌文件夾"; + +/* Class = "NSButtonCell"; title = "Open"; ObjectID = "Wci-1w-zlx"; */ +"Wci-1w-zlx.title" = "打開"; diff --git a/Gemfile b/Gemfile index e4b5be5c3..6a51e25e7 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,6 @@ source "https://rubygems.org" gem 'fastlane' gem 'cocoapods' - +gem "activesupport", "= 7.0.8" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 85d7892ae..e16815078 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,47 +1,47 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.3) - activesupport (6.1.4.1) + CFPropertyList (3.0.6) + rexml + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.500.0) - aws-sdk-core (3.121.0) + aws-partitions (1.835.0) + aws-sdk-core (3.185.1) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) - aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.48.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.72.0) + aws-sdk-core (~> 3, >= 3.184.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.102.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-s3 (1.136.0) + aws-sdk-core (~> 3, >= 3.181.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - claide (1.0.3) - cocoapods (1.11.0) + claide (1.1.0) + cocoapods (1.13.0) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.0) + cocoapods-core (= 1.13.0) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-downloader (>= 1.6.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) @@ -49,10 +49,10 @@ GEM gh_inspector (~> 1.0) molinillo (~> 0.8.0) nap (~> 1.0) - ruby-macho (>= 1.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.0) - activesupport (>= 5.0, < 7) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.13.0) + activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) concurrent-ruby (~> 1.1) @@ -62,7 +62,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.5.1) + cocoapods-downloader (1.6.3) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -74,28 +74,29 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.1.9) + concurrent-ruby (1.2.2) declarative (0.0.20) - digest-crc (0.6.4) + digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.6) - emoji_regex (3.2.2) + dotenv (2.8.1) + emoji_regex (3.2.3) escape (0.0.4) - ethon (0.14.0) + ethon (0.16.0) ffi (>= 1.15.0) - excon (0.85.0) - faraday (1.7.1) + excon (0.104.0) + faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) + faraday-net_http_persistent (~> 1.0) faraday-patron (~> 1.0) faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.7) faraday (>= 0.8.0) @@ -104,14 +105,17 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.1.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.5) - fastlane (2.193.1) + fastimage (2.2.7) + fastlane (2.216.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -132,10 +136,11 @@ GEM google-apis-playcustomapp_v1 (~> 0.1) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) + multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) optparse (~> 0.1.1) plist (>= 3.1.0, < 4.0.0) @@ -143,23 +148,23 @@ GEM security (= 0.1.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-appcenter (1.11.0) + fastlane-plugin-appcenter (2.1.1) fastlane-plugin-update_xcodeproj (1.0.1) - fastlane-plugin-versioning (0.4.4) - ffi (1.15.4) + fastlane-plugin-versioning (0.5.2) + ffi (1.16.3) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.11.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.1) + google-apis-androidpublisher_v3 (0.50.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.1) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -168,97 +173,95 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.7.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.5.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.6.0) - google-apis-core (>= 0.4, < 2.a) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.5.0) - faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.1.0) - google-cloud-storage (1.34.1) - addressable (~> 2.5) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.44.0) + addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.19.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.17.1) - faraday (>= 0.17.3, < 2.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.15) + signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.8.10) + i18n (1.14.1) concurrent-ruby (~> 1.0) - jmespath (1.4.0) - json (2.5.1) - jwt (2.2.3) - memoist (0.16.2) - mini_magick (4.11.0) - mini_mime (1.1.1) - minitest (5.14.4) + jmespath (1.6.2) + json (2.6.3) + jwt (2.7.1) + mini_magick (4.12.0) + mini_mime (1.1.5) + minitest (5.20.0) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.0.0) + multipart-post (2.3.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) optparse (0.1.1) - os (1.1.1) - plist (3.6.0) - public_suffix (4.0.6) + os (1.1.4) + plist (3.7.0) + public_suffix (4.0.7) rake (13.0.6) - representable (3.1.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.6) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.16.0) + signet (0.18.0) addressable (~> 2.8) - faraday (>= 0.17.3, < 2.0) + faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.8) + simctl (1.6.10) CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) tty-cursor (~> 0.7) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (2.0.4) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.7) - unicode-display_width (1.7.0) - webrick (1.7.0) + unf_ext (0.0.8.2) + unicode-display_width (2.5.0) + webrick (1.8.1) word_wrap (1.0.0) - xcodeproj (1.21.0) + xcodeproj (1.23.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -269,12 +272,12 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.4.2) PLATFORMS ruby DEPENDENCIES + activesupport (= 7.0.8) cocoapods fastlane fastlane-plugin-appcenter @@ -282,4 +285,4 @@ DEPENDENCIES fastlane-plugin-versioning BUNDLED WITH - 2.2.27 + 2.2.32 diff --git a/Podfile b/Podfile index 17ad581b6..243b43b68 100644 --- a/Podfile +++ b/Podfile @@ -8,8 +8,8 @@ post_install do |installer| config.build_settings['SWIFT_VERSION'] = '5' end end - if config.build_settings['MACOSX_DEPLOYMENT_TARGET'] == '' - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.10' + if config.build_settings['MACOSX_DEPLOYMENT_TARGET'] == '' || Gem::Version.new(config.build_settings['MACOSX_DEPLOYMENT_TARGET']) < Gem::Version.new("10.14") + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.14' end end end @@ -24,11 +24,13 @@ target 'ClashX' do pod 'RxSwift' pod 'RxCocoa' pod 'CocoaLumberjack/Swift' - pod 'WebViewJavascriptBridge' pod 'Starscream','3.1.1' pod 'AppCenter/Analytics' - pod 'Sparkle','~>1.0' + pod 'AppCenter/Crashes' + pod 'Sparkle','~>2.0' pod "FlexibleDiff" pod 'GzipSwift' + pod 'SwiftLint' + pod 'SwiftFormat/CLI', '~> 0.49' end diff --git a/Podfile.lock b/Podfile.lock index 15ec51fc2..b117f5aed 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,38 +1,43 @@ PODS: - - Alamofire (5.5.0) - - AppCenter/Analytics (4.4.1): + - Alamofire (5.8.0) + - AppCenter/Analytics (5.0.4): - AppCenter/Core - - AppCenter/Core (4.4.1) - - CocoaLumberjack/Core (3.7.4) - - CocoaLumberjack/Swift (3.7.4): + - AppCenter/Core (5.0.4) + - AppCenter/Crashes (5.0.4): + - AppCenter/Core + - CocoaLumberjack/Core (3.8.1) + - CocoaLumberjack/Swift (3.8.1): - CocoaLumberjack/Core - FlexibleDiff (0.0.9) - GzipSwift (5.1.1) - LetsMove (1.25) - - RxCocoa (6.5.0): - - RxRelay (= 6.5.0) - - RxSwift (= 6.5.0) - - RxRelay (6.5.0): - - RxSwift (= 6.5.0) - - RxSwift (6.5.0) - - Sparkle (1.27.1) + - RxCocoa (6.6.0): + - RxRelay (= 6.6.0) + - RxSwift (= 6.6.0) + - RxRelay (6.6.0): + - RxSwift (= 6.6.0) + - RxSwift (6.6.0) + - Sparkle (2.5.1) - Starscream (3.1.1) + - SwiftFormat/CLI (0.52.7) + - SwiftLint (0.53.0) - SwiftyJSON (5.0.1) - - WebViewJavascriptBridge (6.0.3) DEPENDENCIES: - Alamofire (~> 5.0) - AppCenter/Analytics + - AppCenter/Crashes - CocoaLumberjack/Swift - FlexibleDiff - GzipSwift - LetsMove - RxCocoa - RxSwift - - Sparkle (~> 1.0) + - Sparkle (~> 2.0) - Starscream (= 3.1.1) + - SwiftFormat/CLI (~> 0.49) + - SwiftLint - SwiftyJSON - - WebViewJavascriptBridge SPEC REPOS: trunk: @@ -47,24 +52,26 @@ SPEC REPOS: - RxSwift - Sparkle - Starscream + - SwiftFormat + - SwiftLint - SwiftyJSON - - WebViewJavascriptBridge SPEC CHECKSUMS: - Alamofire: 1c4fb5369c3fe93d2857c780d8bbe09f06f97e7c - AppCenter: b0b6f1190215b5f983c42934db718f3b46fff3c0 - CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646 + Alamofire: 0e92e751b3e9e66d7982db43919d01f313b8eb91 + AppCenter: 85c92db0759d2792a65eb61d6842d2e86611a49a + CocoaLumberjack: 5c7e64cdb877770859bddec4d3d5a0d7c9299df9 FlexibleDiff: b9ee9b8305b42c784f5dd40589203c97c55bbaa0 GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa LetsMove: 7b9fe44737707d984fbd3f47af46609a9a07b461 - RxCocoa: 94f817b71c07517321eb4f9ad299112ca8af743b - RxRelay: 1de1523e604c72b6c68feadedd1af3b1b4d0ecbd - RxSwift: 5710a9e6b17f3c3d6e40d6e559b9fa1e813b2ef8 - Sparkle: 23f98b268284c8c03e6228230fc8f1807ef041d5 + RxCocoa: 44a80de90e25b739b5aeaae3c8c371a32e3343cc + RxRelay: 45eaa5db8ee4fb50e5ebd57deec0159e97fa51e6 + RxSwift: a4b44f7d24599f674deebd1818eab82e58410632 + Sparkle: ce9957501a2655dd4c8264312c6134ff478a777c Starscream: 4bb2f9942274833f7b4d296a55504dcfc7edb7b0 + SwiftFormat: 2c4785ad647322b41e027b9d4df160aef526a656 + SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e - WebViewJavascriptBridge: 7f5bc4d3581e672e8f32bd0f812d54bc69bb8e29 -PODFILE CHECKSUM: 3d889ed6c786fdaa0a2b35a70d4c5abcb240da2b +PODFILE CHECKSUM: 8c6ef0c5999141047a530cd8b74a41d2161ae11b -COCOAPODS: 1.11.2 +COCOAPODS: 1.13.0 diff --git a/ProxyConfigHelper/Helper-Info.plist b/ProxyConfigHelper/Helper-Info.plist index 7c3ec8662..ac9ef290a 100755 --- a/ProxyConfigHelper/Helper-Info.plist +++ b/ProxyConfigHelper/Helper-Info.plist @@ -9,9 +9,9 @@ CFBundleName com.west2online.ClashX.ProxyConfigHelper CFBundleShortVersionString - 1.8 + 2.0 CFBundleVersion - 3 + 5 SMAuthorizedClients anchor apple generic and identifier "com.west2online.ClashX" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = MEWHFZ92DY) diff --git a/ProxyConfigHelper/Helper-Launchd.plist b/ProxyConfigHelper/Helper-Launchd.plist index d02328484..d63b8e4f1 100755 --- a/ProxyConfigHelper/Helper-Launchd.plist +++ b/ProxyConfigHelper/Helper-Launchd.plist @@ -2,6 +2,8 @@ + AssociatedBundleIdentifiers + com.west2online.ClashX Label com.west2online.ClashX.ProxyConfigHelper MachServices diff --git a/ProxyConfigHelper/ProxyConfigHelper.m b/ProxyConfigHelper/ProxyConfigHelper.m index 3ad0e2513..773bd3d91 100644 --- a/ProxyConfigHelper/ProxyConfigHelper.m +++ b/ProxyConfigHelper/ProxyConfigHelper.m @@ -92,10 +92,11 @@ - (void)enableProxyWithPort:(int)port socksPort:(int)socksPort pac:(NSString *)pac filterInterface:(BOOL)filterInterface + ignoreList:(NSArray*)ignoreList error:(stringReplyBlock)reply { dispatch_async(dispatch_get_main_queue(), ^{ ProxySettingTool *tool = [ProxySettingTool new]; - [tool enableProxyWithport:port socksPort:socksPort pacUrl:pac filterInterface:filterInterface]; + [tool enableProxyWithport:port socksPort:socksPort pacUrl:pac filterInterface:filterInterface ignoreList:ignoreList]; reply(nil); }); } @@ -116,7 +117,7 @@ - (void)restoreProxyWithCurrentPort:(int)port error:(stringReplyBlock)reply { dispatch_async(dispatch_get_main_queue(), ^{ ProxySettingTool *tool = [ProxySettingTool new]; - [tool restoreProxySettint:dict currentPort:port currentSocksPort:socksPort filterInterface:filterInterface]; + [tool restoreProxySetting:dict currentPort:port currentSocksPort:socksPort filterInterface:filterInterface]; reply(nil); }); } diff --git a/ProxyConfigHelper/ProxyConfigRemoteProcessProtocol.h b/ProxyConfigHelper/ProxyConfigRemoteProcessProtocol.h index 0f16a0b30..51eb3f2b6 100644 --- a/ProxyConfigHelper/ProxyConfigRemoteProcessProtocol.h +++ b/ProxyConfigHelper/ProxyConfigRemoteProcessProtocol.h @@ -18,10 +18,11 @@ typedef void(^dictReplyBlock)(NSDictionary *); - (void)getVersion:(stringReplyBlock)reply; - (void)enableProxyWithPort:(int)port - socksPort:(int)socksPort - pac:(NSString *)pac + socksPort:(int)socksPort + pac:(NSString *)pac filterInterface:(BOOL)filterInterface - error:(stringReplyBlock)reply; + ignoreList:(NSArray*)ignoreList + error:(stringReplyBlock)reply; - (void)disableProxyWithFilterInterface:(BOOL)filterInterface reply:(stringReplyBlock)reply; diff --git a/ProxyConfigHelper/ProxySettingTool.h b/ProxyConfigHelper/ProxySettingTool.h index a11bb0441..2df8215d7 100644 --- a/ProxyConfigHelper/ProxySettingTool.h +++ b/ProxyConfigHelper/ProxySettingTool.h @@ -14,10 +14,12 @@ NS_ASSUME_NONNULL_BEGIN - (void)enableProxyWithport:(int)port socksPort:(int)socksPort pacUrl:(NSString *)pacUrl - filterInterface:(BOOL)filterInterFace; + filterInterface:(BOOL)filterInterface + ignoreList:(NSArray*)ignoreList; + - (void)disableProxyWithfilterInterface:(BOOL)filterInterFace; -- (void)restoreProxySettint:(NSDictionary *)savedInfo +- (void)restoreProxySetting:(NSDictionary *)savedInfo currentPort:(int)port currentSocksPort:(int)socksPort filterInterface:(BOOL)filterInterface; diff --git a/ProxyConfigHelper/ProxySettingTool.m b/ProxyConfigHelper/ProxySettingTool.m index 123bbea0f..7a5614442 100644 --- a/ProxyConfigHelper/ProxySettingTool.m +++ b/ProxyConfigHelper/ProxySettingTool.m @@ -29,11 +29,12 @@ - (instancetype)init { - (void)enableProxyWithport:(int)port socksPort:(int)socksPort pacUrl:(NSString *)pacUrl - filterInterface:(BOOL)filterInterface { + filterInterface:(BOOL)filterInterface + ignoreList:(NSArray*)ignoreList { [self applySCNetworkSettingWithRef:^(SCPreferencesRef ref) { [ProxySettingTool getDiviceListWithPrefRef:ref filterInterface:filterInterface devices:^(NSString *key, NSDictionary *dict) { - [self enableProxySettings:ref interface:key port:port socksPort:socksPort pac:pacUrl]; + [self enableProxySettings:ref interface:key port:port socksPort:socksPort ignoreList:ignoreList pac:pacUrl]; }]; }]; } @@ -46,7 +47,7 @@ - (void)disableProxyWithfilterInterface:(BOOL)filterInterface { }]; } -- (void)restoreProxySettint:(NSDictionary *)savedInfo +- (void)restoreProxySetting:(NSDictionary *)savedInfo currentPort:(int)port currentSocksPort:(int)socksPort filterInterface:(BOOL)filterInterface{ @@ -110,45 +111,9 @@ - (void)dealloc { } -+ (NSString *)getUserHomePath { - NSString *userName = [CommonUtils runCommand:@"/usr/bin/stat" args:@[@"-f",@"%Su",@"/dev/console"]]; - userName = [userName stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; - if (!userName) { - return nil; - } - NSString *path = [NSString stringWithFormat:@"/Users/%@", userName]; - if([NSFileManager.defaultManager fileExistsAtPath:path]) { - return path; - } - return nil; -} - - -- (NSArray *)getIgnoreList { - NSString *homePath = [ProxySettingTool getUserHomePath]; - if (homePath.length > 0) { - NSString *configPath = [homePath stringByAppendingString:@"/.config/clash/proxyIgnoreList.plist"]; - if ([NSFileManager.defaultManager fileExistsAtPath:configPath]) { - NSArray *arr = [[NSArray alloc] initWithContentsOfFile:configPath]; - if (arr != nil && arr.count > 0) { - return arr; - } - } - } - NSArray *ignoreList = @[ - @"192.168.0.0/16", - @"10.0.0.0/8", - @"172.16.0.0/12", - @"127.0.0.1", - @"localhost", - @"*.local", - @"timestamp.apple.com" - ]; - return ignoreList; -} - - (NSDictionary *)getProxySetting:(BOOL)enable port:(int) port - socksPort: (int)socksPort pac:(NSString *)pac { + socksPort: (int)socksPort pac:(NSString *)pac + ignoreList:(NSArray*)ignoreList { NSMutableDictionary *proxySettings = [NSMutableDictionary dictionary]; @@ -183,7 +148,7 @@ - (NSDictionary *)getProxySetting:(BOOL)enable port:(int) port } if (enable) { - proxySettings[(__bridge NSString *)kCFNetworkProxiesExceptionsList] = [self getIgnoreList]; + proxySettings[(__bridge NSString *)kCFNetworkProxiesExceptionsList] = ignoreList; } else { proxySettings[(__bridge NSString *)kCFNetworkProxiesExceptionsList] = @[]; } @@ -202,16 +167,17 @@ - (void)enableProxySettings:(SCPreferencesRef)prefs interface:(NSString *)interfaceKey port:(int) port socksPort:(int) socksPort + ignoreList:(NSArray*)ignoreList pac:(NSString *)pac { - NSDictionary *proxySettings = [self getProxySetting:YES port:port socksPort:socksPort pac:pac]; + NSDictionary *proxySettings = [self getProxySetting:YES port:port socksPort:socksPort pac:pac ignoreList:ignoreList]; [self setProxyConfig:prefs interface:interfaceKey proxySetting:proxySettings]; } - (void)disableProxySetting:(SCPreferencesRef)prefs interface:(NSString *)interfaceKey { - NSDictionary *proxySettings = [self getProxySetting:NO port:0 socksPort:0 pac:nil]; + NSDictionary *proxySettings = [self getProxySetting:NO port:0 socksPort:0 pac:nil ignoreList:@[]]; [self setProxyConfig:prefs interface:interfaceKey proxySetting:proxySettings]; } diff --git a/README.md b/README.md index 1390e89a5..96915e95a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ A rule based proxy For Mac base on [Clash](https://github.com/Dreamacro/clash). ClashX 旨在提供一个简单轻量化的代理客户端,如果需要更多的定制化,可以考虑使用 [CFW Mac 版](https://github.com/Fndroid/clash_for_windows_pkg/releases) + +## 注意 +- ClashX / ClashX Pro 只是一个代理工具,不提供任何代理服务器。如果服务器不可用或与服务器续费有关的问题,请与您的提供商联系。 +- ClashX / ClashX Pro 目前并没有创建官网。凡是声称是 ClashX / ClashX Pro 官网的一定是骗子。 + ## Features - HTTP/HTTPS and SOCKS protocol @@ -22,8 +27,9 @@ ClashX 旨在提供一个简单轻量化的代理客户端,如果需要更多 You can download from [Release](https://github.com/yichengchen/clashX/releases) page -**Download ClashX Pro With enhanced mode and Native Apple Silicon support at [AppCenter](https://install.appcenter.ms/users/clashx/apps/clashx-pro/distribution_groups/public) for free permanently.** +**Download ClashX Pro With enhanced mode and other clash premium feature at [AppCenter](https://install.appcenter.ms/users/clashx/apps/clashx-pro/distribution_groups/public) for free permanently.** +**在 [AppCenter](https://install.appcenter.ms/users/clashx/apps/clashx-pro/distribution_groups/public) 免费下载ClashX Pro版本,支持增强模式以及更多Clash Premium Core特性。** ## Build - Make sure have python3 and golang installed in your computer. @@ -54,9 +60,9 @@ Checkout [Clash](https://github.com/Dreamacro/clash) or [SS-Rule-Snippet for Cla ## Advance Config -### Change the ports of ClashX +### 修改代理端口号 +1. 在菜单栏->配置->更多设置中修改对应端口号 - Please modify the `config.yaml` file generated by ClashX, not the other config file you created or downloaded. The `General` section settings in your custom config file would be ignored. Then relaunch ClashX to apply changes. ### Change your status menu icon @@ -65,13 +71,7 @@ Checkout [Clash](https://github.com/Dreamacro/clash) or [SS-Rule-Snippet for Cla ### Change default system ignore list. -- Download sample plist in the [Here](proxyIgnoreList.plist) and place in the - - ``` - ~/.config/clash/proxyIgnoreList.plist - ``` - -- Edit the `proxyIgnoreList.plist` to set up your own proxy ignore list +- Change by menu -> Config -> Setting -> Bypass proxy settings for these Hosts & Domains ### URL Schemes. @@ -106,14 +106,10 @@ script: ### 关闭ClashX的通知 1. 在系统设置中关闭 clashx 的推送权限 -2. 执行 -``` -defaults write com.west2online.ClashX disableNoti -bool true -defaults write com.west2online.ClashXPro disableNoti -bool true -``` +2. 在菜单栏->配置->更多设置中选中减少通知 Note:强烈不推荐这么做,这可能导致clashx的很多重要错误提醒无法显示。 ### 全局快捷键 - -- 设置详情点击 [全局快捷键](Shortcuts.md) \ No newline at end of file +- 在菜单栏配置->更多配置中,自定义对应功能的快捷键。(需要1.116.1之后的版本) +- 使用AppleScript设置, 详情点击 [全局快捷键](Shortcuts.md) diff --git a/SMJobBlessUtil.py b/SMJobBlessUtil.py index fd54a506e..349750f7e 100644 --- a/SMJobBlessUtil.py +++ b/SMJobBlessUtil.py @@ -1,20 +1,20 @@ -#! /usr/bin/python -# +#! /usr/bin/python3 +# # File: SMJobBlessUtil.py -# +# # Contains: Tool for checking and correcting apps that use SMJobBless. -# +# # Written by: DTS -# +# # Copyright: Copyright (c) 2012 Apple Inc. All Rights Reserved. -# +# # Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. # ("Apple") in consideration of your agreement to the following # terms, and your use, installation, modification or # redistribution of this Apple software constitutes acceptance of # these terms. If you do not agree with these terms, please do # not use, install, modify or redistribute this Apple software. -# +# # In consideration of your agreement to abide by the following # terms, and subject to these terms, Apple grants you a personal, # non-exclusive license, under Apple's copyrights in this @@ -32,14 +32,14 @@ # are granted by Apple herein, including but not limited to any # patent rights that may be infringed by your derivative works or # by other works in which the Apple Software may be incorporated. -# -# The Apple Software is provided by Apple on an "AS IS" basis. +# +# The Apple Software is provided by Apple on an "AS IS" basis. # APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING # WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING # THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN # COMBINATION WITH YOUR PRODUCTS. -# +# # IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, # INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, @@ -49,7 +49,7 @@ # OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR # OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. -# +# import sys import os @@ -57,17 +57,18 @@ import subprocess import plistlib import operator +import platform class UsageException (Exception): """ - Raised when the progam detects a usage issue; the top-level code catches this + Raised when the progam detects a usage issue; the top-level code catches this and prints a usage message. """ pass class CheckException (Exception): """ - Raised when the "check" subcommand detects a problem; the top-level code catches + Raised when the "check" subcommand detects a problem; the top-level code catches this and prints a nice error message. """ def __init__(self, message, path=None): @@ -77,38 +78,38 @@ def __init__(self, message, path=None): def checkCodeSignature(programPath, programType): """Checks the code signature of the referenced program.""" - # Use the codesign tool to check the signature. The second "-v" is required to enable - # verbose mode, which causes codesign to do more checking. By default it does the minimum - # amount of checking ("Is the program properly signed?"). If you enabled verbose mode it - # does other sanity checks, which we definitely want. The specific thing I'd like to - # detect is "Does the code satisfy its own designated requirement?" and I need to enable + # Use the codesign tool to check the signature. The second "-v" is required to enable + # verbose mode, which causes codesign to do more checking. By default it does the minimum + # amount of checking ("Is the program properly signed?"). If you enabled verbose mode it + # does other sanity checks, which we definitely want. The specific thing I'd like to + # detect is "Does the code satisfy its own designated requirement?" and I need to enable # verbose mode to get that. args = [ - # "false", - "codesign", - "-v", + # "false", + "codesign", + "-v", "-v", programPath ] try: subprocess.check_call(args, stderr=open("/dev/null")) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: raise CheckException("%s code signature invalid" % programType, programPath) - + def readDesignatedRequirement(programPath, programType): """Returns the designated requirement of the program as a string.""" args = [ - # "false", - "codesign", - "-d", - "-r", - "-", + # "false", + "codesign", + "-d", + "-r", + "-", programPath ] try: - req = subprocess.check_output(args, stderr=open("/dev/null")) - except subprocess.CalledProcessError, e: + req = subprocess.check_output(args, stderr=open("/dev/null"), encoding="utf-8") + except subprocess.CalledProcessError as e: raise CheckException("%s designated requirement unreadable" % programType, programPath) reqLines = req.splitlines() @@ -119,77 +120,97 @@ def readDesignatedRequirement(programPath, programType): def readInfoPlistFromPath(infoPath): """Reads an "Info.plist" file from the specified path.""" try: - info = plistlib.readPlist(infoPath) + with open(infoPath, 'rb') as fp: + info = plistlib.load(fp) except: raise CheckException("'Info.plist' not readable", infoPath) if not isinstance(info, dict): raise CheckException("'Info.plist' root must be a dictionary", infoPath) return info - + def readPlistFromToolSection(toolPath, segmentName, sectionName): """Reads a dictionary property list from the specified section within the specified executable.""" - + # Run otool -s to get a hex dump of the section. - + args = [ - # "false", - "otool", - "-s", - segmentName, - sectionName, + # "false", + "otool", + "-V", + "-arch", + platform.machine(), + "-s", + segmentName, + sectionName, toolPath ] try: - plistDump = subprocess.check_output(args) - except subprocess.CalledProcessError, e: + plistDump = subprocess.check_output(args, encoding="utf-8") + except subprocess.CalledProcessError as e: raise CheckException("tool %s / %s section unreadable" % (segmentName, sectionName), toolPath) - # Convert that hex dump to an property list. - - plistLines = plistDump.splitlines() - if len(plistLines) < 3 or plistLines[1] != ("Contents of (%s,%s) section" % (segmentName, sectionName)): + # Convert that dump to an property list. + + plistLines = plistDump.strip().splitlines(keepends=True) + + if len(plistLines) < 3: raise CheckException("tool %s / %s section dump malformed (1)" % (segmentName, sectionName), toolPath) + + header = plistLines[1].strip() + + if not header.endswith("(%s,%s) section" % (segmentName, sectionName)): + raise CheckException("tool %s / %s section dump malformed (2)" % (segmentName, sectionName), toolPath) + del plistLines[0:2] try: - bytes = [] - for line in plistLines: - # line looks like this: - # - # '0000000100000b80\t3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d 22 31 ' - columns = line.split("\t") - assert len(columns) == 2 - for hexStr in columns[1].split(): - bytes.append(int(hexStr, 16)) - plist = plistlib.readPlistFromString(bytearray(bytes)) + + if header.startswith('Contents of'): + data = [] + for line in plistLines: + # line looks like this: + # + # '100000000 3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d 22 31 |= 2 + del columns[0] + for hexStr in columns: + data.append(int(hexStr, 16)) + data = bytes(data) + else: + data = bytes("".join(plistLines), encoding="utf-8") + + plist = plistlib.loads(data) except: - raise CheckException("tool %s / %s section dump malformed (2)" % (segmentName, sectionName), toolPath) + raise CheckException("tool %s / %s section dump malformed (3)" % (segmentName, sectionName), toolPath) # Check the root of the property list. - + if not isinstance(plist, dict): raise CheckException("tool %s / %s property list root must be a dictionary" % (segmentName, sectionName), toolPath) return plist - + def checkStep1(appPath): """Checks that the app and the tool are both correctly code signed.""" - + if not os.path.isdir(appPath): raise CheckException("app not found", appPath) - + # Check the app's code signature. - + checkCodeSignature(appPath, "app") - + # Check the tool directory. - + toolDirPath = os.path.join(appPath, "Contents", "Library", "LaunchServices") if not os.path.isdir(toolDirPath): raise CheckException("tool directory not found", toolDirPath) # Check each tool's code signature. - + toolPathList = [] for toolName in os.listdir(toolDirPath): if toolName != ".DS_Store": @@ -200,50 +221,50 @@ def checkStep1(appPath): toolPathList.append(toolPath) # Check that we have at least one tool. - + if len(toolPathList) == 0: raise CheckException("no tools found", toolDirPath) return toolPathList - + def checkStep2(appPath, toolPathList): """Checks the SMPrivilegedExecutables entry in the app's "Info.plist".""" # Create a map from the tool name (not path) to its designated requirement. - + toolNameToReqMap = dict() for toolPath in toolPathList: req = readDesignatedRequirement(toolPath, "tool") toolNameToReqMap[os.path.basename(toolPath)] = req - + # Read the Info.plist for the app and extract the SMPrivilegedExecutables value. - + infoPath = os.path.join(appPath, "Contents", "Info.plist") info = readInfoPlistFromPath(infoPath) - if not info.has_key("SMPrivilegedExecutables"): + if "SMPrivilegedExecutables" not in info: raise CheckException("'SMPrivilegedExecutables' not found", infoPath) infoToolDict = info["SMPrivilegedExecutables"] if not isinstance(infoToolDict, dict): raise CheckException("'SMPrivilegedExecutables' must be a dictionary", infoPath) - + # Check that the list of tools matches the list of SMPrivilegedExecutables entries. - + if sorted(infoToolDict.keys()) != sorted(toolNameToReqMap.keys()): raise CheckException("'SMPrivilegedExecutables' and tools in 'Contents/Library/LaunchServices' don't match") - + # Check that all the requirements match. - - # This is an interesting policy choice. Technically the tool just needs to match - # the requirement listed in SMPrivilegedExecutables, and we can check that by + + # This is an interesting policy choice. Technically the tool just needs to match + # the requirement listed in SMPrivilegedExecutables, and we can check that by # putting the requirement into tmp.req and then running # # $ codesign -v -R tmp.req /path/to/tool # - # However, for a Developer ID signed tool we really want to have the SMPrivilegedExecutables - # entry contain the tool's designated requirement because Xcode has built a - # more complex DR that does lots of useful and important checks. So, as a matter + # However, for a Developer ID signed tool we really want to have the SMPrivilegedExecutables + # entry contain the tool's designated requirement because Xcode has built a + # more complex DR that does lots of useful and important checks. So, as a matter # of policy we require that the value in SMPrivilegedExecutables match the tool's DR. - + for toolName in infoToolDict: if infoToolDict[toolName] != toolNameToReqMap[toolName]: raise CheckException("tool designated requirement (%s) doesn't match entry in 'SMPrivilegedExecutables' (%s)" % (toolNameToReqMap[toolName], infoToolDict[toolName])) @@ -252,29 +273,29 @@ def checkStep3(appPath, toolPathList): """Checks the "Info.plist" embedded in each helper tool.""" # First get the app's designated requirement. - + appReq = readDesignatedRequirement(appPath, "app") - # Then check that the tool's SMAuthorizedClients value matches it. - + # Then check that the tool's SMAuthorizedClients value matches it. + for toolPath in toolPathList: info = readPlistFromToolSection(toolPath, "__TEXT", "__info_plist") - if not info.has_key("CFBundleInfoDictionaryVersion") or info["CFBundleInfoDictionaryVersion"] != "6.0": + if "CFBundleInfoDictionaryVersion" not in info or info["CFBundleInfoDictionaryVersion"] != "6.0": raise CheckException("'CFBundleInfoDictionaryVersion' in tool __TEXT / __info_plist section must be '6.0'", toolPath) - if not info.has_key("CFBundleIdentifier") or info["CFBundleIdentifier"] != os.path.basename(toolPath): + if "CFBundleIdentifier" not in info or info["CFBundleIdentifier"] != os.path.basename(toolPath): raise CheckException("'CFBundleIdentifier' in tool __TEXT / __info_plist section must match tool name", toolPath) - if not info.has_key("SMAuthorizedClients"): + if "SMAuthorizedClients" not in info: raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section not found", toolPath) infoClientList = info["SMAuthorizedClients"] if not isinstance(infoClientList, list): raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section must be an array", toolPath) if len(infoClientList) != 1: raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section must have one entry", toolPath) - - # Again, as a matter of policy we require that the SMAuthorizedClients entry must + + # Again, as a matter of policy we require that the SMAuthorizedClients entry must # match exactly the designated requirement of the app. if infoClientList[0] != appReq: @@ -286,22 +307,22 @@ def checkStep4(appPath, toolPathList): for toolPath in toolPathList: launchd = readPlistFromToolSection(toolPath, "__TEXT", "__launchd_plist") - if not launchd.has_key("Label") or launchd["Label"] != os.path.basename(toolPath): + if "Label" not in launchd or launchd["Label"] != os.path.basename(toolPath): raise CheckException("'Label' in tool __TEXT / __launchd_plist section must match tool name", toolPath) - # We don't need to check that the label matches the bundle identifier because - # we know it matches the tool name and step 4 checks that the tool name matches + # We don't need to check that the label matches the bundle identifier because + # we know it matches the tool name and step 4 checks that the tool name matches # the bundle identifier. def checkStep5(appPath): """There's nothing to do here; we effectively checked for this is steps 1 and 2.""" pass - + def check(appPath): """Checks the SMJobBless setup of the specified app.""" # Each of the following steps matches a bullet point in the SMJobBless header doc. - + toolPathList = checkStep1(appPath) checkStep2(appPath, toolPathList) @@ -314,7 +335,7 @@ def check(appPath): def setreq(appPath, appInfoPlistPath, toolInfoPlistPaths): """ - Reads information from the built app and uses it to set the SMJobBless setup + Reads information from the built app and uses it to set the SMJobBless setup in the specified app and tool Info.plist source files. """ @@ -328,20 +349,16 @@ def setreq(appPath, appInfoPlistPath, toolInfoPlistPaths): raise CheckException("app 'Info.plist' not found", toolInfoPlistPath) # Get the designated requirement for the app and each of the tools. - + appReq = readDesignatedRequirement(appPath, "app") toolDirPath = os.path.join(appPath, "Contents", "Library", "LaunchServices") if not os.path.isdir(toolDirPath): raise CheckException("tool directory not found", toolDirPath) - + toolNameToReqMap = {} - print os.listdir(toolDirPath) for toolName in os.listdir(toolDirPath): req = readDesignatedRequirement(os.path.join(toolDirPath, toolName), "tool") - print '-----' - print toolName - print '-----' toolNameToReqMap[toolName] = req if len(toolNameToReqMap) > len(toolInfoPlistPaths): @@ -350,58 +367,60 @@ def setreq(appPath, appInfoPlistPath, toolInfoPlistPaths): raise CheckException("tool directory has fewer tools (%d) than you've supplied tool 'Info.plist' paths (%d)" % (len(toolNameToReqMap), len(toolInfoPlistPaths)), toolDirPath) # Build the new value for SMPrivilegedExecutables. - + appToolDict = {} toolInfoPlistPathToToolInfoMap = {} for toolInfoPlistPath in toolInfoPlistPaths: toolInfo = readInfoPlistFromPath(toolInfoPlistPath) toolInfoPlistPathToToolInfoMap[toolInfoPlistPath] = toolInfo - if not toolInfo.has_key("CFBundleIdentifier"): + if "CFBundleIdentifier" not in toolInfo: raise CheckException("'CFBundleIdentifier' not found", toolInfoPlistPath) bundleID = toolInfo["CFBundleIdentifier"] - if not isinstance(bundleID, basestring): + if not isinstance(bundleID, str): raise CheckException("'CFBundleIdentifier' must be a string", toolInfoPlistPath) appToolDict[bundleID] = toolNameToReqMap[bundleID] # Set the SMPrivilegedExecutables value in the app "Info.plist". appInfo = readInfoPlistFromPath(appInfoPlistPath) - needsUpdate = not appInfo.has_key("SMPrivilegedExecutables") + needsUpdate = "SMPrivilegedExecutables" not in appInfo if not needsUpdate: oldAppToolDict = appInfo["SMPrivilegedExecutables"] if not isinstance(oldAppToolDict, dict): raise CheckException("'SMPrivilegedExecutables' must be a dictionary", appInfoPlistPath) - appToolDictSorted = sorted(appToolDict.iteritems(), key=operator.itemgetter(0)) - oldAppToolDictSorted = sorted(oldAppToolDict.iteritems(), key=operator.itemgetter(0)) + appToolDictSorted = sorted(appToolDict.items(), key=operator.itemgetter(0)) + oldAppToolDictSorted = sorted(oldAppToolDict.items(), key=operator.itemgetter(0)) needsUpdate = (appToolDictSorted != oldAppToolDictSorted) - + if needsUpdate: appInfo["SMPrivilegedExecutables"] = appToolDict - plistlib.writePlist(appInfo, appInfoPlistPath) - print >> sys.stdout, "%s: updated" % appInfoPlistPath - + with open(appInfoPlistPath, 'wb') as fp: + plistlib.dump(appInfo, fp) + print ("%s: updated" % appInfoPlistPath, file = sys.stdout) + # Set the SMAuthorizedClients value in each tool's "Info.plist". toolAppListSorted = [ appReq ] # only one element, so obviously sorted (-: for toolInfoPlistPath in toolInfoPlistPaths: toolInfo = toolInfoPlistPathToToolInfoMap[toolInfoPlistPath] - - needsUpdate = not toolInfo.has_key("SMAuthorizedClients") + + needsUpdate = "SMAuthorizedClients" not in toolInfo if not needsUpdate: oldToolAppList = toolInfo["SMAuthorizedClients"] if not isinstance(oldToolAppList, list): raise CheckException("'SMAuthorizedClients' must be an array", toolInfoPlistPath) oldToolAppListSorted = sorted(oldToolAppList) needsUpdate = (toolAppListSorted != oldToolAppListSorted) - + if needsUpdate: toolInfo["SMAuthorizedClients"] = toolAppListSorted - plistlib.writePlist(toolInfo, toolInfoPlistPath) - print >> sys.stdout, "%s: updated" % toolInfoPlistPath + with open(toolInfoPlistPath, 'wb') as f: + plistlib.dump(toolInfo, f) + print("%s: updated" % toolInfoPlistPath, file = sys.stdout) def main(): options, appArgs = getopt.getopt(sys.argv[1:], "d") - + debug = False for opt, val in options: if opt == "-d": @@ -426,16 +445,16 @@ def main(): if __name__ == "__main__": try: main() - except CheckException, e: + except CheckException as e: if e.path is None: - print >> sys.stderr, "%s: %s" % (os.path.basename(sys.argv[0]), e.message) + print("%s: %s" % (os.path.basename(sys.argv[0]), e.message), file = sys.stderr) else: path = e.path if path.endswith("/"): path = path[:-1] - print >> sys.stderr, "%s: %s" % (path, e.message) - sys.exit(1) - except UsageException, e: - print >> sys.stderr, "usage: %s check /path/to/app" % os.path.basename(sys.argv[0]) - print >> sys.stderr, " %s setreq /path/to/app /path/to/app/Info.plist /path/to/tool/Info.plist..." % os.path.basename(sys.argv[0]) + print("%s: %s" % (path, e.message), file = sys.stderr) sys.exit(1) + except UsageException as e: + print("usage: %s check /path/to/app" % os.path.basename(sys.argv[0]), file = sys.stderr) + print(" %s setreq /path/to/app /path/to/app/Info.plist /path/to/tool/Info.plist..." % os.path.basename(sys.argv[0]), file = sys.stderr) + sys.exit(1) \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bf775b179..8d5c5544c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,31 +6,23 @@ build_app( workspace: "ClashX.xcworkspace", scheme: "ClashX", export_method: "developer-id", - skip_package_pkg: "true", - clean: "true" + skip_package_pkg: "false", + clean: "true", + derived_data_path: "./build_derived_data" ) - -sh(command: "rm -vfr ~/Library/Developer/Xcode/Archives/*") end -lane :beta do -current_version = get_version_number( - target: "ClashX" -) -timestamp = Time.now.utc -new_build_identifier = "%d%02d%02d%02d%02d" % [ - timestamp.year, - timestamp.month, - timestamp.day, - timestamp.hour, - timestamp.min, -] -new_version = current_version + "." + new_build_identifier -increment_build_number_in_plist( - build_number: new_version, - target: 'ClashX' -) -end +lane :check do + build_app( + workspace: "ClashX.xcworkspace", + scheme: "ClashX", + codesigning_identity: "-", + export_method: "developer-id", + skip_package_pkg: "true", + clean: "true", + derived_data_path: "./build_derived_data" + ) + end lane :addKeyChain do if is_ci? diff --git a/fastlane/README.md b/fastlane/README.md index 1c118220a..66d96fe9d 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -1,38 +1,54 @@ fastlane documentation -================ +---- + # Installation Make sure you have the latest version of the Xcode command line tools installed: -``` +```sh xcode-select --install ``` -Install _fastlane_ using -``` -[sudo] gem install fastlane -NV -``` -or alternatively using `brew install fastlane` +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions + ### build + +```sh +[bundle exec] fastlane build ``` -fastlane build + + + +### check + +```sh +[bundle exec] fastlane check ``` + + ### beta + +```sh +[bundle exec] fastlane beta ``` -fastlane beta -``` + + ### addKeyChain + +```sh +[bundle exec] fastlane addKeyChain ``` -fastlane addKeyChain -``` + ---- -This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. -More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). -The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/install_dependency.sh b/install_dependency.sh index a1d0fa9b2..e2c37f334 100755 --- a/install_dependency.sh +++ b/install_dependency.sh @@ -20,3 +20,5 @@ mv Country.mmdb.gz ./ClashX/Resources/Country.mmdb.gz echo "install dashboard" cd ClashX/Resources git clone -b gh-pages https://github.com/Dreamacro/clash-dashboard.git dashboard +cd dashboard +rm -rf *.webmanifest *.js CNAME .git diff --git a/proxyIgnoreList.plist b/proxyIgnoreList.plist deleted file mode 100644 index 9577c079f..000000000 --- a/proxyIgnoreList.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - 192.168.0.0/16 - 10.0.0.0/8 - 172.16.0.0/12 - 127.0.0.1 - localhost - *.local - *.crashlytics.com - my-custom-site.com - - diff --git a/ClashX/swiftFormate.sh b/updateLocalization.sh similarity index 100% rename from ClashX/swiftFormate.sh rename to updateLocalization.sh