diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2f53f94 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +# This workflow will build a Swift project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift + +name: Swift + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + # vars + APP_NAME: ${{ vars.APP_NAME }} + INFO_PLIST_PATH: ${{ vars.INFO_PLIST_PATH }} + PROJECT_PATH: ${{ vars.PROJECT_PATH }} + VERSION_FILE_PATH: ${{ vars.VERSION_FILE_PATH }} + XCODE_BUILD_DIR: ${{ vars.XCODE_BUILD_DIR }} + XCODE_BUILD_PATH: ${{ vars.XCODE_BUILD_PATH }} + + # secrets + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ALI_AK: ${{ secrets.ALI_AK }} + ALI_SK: ${{ secrets.ALI_SK }} + BAIDU_AK: ${{ secrets.BAIDU_AK }} + BAIDU_SK: ${{ secrets.BAIDU_SK }} + BIGHUGETHESAURUS_SK: ${{ secrets.BIGHUGETHESAURUS_SK }} + NIUTRANS_SK: ${{ secrets.NIUTRANS_SK }} + VOLCENGINE_AK: ${{ secrets.VOLCENGINE_AK }} + VOLCENGINE_SK: ${{ secrets.VOLCENGINE_SK }} + SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }} + +jobs: + build: + runs-on: macos-14-large + if: ${{ !contains(github.event.head_commit.message, 'chore') }} + steps: + - uses: actions/checkout@v3 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: 15.2 + - name: Release + run: scripts/release.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c86e184 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store + +Packages +build +DerivedData +xcuserdata + +version.txt + +# *.xcodeproj \ No newline at end of file diff --git a/liltr.xcodeproj/project.pbxproj b/liltr.xcodeproj/project.pbxproj new file mode 100644 index 0000000..eadd641 --- /dev/null +++ b/liltr.xcodeproj/project.pbxproj @@ -0,0 +1,701 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2B59A3902B75B92F005DB8B1 /* liltrApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B59A38F2B75B92F005DB8B1 /* liltrApp.swift */; }; + 2B59A3972B75B930005DB8B1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B59A3962B75B930005DB8B1 /* Preview Assets.xcassets */; }; + 2B59A3A02B75B9C7005DB8B1 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 2B59A39F2B75B9C7005DB8B1 /* KeyboardShortcuts */; }; + 2B59A3A22B75BA07005DB8B1 /* userDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B59A3A12B75BA07005DB8B1 /* userDefaults.swift */; }; + 2B59A3AF2B75BA11005DB8B1 /* appDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B59A3A72B75BA11005DB8B1 /* appDelegate.swift */; }; + 2B59A3B22B75BA11005DB8B1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B59A3AA2B75BA11005DB8B1 /* Assets.xcassets */; }; + 2B7CF3152B75BCB400BFA0BA /* KeyboardShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2F12B75BCB400BFA0BA /* KeyboardShortcut.swift */; }; + 2B7CF3162B75BCB400BFA0BA /* Encoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2F32B75BCB400BFA0BA /* Encoder.swift */; }; + 2B7CF3172B75BCB400BFA0BA /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2F52B75BCB400BFA0BA /* NotificationManager.swift */; }; + 2B7CF3182B75BCB400BFA0BA /* OCRManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2F72B75BCB400BFA0BA /* OCRManager.swift */; }; + 2B7CF31A2B75BCB400BFA0BA /* BigHugeThesaurus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2FA2B75BCB400BFA0BA /* BigHugeThesaurus.swift */; }; + 2B7CF31B2B75BCB400BFA0BA /* Baidu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2FB2B75BCB400BFA0BA /* Baidu.swift */; }; + 2B7CF31C2B75BCB400BFA0BA /* NiuTrans.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2FC2B75BCB400BFA0BA /* NiuTrans.swift */; }; + 2B7CF31D2B75BCB400BFA0BA /* Ali.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2FD2B75BCB400BFA0BA /* Ali.swift */; }; + 2B7CF31E2B75BCB400BFA0BA /* AppleDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2FE2B75BCB400BFA0BA /* AppleDictionary.swift */; }; + 2B7CF31F2B75BCB400BFA0BA /* ProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF2FF2B75BCB400BFA0BA /* ProviderManager.swift */; }; + 2B7CF3202B75BCB400BFA0BA /* Volcengine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3002B75BCB400BFA0BA /* Volcengine.swift */; }; + 2B7CF3212B75BCB400BFA0BA /* Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3022B75BCB400BFA0BA /* Language.swift */; }; + 2B7CF3222B75BCB400BFA0BA /* LanguageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3032B75BCB400BFA0BA /* LanguageManager.swift */; }; + 2B7CF3232B75BCB400BFA0BA /* SpeechManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3042B75BCB400BFA0BA /* SpeechManager.swift */; }; + 2B7CF3242B75BCB400BFA0BA /* extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3052B75BCB400BFA0BA /* extensions.swift */; }; + 2B7CF3252B75BCB400BFA0BA /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3072B75BCB400BFA0BA /* Common.swift */; }; + 2B7CF3262B75BCB400BFA0BA /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3082B75BCB400BFA0BA /* Debouncer.swift */; }; + 2B7CF3272B75BCB400BFA0BA /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF30A2B75BCB400BFA0BA /* WindowManager.swift */; }; + 2B7CF3282B75BCB400BFA0BA /* views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF30B2B75BCB400BFA0BA /* views.swift */; }; + 2B7CF3292B75BCB400BFA0BA /* TTTDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF30D2B75BCB400BFA0BA /* TTTDictionary.cpp */; }; + 2B7CF32B2B75BCB400BFA0BA /* TextHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3122B75BCB400BFA0BA /* TextHandler.swift */; }; + 2B7CF32C2B75BCB400BFA0BA /* SchemeURLManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3142B75BCB400BFA0BA /* SchemeURLManager.swift */; }; + 2B7CF36D2B75BD0F00BFA0BA /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 2B7CF36C2B75BD0F00BFA0BA /* Alamofire */; }; + 2B7CF3702B75BF2300BFA0BA /* TTTDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF36F2B75BF2300BFA0BA /* TTTDictionary.m */; }; + 2B7CF3772B75BFE400BFA0BA /* ToolbarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3722B75BFE400BFA0BA /* ToolbarItem.swift */; }; + 2B7CF3782B75BFE400BFA0BA /* TabItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3732B75BFE400BFA0BA /* TabItem.swift */; }; + 2B7CF3792B75BFE400BFA0BA /* TopTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3742B75BFE400BFA0BA /* TopTabView.swift */; }; + 2B7CF37A2B75BFE400BFA0BA /* ProviderPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3752B75BFE400BFA0BA /* ProviderPicker.swift */; }; + 2B7CF37B2B75BFE400BFA0BA /* LanguagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3762B75BFE400BFA0BA /* LanguagePicker.swift */; }; + 2B7CF3822B75BFF000BFA0BA /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF37D2B75BFF000BFA0BA /* AboutView.swift */; }; + 2B7CF3832B75BFF000BFA0BA /* Hotkeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF37E2B75BFF000BFA0BA /* Hotkeys.swift */; }; + 2B7CF3842B75BFF000BFA0BA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF37F2B75BFF000BFA0BA /* SettingsView.swift */; }; + 2B7CF3852B75BFF000BFA0BA /* GeneralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3802B75BFF000BFA0BA /* GeneralView.swift */; }; + 2B7CF3862B75BFF000BFA0BA /* ProvidersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3812B75BFF000BFA0BA /* ProvidersView.swift */; }; + 2B7CF38D2B75C00400BFA0BA /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3882B75C00400BFA0BA /* TranslateView.swift */; }; + 2B7CF38E2B75C00400BFA0BA /* TranslateFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF3892B75C00400BFA0BA /* TranslateFieldView.swift */; }; + 2B7CF38F2B75C00400BFA0BA /* TopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF38A2B75C00400BFA0BA /* TopBarView.swift */; }; + 2B7CF3902B75C00400BFA0BA /* MidBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF38B2B75C00400BFA0BA /* MidBarView.swift */; }; + 2B7CF3912B75C00400BFA0BA /* BottomBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7CF38C2B75C00400BFA0BA /* BottomBarView.swift */; }; + 2B7CF3942B75FCDA00BFA0BA /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 2B7CF3932B75FCDA00BFA0BA /* Sparkle */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2B59A38C2B75B92F005DB8B1 /* liltr.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = liltr.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2B59A38F2B75B92F005DB8B1 /* liltrApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = liltrApp.swift; sourceTree = ""; }; + 2B59A3962B75B930005DB8B1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2B59A3982B75B930005DB8B1 /* liltr.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = liltr.entitlements; sourceTree = ""; }; + 2B59A3A12B75BA07005DB8B1 /* userDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = userDefaults.swift; sourceTree = ""; }; + 2B59A3A72B75BA11005DB8B1 /* appDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = appDelegate.swift; sourceTree = ""; }; + 2B59A3AA2B75BA11005DB8B1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2B59A3B32B75BA49005DB8B1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2B7CF2F12B75BCB400BFA0BA /* KeyboardShortcut.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardShortcut.swift; sourceTree = ""; }; + 2B7CF2F32B75BCB400BFA0BA /* Encoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Encoder.swift; sourceTree = ""; }; + 2B7CF2F52B75BCB400BFA0BA /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; + 2B7CF2F72B75BCB400BFA0BA /* OCRManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCRManager.swift; sourceTree = ""; }; + 2B7CF2FA2B75BCB400BFA0BA /* BigHugeThesaurus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BigHugeThesaurus.swift; sourceTree = ""; }; + 2B7CF2FB2B75BCB400BFA0BA /* Baidu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Baidu.swift; sourceTree = ""; }; + 2B7CF2FC2B75BCB400BFA0BA /* NiuTrans.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NiuTrans.swift; sourceTree = ""; }; + 2B7CF2FD2B75BCB400BFA0BA /* Ali.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ali.swift; sourceTree = ""; }; + 2B7CF2FE2B75BCB400BFA0BA /* AppleDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleDictionary.swift; sourceTree = ""; }; + 2B7CF2FF2B75BCB400BFA0BA /* ProviderManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderManager.swift; sourceTree = ""; }; + 2B7CF3002B75BCB400BFA0BA /* Volcengine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Volcengine.swift; sourceTree = ""; }; + 2B7CF3022B75BCB400BFA0BA /* Language.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Language.swift; sourceTree = ""; }; + 2B7CF3032B75BCB400BFA0BA /* LanguageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageManager.swift; sourceTree = ""; }; + 2B7CF3042B75BCB400BFA0BA /* SpeechManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeechManager.swift; sourceTree = ""; }; + 2B7CF3052B75BCB400BFA0BA /* extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = extensions.swift; sourceTree = ""; }; + 2B7CF3072B75BCB400BFA0BA /* Common.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; + 2B7CF3082B75BCB400BFA0BA /* Debouncer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; }; + 2B7CF30A2B75BCB400BFA0BA /* WindowManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; + 2B7CF30B2B75BCB400BFA0BA /* views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = views.swift; sourceTree = ""; }; + 2B7CF30D2B75BCB400BFA0BA /* TTTDictionary.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TTTDictionary.cpp; sourceTree = ""; }; + 2B7CF30E2B75BCB400BFA0BA /* TTTDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TTTDictionary.h; sourceTree = ""; }; + 2B7CF3122B75BCB400BFA0BA /* TextHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextHandler.swift; sourceTree = ""; }; + 2B7CF3142B75BCB400BFA0BA /* SchemeURLManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemeURLManager.swift; sourceTree = ""; }; + 2B7CF36E2B75BEFC00BFA0BA /* liltr-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "liltr-Bridging-Header.h"; sourceTree = ""; }; + 2B7CF36F2B75BF2300BFA0BA /* TTTDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TTTDictionary.m; sourceTree = ""; }; + 2B7CF3722B75BFE400BFA0BA /* ToolbarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarItem.swift; sourceTree = ""; }; + 2B7CF3732B75BFE400BFA0BA /* TabItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabItem.swift; sourceTree = ""; }; + 2B7CF3742B75BFE400BFA0BA /* TopTabView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTabView.swift; sourceTree = ""; }; + 2B7CF3752B75BFE400BFA0BA /* ProviderPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderPicker.swift; sourceTree = ""; }; + 2B7CF3762B75BFE400BFA0BA /* LanguagePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguagePicker.swift; sourceTree = ""; }; + 2B7CF37D2B75BFF000BFA0BA /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; + 2B7CF37E2B75BFF000BFA0BA /* Hotkeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hotkeys.swift; sourceTree = ""; }; + 2B7CF37F2B75BFF000BFA0BA /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 2B7CF3802B75BFF000BFA0BA /* GeneralView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralView.swift; sourceTree = ""; }; + 2B7CF3812B75BFF000BFA0BA /* ProvidersView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProvidersView.swift; sourceTree = ""; }; + 2B7CF3882B75C00400BFA0BA /* TranslateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = ""; }; + 2B7CF3892B75C00400BFA0BA /* TranslateFieldView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslateFieldView.swift; sourceTree = ""; }; + 2B7CF38A2B75C00400BFA0BA /* TopBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopBarView.swift; sourceTree = ""; }; + 2B7CF38B2B75C00400BFA0BA /* MidBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MidBarView.swift; sourceTree = ""; }; + 2B7CF38C2B75C00400BFA0BA /* BottomBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomBarView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2B59A3892B75B92F005DB8B1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B7CF3942B75FCDA00BFA0BA /* Sparkle in Frameworks */, + 2B59A3A02B75B9C7005DB8B1 /* KeyboardShortcuts in Frameworks */, + 2B7CF36D2B75BD0F00BFA0BA /* Alamofire in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2B59A3832B75B92F005DB8B1 = { + isa = PBXGroup; + children = ( + 2B59A38E2B75B92F005DB8B1 /* liltr */, + 2B59A38D2B75B92F005DB8B1 /* Products */, + ); + sourceTree = ""; + }; + 2B59A38D2B75B92F005DB8B1 /* Products */ = { + isa = PBXGroup; + children = ( + 2B59A38C2B75B92F005DB8B1 /* liltr.app */, + ); + name = Products; + sourceTree = ""; + }; + 2B59A38E2B75B92F005DB8B1 /* liltr */ = { + isa = PBXGroup; + children = ( + 2B59A3B32B75BA49005DB8B1 /* Info.plist */, + 2B59A3982B75B930005DB8B1 /* liltr.entitlements */, + 2B59A38F2B75B92F005DB8B1 /* liltrApp.swift */, + 2B59A3A72B75BA11005DB8B1 /* appDelegate.swift */, + 2B59A3A12B75BA07005DB8B1 /* userDefaults.swift */, + 2B59A3AA2B75BA11005DB8B1 /* Assets.xcassets */, + 2B7CF3872B75C00400BFA0BA /* Translate */, + 2B7CF37C2B75BFF000BFA0BA /* Settings */, + 2B7CF3712B75BFE400BFA0BA /* Components */, + 2B7CF2EF2B75BCB400BFA0BA /* Utils */, + 2B59A3952B75B930005DB8B1 /* Preview Content */, + ); + path = liltr; + sourceTree = ""; + }; + 2B59A3952B75B930005DB8B1 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 2B59A3962B75B930005DB8B1 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 2B7CF2EF2B75BCB400BFA0BA /* Utils */ = { + isa = PBXGroup; + children = ( + 2B7CF3052B75BCB400BFA0BA /* extensions.swift */, + 2B7CF30B2B75BCB400BFA0BA /* views.swift */, + 2B7CF2F02B75BCB400BFA0BA /* KeyboardShortcut */, + 2B7CF2F22B75BCB400BFA0BA /* CryptoEncoder */, + 2B7CF2F42B75BCB400BFA0BA /* Notification */, + 2B7CF2F62B75BCB400BFA0BA /* OCR */, + 2B7CF2F82B75BCB400BFA0BA /* Provider */, + 2B7CF3012B75BCB400BFA0BA /* Language */, + 2B7CF3062B75BCB400BFA0BA /* Common */, + 2B7CF3092B75BCB400BFA0BA /* Window */, + 2B7CF30C2B75BCB400BFA0BA /* TTTDictionary */, + 2B7CF3112B75BCB400BFA0BA /* TextHandler */, + 2B7CF3132B75BCB400BFA0BA /* SchemeURL */, + ); + path = Utils; + sourceTree = ""; + }; + 2B7CF2F02B75BCB400BFA0BA /* KeyboardShortcut */ = { + isa = PBXGroup; + children = ( + 2B7CF2F12B75BCB400BFA0BA /* KeyboardShortcut.swift */, + ); + path = KeyboardShortcut; + sourceTree = ""; + }; + 2B7CF2F22B75BCB400BFA0BA /* CryptoEncoder */ = { + isa = PBXGroup; + children = ( + 2B7CF2F32B75BCB400BFA0BA /* Encoder.swift */, + ); + path = CryptoEncoder; + sourceTree = ""; + }; + 2B7CF2F42B75BCB400BFA0BA /* Notification */ = { + isa = PBXGroup; + children = ( + 2B7CF2F52B75BCB400BFA0BA /* NotificationManager.swift */, + ); + path = Notification; + sourceTree = ""; + }; + 2B7CF2F62B75BCB400BFA0BA /* OCR */ = { + isa = PBXGroup; + children = ( + 2B7CF2F72B75BCB400BFA0BA /* OCRManager.swift */, + ); + path = OCR; + sourceTree = ""; + }; + 2B7CF2F82B75BCB400BFA0BA /* Provider */ = { + isa = PBXGroup; + children = ( + 2B7CF2FA2B75BCB400BFA0BA /* BigHugeThesaurus.swift */, + 2B7CF2FB2B75BCB400BFA0BA /* Baidu.swift */, + 2B7CF2FC2B75BCB400BFA0BA /* NiuTrans.swift */, + 2B7CF2FD2B75BCB400BFA0BA /* Ali.swift */, + 2B7CF2FE2B75BCB400BFA0BA /* AppleDictionary.swift */, + 2B7CF2FF2B75BCB400BFA0BA /* ProviderManager.swift */, + 2B7CF3002B75BCB400BFA0BA /* Volcengine.swift */, + ); + path = Provider; + sourceTree = ""; + }; + 2B7CF3012B75BCB400BFA0BA /* Language */ = { + isa = PBXGroup; + children = ( + 2B7CF3022B75BCB400BFA0BA /* Language.swift */, + 2B7CF3032B75BCB400BFA0BA /* LanguageManager.swift */, + 2B7CF3042B75BCB400BFA0BA /* SpeechManager.swift */, + ); + path = Language; + sourceTree = ""; + }; + 2B7CF3062B75BCB400BFA0BA /* Common */ = { + isa = PBXGroup; + children = ( + 2B7CF3072B75BCB400BFA0BA /* Common.swift */, + 2B7CF3082B75BCB400BFA0BA /* Debouncer.swift */, + ); + path = Common; + sourceTree = ""; + }; + 2B7CF3092B75BCB400BFA0BA /* Window */ = { + isa = PBXGroup; + children = ( + 2B7CF30A2B75BCB400BFA0BA /* WindowManager.swift */, + ); + path = Window; + sourceTree = ""; + }; + 2B7CF30C2B75BCB400BFA0BA /* TTTDictionary */ = { + isa = PBXGroup; + children = ( + 2B7CF30D2B75BCB400BFA0BA /* TTTDictionary.cpp */, + 2B7CF30E2B75BCB400BFA0BA /* TTTDictionary.h */, + 2B7CF36E2B75BEFC00BFA0BA /* liltr-Bridging-Header.h */, + 2B7CF36F2B75BF2300BFA0BA /* TTTDictionary.m */, + ); + path = TTTDictionary; + sourceTree = ""; + }; + 2B7CF3112B75BCB400BFA0BA /* TextHandler */ = { + isa = PBXGroup; + children = ( + 2B7CF3122B75BCB400BFA0BA /* TextHandler.swift */, + ); + path = TextHandler; + sourceTree = ""; + }; + 2B7CF3132B75BCB400BFA0BA /* SchemeURL */ = { + isa = PBXGroup; + children = ( + 2B7CF3142B75BCB400BFA0BA /* SchemeURLManager.swift */, + ); + path = SchemeURL; + sourceTree = ""; + }; + 2B7CF3712B75BFE400BFA0BA /* Components */ = { + isa = PBXGroup; + children = ( + 2B7CF3722B75BFE400BFA0BA /* ToolbarItem.swift */, + 2B7CF3732B75BFE400BFA0BA /* TabItem.swift */, + 2B7CF3742B75BFE400BFA0BA /* TopTabView.swift */, + 2B7CF3752B75BFE400BFA0BA /* ProviderPicker.swift */, + 2B7CF3762B75BFE400BFA0BA /* LanguagePicker.swift */, + ); + path = Components; + sourceTree = ""; + }; + 2B7CF37C2B75BFF000BFA0BA /* Settings */ = { + isa = PBXGroup; + children = ( + 2B7CF37F2B75BFF000BFA0BA /* SettingsView.swift */, + 2B7CF3802B75BFF000BFA0BA /* GeneralView.swift */, + 2B7CF3812B75BFF000BFA0BA /* ProvidersView.swift */, + 2B7CF37D2B75BFF000BFA0BA /* AboutView.swift */, + 2B7CF37E2B75BFF000BFA0BA /* Hotkeys.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 2B7CF3872B75C00400BFA0BA /* Translate */ = { + isa = PBXGroup; + children = ( + 2B7CF3882B75C00400BFA0BA /* TranslateView.swift */, + 2B7CF3892B75C00400BFA0BA /* TranslateFieldView.swift */, + 2B7CF38A2B75C00400BFA0BA /* TopBarView.swift */, + 2B7CF38B2B75C00400BFA0BA /* MidBarView.swift */, + 2B7CF38C2B75C00400BFA0BA /* BottomBarView.swift */, + ); + path = Translate; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2B59A38B2B75B92F005DB8B1 /* liltr */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2B59A39B2B75B930005DB8B1 /* Build configuration list for PBXNativeTarget "liltr" */; + buildPhases = ( + 2B59A3882B75B92F005DB8B1 /* Sources */, + 2B59A3892B75B92F005DB8B1 /* Frameworks */, + 2B59A38A2B75B92F005DB8B1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = liltr; + packageProductDependencies = ( + 2B59A39F2B75B9C7005DB8B1 /* KeyboardShortcuts */, + 2B7CF36C2B75BD0F00BFA0BA /* Alamofire */, + 2B7CF3932B75FCDA00BFA0BA /* Sparkle */, + ); + productName = liltr; + productReference = 2B59A38C2B75B92F005DB8B1 /* liltr.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2B59A3842B75B92F005DB8B1 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1520; + LastUpgradeCheck = 1520; + TargetAttributes = { + 2B59A38B2B75B92F005DB8B1 = { + CreatedOnToolsVersion = 15.2; + }; + }; + }; + buildConfigurationList = 2B59A3872B75B92F005DB8B1 /* Build configuration list for PBXProject "liltr" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2B59A3832B75B92F005DB8B1; + packageReferences = ( + 2B59A39E2B75B9C7005DB8B1 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, + 2B7CF36B2B75BD0F00BFA0BA /* XCRemoteSwiftPackageReference "Alamofire" */, + 2B7CF3922B75FCDA00BFA0BA /* XCRemoteSwiftPackageReference "Sparkle" */, + ); + productRefGroup = 2B59A38D2B75B92F005DB8B1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2B59A38B2B75B92F005DB8B1 /* liltr */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2B59A38A2B75B92F005DB8B1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B59A3972B75B930005DB8B1 /* Preview Assets.xcassets in Resources */, + 2B59A3B22B75BA11005DB8B1 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2B59A3882B75B92F005DB8B1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B7CF3842B75BFF000BFA0BA /* SettingsView.swift in Sources */, + 2B7CF31E2B75BCB400BFA0BA /* AppleDictionary.swift in Sources */, + 2B7CF3172B75BCB400BFA0BA /* NotificationManager.swift in Sources */, + 2B7CF3862B75BFF000BFA0BA /* ProvidersView.swift in Sources */, + 2B7CF3262B75BCB400BFA0BA /* Debouncer.swift in Sources */, + 2B7CF32B2B75BCB400BFA0BA /* TextHandler.swift in Sources */, + 2B59A3A22B75BA07005DB8B1 /* userDefaults.swift in Sources */, + 2B7CF38E2B75C00400BFA0BA /* TranslateFieldView.swift in Sources */, + 2B7CF31C2B75BCB400BFA0BA /* NiuTrans.swift in Sources */, + 2B7CF31F2B75BCB400BFA0BA /* ProviderManager.swift in Sources */, + 2B7CF31A2B75BCB400BFA0BA /* BigHugeThesaurus.swift in Sources */, + 2B7CF3242B75BCB400BFA0BA /* extensions.swift in Sources */, + 2B7CF3282B75BCB400BFA0BA /* views.swift in Sources */, + 2B7CF3772B75BFE400BFA0BA /* ToolbarItem.swift in Sources */, + 2B7CF31B2B75BCB400BFA0BA /* Baidu.swift in Sources */, + 2B7CF3252B75BCB400BFA0BA /* Common.swift in Sources */, + 2B7CF3822B75BFF000BFA0BA /* AboutView.swift in Sources */, + 2B59A3AF2B75BA11005DB8B1 /* appDelegate.swift in Sources */, + 2B7CF3292B75BCB400BFA0BA /* TTTDictionary.cpp in Sources */, + 2B7CF31D2B75BCB400BFA0BA /* Ali.swift in Sources */, + 2B7CF3902B75C00400BFA0BA /* MidBarView.swift in Sources */, + 2B7CF3202B75BCB400BFA0BA /* Volcengine.swift in Sources */, + 2B7CF37B2B75BFE400BFA0BA /* LanguagePicker.swift in Sources */, + 2B7CF3832B75BFF000BFA0BA /* Hotkeys.swift in Sources */, + 2B7CF3702B75BF2300BFA0BA /* TTTDictionary.m in Sources */, + 2B7CF3162B75BCB400BFA0BA /* Encoder.swift in Sources */, + 2B7CF3792B75BFE400BFA0BA /* TopTabView.swift in Sources */, + 2B7CF3782B75BFE400BFA0BA /* TabItem.swift in Sources */, + 2B7CF3152B75BCB400BFA0BA /* KeyboardShortcut.swift in Sources */, + 2B7CF37A2B75BFE400BFA0BA /* ProviderPicker.swift in Sources */, + 2B7CF38F2B75C00400BFA0BA /* TopBarView.swift in Sources */, + 2B7CF3232B75BCB400BFA0BA /* SpeechManager.swift in Sources */, + 2B7CF3912B75C00400BFA0BA /* BottomBarView.swift in Sources */, + 2B7CF3272B75BCB400BFA0BA /* WindowManager.swift in Sources */, + 2B7CF38D2B75C00400BFA0BA /* TranslateView.swift in Sources */, + 2B7CF3212B75BCB400BFA0BA /* Language.swift in Sources */, + 2B7CF32C2B75BCB400BFA0BA /* SchemeURLManager.swift in Sources */, + 2B59A3902B75B92F005DB8B1 /* liltrApp.swift in Sources */, + 2B7CF3852B75BFF000BFA0BA /* GeneralView.swift in Sources */, + 2B7CF3222B75BCB400BFA0BA /* LanguageManager.swift in Sources */, + 2B7CF3182B75BCB400BFA0BA /* OCRManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 2B59A3992B75B930005DB8B1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 2B59A39A2B75B930005DB8B1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 2B59A39C2B75B930005DB8B1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = liltr/liltr.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = H6N2N99XF5; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = liltr/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.rhinoc.liltr; + PRODUCT_NAME = liltr; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "liltr/Utils/TTTDictionary/liltr-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 2B59A39D2B75B930005DB8B1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = liltr/liltr.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = H6N2N99XF5; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = liltr/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.rhinoc.liltr; + PRODUCT_NAME = liltr; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "liltr/Utils/TTTDictionary/liltr-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2B59A3872B75B92F005DB8B1 /* Build configuration list for PBXProject "liltr" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2B59A3992B75B930005DB8B1 /* Debug */, + 2B59A39A2B75B930005DB8B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2B59A39B2B75B930005DB8B1 /* Build configuration list for PBXNativeTarget "liltr" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2B59A39C2B75B930005DB8B1 /* Debug */, + 2B59A39D2B75B930005DB8B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2B59A39E2B75B9C7005DB8B1 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.17.0; + }; + }; + 2B7CF36B2B75BD0F00BFA0BA /* XCRemoteSwiftPackageReference "Alamofire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/Alamofire.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.8.1; + }; + }; + 2B7CF3922B75FCDA00BFA0BA /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.5.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2B59A39F2B75B9C7005DB8B1 /* KeyboardShortcuts */ = { + isa = XCSwiftPackageProductDependency; + package = 2B59A39E2B75B9C7005DB8B1 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; + productName = KeyboardShortcuts; + }; + 2B7CF36C2B75BD0F00BFA0BA /* Alamofire */ = { + isa = XCSwiftPackageProductDependency; + package = 2B7CF36B2B75BD0F00BFA0BA /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = Alamofire; + }; + 2B7CF3932B75FCDA00BFA0BA /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = 2B7CF3922B75FCDA00BFA0BA /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2B59A3842B75B92F005DB8B1 /* Project object */; +} diff --git a/liltr.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/liltr.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/liltr.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/liltr.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/liltr.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/liltr.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/liltr.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/liltr.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..e1d692c --- /dev/null +++ b/liltr.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,32 @@ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", + "version" : "5.8.1" + } + }, + { + "identity" : "keyboardshortcuts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/KeyboardShortcuts", + "state" : { + "revision" : "ac12762853126cf2e7ad63a6a58e1c9f58c6a0ee", + "version" : "1.17.0" + } + }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "47d3d90aee3c52b6f61d04ceae426e607df62347", + "version" : "2.5.2" + } + } + ], + "version" : 2 +} diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/128_1x.png b/liltr/Assets.xcassets/AppIcon.appiconset/128_1x.png new file mode 100644 index 0000000..b1c9b32 Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/128_1x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/128_2x.png b/liltr/Assets.xcassets/AppIcon.appiconset/128_2x.png new file mode 100644 index 0000000..23f2cc3 Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/128_2x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/16_1x.png b/liltr/Assets.xcassets/AppIcon.appiconset/16_1x.png new file mode 100644 index 0000000..c9e3242 Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/16_1x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/16_2x.png b/liltr/Assets.xcassets/AppIcon.appiconset/16_2x.png new file mode 100644 index 0000000..fd3ba0d Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/16_2x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/256_1x.png b/liltr/Assets.xcassets/AppIcon.appiconset/256_1x.png new file mode 100644 index 0000000..23f2cc3 Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/256_1x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/256_2x.png b/liltr/Assets.xcassets/AppIcon.appiconset/256_2x.png new file mode 100644 index 0000000..1b02383 Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/256_2x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/32_1x.png b/liltr/Assets.xcassets/AppIcon.appiconset/32_1x.png new file mode 100644 index 0000000..fd3ba0d Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/32_1x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/32_2x.png b/liltr/Assets.xcassets/AppIcon.appiconset/32_2x.png new file mode 100644 index 0000000..e2199bb Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/32_2x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/512_1x.png b/liltr/Assets.xcassets/AppIcon.appiconset/512_1x.png new file mode 100644 index 0000000..1b02383 Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/512_1x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/512_2x.png b/liltr/Assets.xcassets/AppIcon.appiconset/512_2x.png new file mode 100644 index 0000000..366b6b6 Binary files /dev/null and b/liltr/Assets.xcassets/AppIcon.appiconset/512_2x.png differ diff --git a/liltr/Assets.xcassets/AppIcon.appiconset/Contents.json b/liltr/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..22670ed --- /dev/null +++ b/liltr/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "16_1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "16_2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32_1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "32_2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128_1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "128_2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256_1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "256_2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512_1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "512_2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/liltr/Assets.xcassets/Contents.json b/liltr/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/liltr/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/liltr/Assets.xcassets/monochrome.fill.imageset/1x.svg b/liltr/Assets.xcassets/monochrome.fill.imageset/1x.svg new file mode 100644 index 0000000..b7e9122 --- /dev/null +++ b/liltr/Assets.xcassets/monochrome.fill.imageset/1x.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/liltr/Assets.xcassets/monochrome.fill.imageset/2x.svg b/liltr/Assets.xcassets/monochrome.fill.imageset/2x.svg new file mode 100644 index 0000000..b7e9122 --- /dev/null +++ b/liltr/Assets.xcassets/monochrome.fill.imageset/2x.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/liltr/Assets.xcassets/monochrome.fill.imageset/3x.svg b/liltr/Assets.xcassets/monochrome.fill.imageset/3x.svg new file mode 100644 index 0000000..b7e9122 --- /dev/null +++ b/liltr/Assets.xcassets/monochrome.fill.imageset/3x.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/liltr/Assets.xcassets/monochrome.fill.imageset/Contents.json b/liltr/Assets.xcassets/monochrome.fill.imageset/Contents.json new file mode 100644 index 0000000..9e7e851 --- /dev/null +++ b/liltr/Assets.xcassets/monochrome.fill.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "1x.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "2x.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "3x.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/liltr/Assets.xcassets/monochrome.imageset/1x.svg b/liltr/Assets.xcassets/monochrome.imageset/1x.svg new file mode 100644 index 0000000..2ed1ba8 --- /dev/null +++ b/liltr/Assets.xcassets/monochrome.imageset/1x.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/liltr/Assets.xcassets/monochrome.imageset/2x.svg b/liltr/Assets.xcassets/monochrome.imageset/2x.svg new file mode 100644 index 0000000..2ed1ba8 --- /dev/null +++ b/liltr/Assets.xcassets/monochrome.imageset/2x.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/liltr/Assets.xcassets/monochrome.imageset/3x.svg b/liltr/Assets.xcassets/monochrome.imageset/3x.svg new file mode 100644 index 0000000..2ed1ba8 --- /dev/null +++ b/liltr/Assets.xcassets/monochrome.imageset/3x.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/liltr/Assets.xcassets/monochrome.imageset/Contents.json b/liltr/Assets.xcassets/monochrome.imageset/Contents.json new file mode 100644 index 0000000..3655788 --- /dev/null +++ b/liltr/Assets.xcassets/monochrome.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "1x.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "2x.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "3x.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/liltr/Components/LanguagePicker.swift b/liltr/Components/LanguagePicker.swift new file mode 100644 index 0000000..5b4923f --- /dev/null +++ b/liltr/Components/LanguagePicker.swift @@ -0,0 +1,17 @@ +import Foundation +import SwiftUI + +struct LanguagePicker: View { + @Binding var languageCode: String + + var withLabel: Bool + + var body: some View { + Picker(selection: $languageCode) { + ForEach(LANGUAGE_ARRAY, id: \.code) { language in + Text("\(language.flag)\(withLabel ? " \(language.name)" : "")").tag(language.code) + } + } label: {} + .frame(width: withLabel ? 130 : 50) + } +} diff --git a/liltr/Components/ProviderPicker.swift b/liltr/Components/ProviderPicker.swift new file mode 100644 index 0000000..c940efe --- /dev/null +++ b/liltr/Components/ProviderPicker.swift @@ -0,0 +1,15 @@ +import Foundation +import SwiftUI + +struct ProviderPicker: View { + @Binding var prividerName: String + + var body: some View { + Picker(selection: $prividerName) { + ForEach(PROVIDER_ARRAY, id: \.name) { provider in + Text("\(provider.name)").tag(provider.name) + } + } label: {} + .frame(width: 200) + } +} diff --git a/liltr/Components/TabItem.swift b/liltr/Components/TabItem.swift new file mode 100644 index 0000000..af463cd --- /dev/null +++ b/liltr/Components/TabItem.swift @@ -0,0 +1,63 @@ +import SwiftUI + +struct TabItem: View { + private let label: String + private let icon: String + private let active: Bool + + private let itemWidth = 81 + private let itemHeight = 50 + private let sizeHolder: SizeHolder + + @State private var hovering: Bool = false + + init(label: String, icon: String, active: Bool, sizeBase: Float? = nil) { + self.label = label + self.icon = icon + self.active = active + self.sizeHolder = SizeHolder(base: sizeBase) + } + + var body: some View { + VStack { + Image(systemName: icon) + .font(.system(size: CGFloat(sizeHolder.iconSize))) + .fontWeight(.semibold) + Spacer() + .frame(height: CGFloat(sizeHolder.innerGapSize)) + Text(label) + .font(.system(size: CGFloat(sizeHolder.fontSize))) + .fontWeight(.semibold) + } + .foregroundStyle(active ? .primary : .secondary) + .frame(width: CGFloat(itemWidth), height: CGFloat(itemHeight)) + .background(getBackgroundColor()) + .clipShape(RoundedRectangle(cornerRadius: CGFloat(sizeHolder.radiusSize))) + .onHover(perform: { hovering in + self.hovering = hovering + }) + } + + private func getBackgroundColor()-> Color { + let colorActive = Color.secondary.opacity(0.18) + let colorHover = Color.secondary.opacity(0.1) + let colorDefault = Color.clear + if (self.active) { + return colorActive + } else if (self.hovering) { + return colorHover + } else { + return colorDefault + } + } +} + + +#Preview { + HStack { + TabItem(label: "General", icon: "gear", active: false) + TabItem(label: "Providers", icon: "cube.box", active: true) + TabItem(label: "About", icon: "info.circle", active: false) + } + .padding() +} diff --git a/liltr/Components/ToolbarItem.swift b/liltr/Components/ToolbarItem.swift new file mode 100644 index 0000000..c8e02f7 --- /dev/null +++ b/liltr/Components/ToolbarItem.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct ToolbarItem: View { + var systemName: String + var action: () -> Void + + @State var hovering: Bool = false + + var body: some View { + Button(action: action, label: { + Image(systemName: systemName) + }) + .buttonStyle(.plain) + .foregroundColor(hovering ? .primary : .secondary) + .onHover(perform: { hovering in + self.hovering = hovering + }) + } +} diff --git a/liltr/Components/TopTabView.swift b/liltr/Components/TopTabView.swift new file mode 100644 index 0000000..71a6329 --- /dev/null +++ b/liltr/Components/TopTabView.swift @@ -0,0 +1,65 @@ +import SwiftUI + +class TabPane: Identifiable { + public let label: String + public let icon: String + public let view: AnyView + + init(label: String, icon: String, view: AnyView) { + self.label = label + self.icon = icon + self.view = view + } +} + +public struct TopTabView: View { + private let tabPanes: [TabPane] + + private let sizeHolder = SizeHolder() + + @State private var activeTabLabel: String + + init(tabPanes: [TabPane], defaultActiveTabLabel: String? = nil) { + self.tabPanes = tabPanes + self.activeTabLabel = defaultActiveTabLabel ?? tabPanes.first!.label + } + + public var tabBar: some View { + HStack(spacing: CGFloat(sizeHolder.outerGapSize)) { + Spacer() + ForEach(tabPanes) { tabPane in + TabItem(label: tabPane.label, icon: tabPane.icon, active: tabPane.label == activeTabLabel) + .onTapGesture(perform: { + self.activeTabLabel = tabPane.label + }) + } + Spacer() + } + } + + public var body: some View { + ZStack(alignment: .top) { + Color.clear + VStack(alignment: .leading) { + tabBar + Spacer() + .frame(height: CGFloat(sizeHolder.outerGapSize)) + Divider() + if let tabPane = tabPanes.first(where: {$0.label == activeTabLabel}) { + tabPane.view + } + } + } + } +} + +#Preview { + VStack { + let tab1 = TabPane(label: "tab1", icon: "1.square.fill", view: AnyView(Image(systemName: "1.square.fill"))) + let tab2 = TabPane(label: "tab2", icon: "2.square.fill", view: AnyView(Image(systemName: "2.square.fill"))) + let tab3 = TabPane(label: "tab3", icon: "3.square.fill", view: AnyView(Image(systemName: "3.square.fill"))) + VStack { + TopTabView(tabPanes: [tab1, tab2, tab3], defaultActiveTabLabel: "tab2") + } + } +} diff --git a/liltr/Info.plist b/liltr/Info.plist new file mode 100644 index 0000000..0072882 --- /dev/null +++ b/liltr/Info.plist @@ -0,0 +1,41 @@ + + + + + APP_NAME + liltr + AliAK + + AliSK + + BaiduAK + + BaiduSK + + BigHugeThesaurusSK + + CFBundleURLTypes + + + CFBundleURLName + com.rhinoc.liltr + CFBundleURLSchemes + + liltr + + + + NiuTransSK + + SUFeedURL + + SUPublicEDKey + jhRLvU+V5Vdg0XzLT8BD/ogDVcRh33w2cR8Ri7xGrCk= + VERSION + 0.0.1 + VolcengineAK + + VolcengineSK + + + diff --git a/liltr/Preview Content/Preview Assets.xcassets/Contents.json b/liltr/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/liltr/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/liltr/Settings/AboutView.swift b/liltr/Settings/AboutView.swift new file mode 100644 index 0000000..c900431 --- /dev/null +++ b/liltr/Settings/AboutView.swift @@ -0,0 +1,79 @@ +import SwiftUI +import Sparkle + +final class CheckForUpdatesViewModel: ObservableObject { + @Published var canCheckForUpdates = false + + init(updater: SPUUpdater) { + updater.publisher(for: \.canCheckForUpdates) + .assign(to: &$canCheckForUpdates) + } +} + +struct AboutView: View { + @ObservedObject private var _checkForUpdatesViewModel: CheckForUpdatesViewModel + private let _gapSize: CGFloat = 8 + private let _updater: SPUUpdater + + init(updater: SPUUpdater) { + self._updater = updater + self._checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) + } + + var body: some View { + VStack(alignment: .center) { + Spacer() + .frame(height: _gapSize * 2) + + HStack(alignment: .center) { + Image(nsImage: NSImage(named: "AppIcon")!) + .resizable() + .frame(width: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/, height: 100) + + VStack(alignment: .leading, spacing: 2) { + Text("\(APP_NAME)") + .font(.system(size: 18)) + .fontWeight(/*@START_MENU_TOKEN@*/.bold/*@END_MENU_TOKEN@*/) + .foregroundStyle(.primary) + + VStack(alignment: .leading) { + Text("Version \(Bundle.main.infoDictionary!["VERSION"] as! String)") + .foregroundStyle(.primary) + Spacer() + Text("© rhinoc") + Text("2023-2024. All Rights Reserved.") + } + .font(.system(size: 13)) + .foregroundStyle(.secondary) + .opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/) + } + .frame(height: 80) + } + + Divider() + .padding(EdgeInsets(top: _gapSize, leading: 0, bottom: _gapSize, trailing: 0)) + + HStack { + Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/, label: { + Text("Acknowledgements") + }) + + Spacer() + + Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/, label: { + Text("Visit Website") + }) + + Button(action: { + try! _updater.start() + _updater.checkForUpdates(); + }, label: { + Text("Check Updates...") + }) + } + .padding(EdgeInsets(top: 0, leading: _gapSize, bottom: 0, trailing: _gapSize)) + + } + .frame(width: 450, height: 140) + } +} diff --git a/liltr/Settings/GeneralView.swift b/liltr/Settings/GeneralView.swift new file mode 100644 index 0000000..6c40df1 --- /dev/null +++ b/liltr/Settings/GeneralView.swift @@ -0,0 +1,135 @@ +import SwiftUI +import WebKit +import KeyboardShortcuts +import ServiceManagement + +struct AlignedText: View { + let text: String + let width: Float + let alignment: Alignment + + init(text: String, width: Float = 135, alignment: Alignment = .trailing) { + self.text = text + self.width = width + self.alignment = alignment + } + + var body: some View { + Text(text) + .frame(width: CGFloat(width), alignment: alignment) + .fontWeight(.semibold) + } +} + +struct GeneralView: View { + @Default(\.launchAtLogin) var launchAtLogin + @Default(\.hotKey) var hotKey + @Default(\.ocrHotKey) var ocrHotKey + @Default(\.hotKeyTriggerInNotification) var hotKeyTriggerInNotification + @Default(\.primaryLanguage) var primaryLanguage + @Default(\.secondaryLanguage) var secondaryLanguage + @Default(\.menuIconSymbol) var menuIconSymbol + @Default(\.preProcessSource) var preProcessSource + + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .center) { + AlignedText(text: "Startup") + Toggle(isOn: $launchAtLogin, label: { + Text("Launch at Login") + }).toggleStyle(.checkbox) + .onChange(of: launchAtLogin) { _, newValue in + setLaunchAtLogin(newValue) + } + } + + Spacer() + .frame(height: 20) + + HStack { + AlignedText(text: "Translate HotKey") + KeyboardShortcuts.Recorder(for: .translate, onChange: onHotkeyChange) + } + + HStack { + AlignedText(text: "OCR HotKey") + KeyboardShortcuts.Recorder(for: .ocr, onChange: onOCRHotkeyChange) + } + + HStack { + AlignedText(text: "HotKey Action") + Toggle(isOn: $hotKeyTriggerInNotification, label: { + Text("In-Notification Mode") + }).toggleStyle(.checkbox) + } + + Spacer() + .frame(height: 20) + + + HStack { + AlignedText(text: "Primary Language") + LanguagePicker(languageCode: $primaryLanguage, withLabel: true) + } + HStack { + AlignedText(text: "Secondary Language") + LanguagePicker(languageCode: $secondaryLanguage, withLabel: true) + } + + Spacer() + .frame(height: 20) + + + HStack { + AlignedText(text: "Preprocess") + Toggle(isOn: $preProcessSource, label: { + Text("Preprocess Source Text") + }).toggleStyle(.checkbox) + } + + // HStack { + // AlignedText(text: "Icon Symbol") + // TextField("Icon Symbol", text: $menuIconSymbol) + // .frame(width: 130) + // } + } + .frame(width: 400, height: 230) + } + + func setLaunchAtLogin(_ enable: Bool) { + do { + if enable { + if SMAppService.mainApp.status == .enabled { + try? SMAppService.mainApp.unregister() + } + + try SMAppService.mainApp.register() + } else { + try SMAppService.mainApp.unregister() + } + } catch { + debugPrint("[Settings] Failed to \(enable ? "enable" : "disable") launch at login: \(error.localizedDescription)") + } + } + + func onHotkeyChange(hotkey: KeyboardShortcuts.Shortcut?) { + if hotkey != nil { + self.hotKey = hotkey!.description + } else { + self.hotKey = "" + } + } + + func onOCRHotkeyChange(hotkey: KeyboardShortcuts.Shortcut?) { + if hotkey != nil { + self.ocrHotKey = hotkey!.description + } else { + self.ocrHotKey = "" + } + } +} + +#Preview("Provider", traits: .fixedLayout(width: 400, height: 500)) { + GeneralView() + .padding() +} diff --git a/liltr/Settings/Hotkeys.swift b/liltr/Settings/Hotkeys.swift new file mode 100644 index 0000000..dedcb7f --- /dev/null +++ b/liltr/Settings/Hotkeys.swift @@ -0,0 +1,6 @@ +import KeyboardShortcuts + +extension KeyboardShortcuts.Name { + static let translate = Self("translate") + static let ocr = Self("ocr") +} diff --git a/liltr/Settings/ProvidersView.swift b/liltr/Settings/ProvidersView.swift new file mode 100644 index 0000000..529ec32 --- /dev/null +++ b/liltr/Settings/ProvidersView.swift @@ -0,0 +1,94 @@ +import SwiftUI + +struct ProviderKeyField: View { + let label: String + let icon: String + + @Binding var ak: String + @Binding var sk: String + + var body: some View { + VStack(alignment: .leading) { + HStack { + AlignedText(text: "Access Key (AK)", width: 120) + TextField("Access Key ID", text: $ak) + .frame(width: 200) + } + .padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0)) + HStack { + AlignedText(text: "Secret Key (SK)", width: 120) + SecureField("Secret Key", text: $sk) + .frame(width: 200) + + } + Spacer() + }.tabItem { + Label(label, systemImage: icon) + } + } +} + +struct ProvidersView: View { + @Default(\.primaryProvider) var primaryProvider + @Default(\.secondaryProvider) var secondaryProvider + + @Default(\.NiuTransAK) var niuTransAK + @Default(\.NiuTransSK) var niuTransSK + + @Default(\.BaiduAK) var baiduAK + @Default(\.BaiduSK) var baiduSK + + @Default(\.VolcengineAK) var volcengineAK + @Default(\.VolcengineSK) var volcengineSK + + @Default(\.AliAK) var aliAK + @Default(\.AliSK) var aliSK + + @Default(\.dictionary) var dictionary + + private let _gapSize: CGFloat = 8 + + var body: some View { + VStack(alignment: .center) { + VStack(alignment: .center) { + HStack { + AlignedText(text: "Primary Provider") + ProviderPicker(prividerName: $primaryProvider) + } + HStack { + AlignedText(text: "Secondary Provider") + ProviderPicker(prividerName: $secondaryProvider) + } + HStack { + AlignedText(text: "Dictionary") + Picker(selection: $dictionary) { + ForEach(AppleDictionaryProvider.shared.getDictionaries(), id: \.self) { dictName in + Text("\(dictName)").tag(dictName) + } + } label: {} + .frame(width: 200) + } + + }.padding(EdgeInsets(top: _gapSize * 2, leading: _gapSize * 2, bottom: 0, trailing: _gapSize * 2)) + + Divider() + .padding(EdgeInsets(top: _gapSize, leading: 0, bottom: _gapSize, trailing: 0)) + + TabView(content: { + ProviderKeyField(label: "NiuTrans", icon: "1.square", ak: $niuTransAK, sk: $niuTransSK) + + ProviderKeyField(label: "Volcengine", icon: "2.square", ak: $volcengineAK, sk: $volcengineSK) + + ProviderKeyField(label: "Ali", icon: "3.square", ak: $aliAK, sk: $aliSK) + + ProviderKeyField(label: "Baidu", icon: "4.square", ak: $baiduAK, sk: $baiduSK) + }).padding(EdgeInsets(top: 0, leading: _gapSize * 2, bottom: 0, trailing: _gapSize * 2)) + } + .frame(width: 400, height: 220) + } +} + +#Preview("Provider", traits: .fixedLayout(width: 400, height: 500)) { + ProvidersView() + .padding() +} diff --git a/liltr/Settings/SettingsView.swift b/liltr/Settings/SettingsView.swift new file mode 100644 index 0000000..1df7129 --- /dev/null +++ b/liltr/Settings/SettingsView.swift @@ -0,0 +1,49 @@ +import SwiftUI +import SwiftData +import Sparkle + +struct SettingsView: View { + private let _updater: SPUUpdater + + init(updater: SPUUpdater) { + self._updater = updater + } + + private func _handleIncomingURL(_ url: URL) { + guard url.scheme == APP_NAME else { + return + } + + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + return + } + + let action = components.host + guard action == SchemeAction.settings.rawValue else { + return + } + } + + var body: some View { + VStack { + let generalTabPane = TabPane(label: "General", icon: "gear", view: AnyView(GeneralView())) + let providersTabPane = TabPane(label: "Providers", icon: "cube.box", view: AnyView(ProvidersView())) + let aboutTabPane = TabPane(label: "About", icon: "info.circle", view: AnyView(AboutView(updater: _updater))) + VStack(alignment: .center) { + Spacer() + .frame(height: 6) + Text("Settings") + .fontWeight(.bold) + Spacer() + .frame(height: 6) + } + + TopTabView(tabPanes: [generalTabPane, providersTabPane, aboutTabPane]) + } + .foregroundColor(.secondary) + .edgesIgnoringSafeArea(.all) + .onOpenURL(perform: { url in + _handleIncomingURL(url) + }) + } +} diff --git a/liltr/Translate/BottomBarView.swift b/liltr/Translate/BottomBarView.swift new file mode 100644 index 0000000..b69c551 --- /dev/null +++ b/liltr/Translate/BottomBarView.swift @@ -0,0 +1,85 @@ +import SwiftUI +import SwiftData +import UniformTypeIdentifiers + +let PART_WIDTH: CGFloat = 80 +let HEIGHT: CGFloat = 20 + +struct BottomBarView: View { + @Binding var languageCode: String + @Binding var isDictionaryMode: Bool + @ObservedObject var provider = ProviderManager.shared + + var getCopyText: () -> String + var getSpeechText: () -> String + var onChangeProvider: () -> Void + + var itemSwitchProvider: some View { + return ToolbarItem(systemName: provider.usePrimary ? "circle.grid.2x1.left.filled" : "circle.grid.2x1.right.filled") { + provider.switchProvider() + onChangeProvider() + } + } + + var itemDictionary: some View { + return ToolbarItem(systemName: isDictionaryMode ? "escape" : "character.magnify") { + isDictionaryMode = !isDictionaryMode + } + } + + var itemCopy: some View { + return ToolbarItem(systemName: "square.on.square") { + copyToPasteboard(getCopyText()) + } + } + + var itemSpeech: some View { + return ToolbarItem(systemName: "speaker.2") { + let text = getSpeechText() + let language = LanguageManager.getLanguageByContent(text) + SpeechManager.start(text, language) + } + } + + var body: some View { + HStack { + if isDictionaryMode { + itemDictionary + + Spacer() + + HStack() { + Spacer() + itemCopy + itemSpeech + } + .frame(width: PART_WIDTH) + + } else { + // MARK: left + HStack { + LanguagePicker(languageCode: $languageCode, withLabel: false) + Spacer() + } + .frame(width: PART_WIDTH) + + Spacer() + + // MARK: mid + itemSwitchProvider + + Spacer() + + // MARK: right + HStack() { + Spacer() + itemDictionary + itemCopy + itemSpeech + } + .frame(width: PART_WIDTH) + } + } + .frame(height: HEIGHT) + } +} diff --git a/liltr/Translate/MidBarView.swift b/liltr/Translate/MidBarView.swift new file mode 100644 index 0000000..4b3a9d4 --- /dev/null +++ b/liltr/Translate/MidBarView.swift @@ -0,0 +1,49 @@ +import SwiftUI +import SwiftData +import UniformTypeIdentifiers + +struct MidBarView: View { + var isLoading: Bool + var onSwap: () -> Void + var onDrag: (_ height: CGFloat) -> Void = { height in } + + private let height = CGFloat(25) + + var body: some View { + ZStack { + Divider() + + Rectangle() + .fill(.clear) + .cursor(.resizeUpDown) + .gesture( + DragGesture() + .onChanged { value in + onDrag(value.translation.height) + } + ) + + if isLoading { + ProgressView() + .scaleEffect(CGSize(width: 0.5, height: 0.5)) + } else { + Button { + onSwap() + } label: { + Image(systemName: "arrow.up.arrow.down") + } + .background(Color.backgroundColor) + } + }.frame(height: height) + } + +} + +#Preview("MidBar isLoading", traits: .fixedLayout(width: 300, height: 200)) { + MidBarView(isLoading: true, onSwap: {}) +} + +#Preview("MidBar", traits: .fixedLayout(width: 300, height: 200)) { + MidBarView(isLoading: false, onSwap: {}) +} + diff --git a/liltr/Translate/TopBarView.swift b/liltr/Translate/TopBarView.swift new file mode 100644 index 0000000..2a9c78b --- /dev/null +++ b/liltr/Translate/TopBarView.swift @@ -0,0 +1,35 @@ +import SwiftUI +import SwiftData +import UniformTypeIdentifiers + +struct TopBarView: View { + @Environment(\.openWindow) private var openWindow + @Default(\.floatOnTop) var floatOnTop + + var body: some View { + HStack { + Spacer() + ToolbarItem(systemName: "gearshape", action: { + WindowManager.open(openWindow: openWindow, id: .settings) + }) + ToolbarItem(systemName: floatOnTop ? "pin.fill" : "pin", action: { + floatOnTop = !floatOnTop + float() + }) + } + .padding(EdgeInsets(top: 1, leading: 100, bottom: 0, trailing: 0)) + .onAppear { + float() + } + } + + func float() { + WindowManager.float(id: .translate, enable: floatOnTop) + } + +} + +#Preview("TopBar", traits: .fixedLayout(width: 300, height: 200)) { + TopBarView() +} + diff --git a/liltr/Translate/TranslateFieldView.swift b/liltr/Translate/TranslateFieldView.swift new file mode 100644 index 0000000..f4c76fd --- /dev/null +++ b/liltr/Translate/TranslateFieldView.swift @@ -0,0 +1,74 @@ +import SwiftUI +import SwiftData +import Combine +import UniformTypeIdentifiers + +struct TranslateFieldView: View { + @Binding var text: String + var placeholder: String = "" + var onChange: (() -> Void)? + var readOnly: Bool = false + var maxLength: Int = 5000 + + @FocusState private var focused: Bool + @State private var triggered = false + let debouncer = PassthroughSubject() + + private let fontSize = CGFloat(14) + private let paddingSize = CGFloat(10) + + var body: some View { + ZStack(alignment: .topLeading) { + ZStack(alignment: .bottomTrailing) { + TextEditor(text: readOnly ? .constant(text) : $text) + .font(.system(size: fontSize)) + .scrollContentBackground(.hidden) + .padding(EdgeInsets(top: 0, leading: paddingSize, bottom: 0, trailing: paddingSize)) + .onReceive(debouncer.debounce(for: .milliseconds(triggered ? 300 : 500), scheduler: RunLoop.main)) { input in + onChange?() + } + .onChange(of: text) { + if (text.isEmpty) { + triggered = false + } else if (!triggered && CharacterSet.whitespacesAndNewlines.contains(text.last!.unicodeScalars.first!)) { + triggered = true + + } + debouncer.send(text) + } +// .onChange(of: text, Debouncer.debounce(delay: .milliseconds(300), action: onChange ?? {})) + .focused($focused) + .bottomFade() + .onAppear { + if (!readOnly) { + focused = true + } + } + .onReceive(Just(text)) { _ in + if text.count > maxLength && !readOnly { + text = String(text.prefix(maxLength)) + } + } + + if !readOnly && !text.isEmpty { + Image(systemName: "xmark.circle.fill") + .onTapGesture { + text = "" + onChange?() + } + .foregroundStyle(.primary.opacity(0.6)) + .padding(EdgeInsets(top: 0, leading: paddingSize, bottom: 0, trailing: paddingSize)) + } + } + + if text.isEmpty && !placeholder.isEmpty { + TextEditor(text: .constant(placeholder)) + .font(.system(size: fontSize)) + .foregroundColor(.secondary) + .scrollContentBackground(.hidden) + .padding(EdgeInsets(top: 0, leading: paddingSize, bottom: 0, trailing: paddingSize)) + .disabled(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + } + } + } +} diff --git a/liltr/Translate/TranslateView.swift b/liltr/Translate/TranslateView.swift new file mode 100644 index 0000000..b764bb2 --- /dev/null +++ b/liltr/Translate/TranslateView.swift @@ -0,0 +1,136 @@ +import SwiftUI +import SwiftData +import UniformTypeIdentifiers +import WebKit + +struct HTMLStringView: NSViewRepresentable { + typealias NSViewType = WKWebView + + let htmlContent: String + + func makeNSView(context: Context) -> WKWebView { + var view = WKWebView() + view.setValue(false, forKey: "drawsBackground") + return view + } + + func updateNSView(_ nsView: WKWebView, context: Context) { + nsView.loadHTMLString(htmlContent, baseURL: nil) + } +} + +struct TranslateView: View { + @ObservedObject private var provider = ProviderManager.shared + + @State private var sourceText: String = "" + @State private var targetText: String = "" + @State private var targetLanguageCode: String = Defaults.shared.primaryLanguage + @State private var height: CGFloat = 100 + @State private var isDictionaryMode: Bool = false + + private let MIN_HEIGHT: CGFloat = 40 + + private func _handleIncomingURL(_ url: URL) { + guard url.scheme == APP_NAME else { + return + } + + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + return + } + + let action = components.host + guard action == SchemeAction.translateInWindow.rawValue else { + return + } + + let src = components.queryItems?.first(where: { $0.name == "src" })?.value ?? "" + guard !src.isEmpty else { + return + } + + sourceText = src.removingPercentEncoding! + } + + var body: some View { + GeometryReader {geometry in + VStack(alignment: .leading) { + // MARK: top bar + TopBarView() + .padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 4)) + + VStack(alignment: .leading, spacing: 0) { + // MARK: source text field + TranslateFieldView(text: $sourceText, placeholder: "Type any words to start...", onChange: onSourceInput) + .frame(height: self.height) + + // MARK: mid bar + MidBarView(isLoading: provider.isTranslating, onSwap: onSwap) { height in + self.height = floor(minMax(self.height + height, min: MIN_HEIGHT, max: geometry.size.height - 100)) + } + + // MARK: target text field + if (targetText.starts(with: " String { + return sourceText + } + + func getTargetText() -> String { + return targetText + } + + func onSwap() { + sourceText = targetText + targetLanguageCode = targetLanguageCode == Defaults.shared.primaryLanguage ? Defaults.shared.secondaryLanguage : Defaults.shared.primaryLanguage + } + + func onSourceInput() { + if (sourceText.isEmpty) { + targetText = "" + } else { + provider.translate(sourceText, isDictionaryMode ? nil : LanguageManager.getLanguageByCode(targetLanguageCode)!, updateTargetText) + } + } + + func updateTargetText(_ result: ProviderCallbackData) { + self.targetText = result.target + self.isDictionaryMode = result.isDictionary + if (result.targetLanguage != nil) { + self.targetLanguageCode = result.targetLanguage!.code + } + } +} diff --git a/liltr/Utils/Common/Common.swift b/liltr/Utils/Common/Common.swift new file mode 100644 index 0000000..9af03ab --- /dev/null +++ b/liltr/Utils/Common/Common.swift @@ -0,0 +1,85 @@ +import Foundation +import Alamofire +import SwiftUI + +func dict2headers(dict: [String: String]) -> HTTPHeaders { + var httpHeaders = HTTPHeaders() + for (key, value) in dict { + httpHeaders.add(name: key, value: value) + } + return httpHeaders +} + +func minMax(_ x: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat { + if (x < min) { + return min + } else if (x > max) { + return max + } else { + return x + } +} + +func copyToPasteboard(_ string: String) { + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.setString(string, forType: .string) +} + +func getSelectedText() -> String? { + let systemWideElement = AXUIElementCreateSystemWide() + + let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String : true] + let accessEnabled = AXIsProcessTrustedWithOptions(options) + + var selectedTextValue: AnyObject? + let errorCode = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute as CFString, &selectedTextValue) + + if errorCode == .success { + let selectedTextElement = selectedTextValue as! AXUIElement + var selectedText: AnyObject? + let textErrorCode = AXUIElementCopyAttributeValue(selectedTextElement, kAXSelectedTextAttribute as CFString, &selectedText) + + if textErrorCode == .success, let selectedTextString = selectedText as? String { + return selectedTextString + } else { + debugPrint("[getSelectedText] AXUIElementCopyAttributeValue errorCode invalid:", textErrorCode) + return nil + } + } else { + debugPrint("[getSelectedText] errorCode invalid:", errorCode) + return nil + } +} + +class MatchManager { + private var info: [String] = [] + + func set(_ info: [String]) { + self.info = info + } + + func match(_ info: [String]) -> Bool { + if info.count != self.info.count { + return false + } else { + return info == self.info + } + } + + func reset() { + self.info = [] + } +} + +func regexMatched(_ string: String, _ regex: String) -> Bool { + do { + let regex = try NSRegularExpression(pattern: regex) + let range = NSRange(location: 0, length: string.utf16.count) + let match = regex.firstMatch(in: string, options: [], range: range) + + return match != nil + } catch let error { + return false + } +} diff --git a/liltr/Utils/Common/Debouncer.swift b/liltr/Utils/Common/Debouncer.swift new file mode 100644 index 0000000..72a7f7d --- /dev/null +++ b/liltr/Utils/Common/Debouncer.swift @@ -0,0 +1,48 @@ +import Foundation + +extension TimeInterval { + + /** + Checks if `since` has passed since `self`. + + - Parameter since: The duration of time that needs to have passed for this function to return `true`. + - Returns: `true` if `since` has passed since now. + */ + func hasPassed(since: TimeInterval) -> Bool { + return Date().timeIntervalSinceReferenceDate - self > since + } + +} + +// https://gist.github.com/simme/b78d10f0b29325743a18c905c5512788 +class Throttler { + static var currentWorkItem: DispatchWorkItem? + static var lastFire: TimeInterval = 0 + + static func throttle(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void { + + return { [] in + guard Throttler.currentWorkItem == nil else { return } + Throttler.currentWorkItem = DispatchWorkItem { + action() + self.lastFire = Date().timeIntervalSinceReferenceDate + self.currentWorkItem = nil + } + + delay.hasPassed(since: self.lastFire) ? queue.async(execute: self.currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: self.currentWorkItem!) + } + } +} + +class Debouncer { + static var currentWorkItem: DispatchWorkItem? + + static func debounce(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void { + return { [] in + Debouncer.currentWorkItem?.cancel() + Debouncer.currentWorkItem = DispatchWorkItem { action() } + queue.asyncAfter(deadline: .now() + delay, execute: self.currentWorkItem!) + } + } +} + diff --git a/liltr/Utils/CryptoEncoder/Encoder.swift b/liltr/Utils/CryptoEncoder/Encoder.swift new file mode 100644 index 0000000..bd33ada --- /dev/null +++ b/liltr/Utils/CryptoEncoder/Encoder.swift @@ -0,0 +1,55 @@ +import Foundation +import CryptoKit + +class CryptoEncoder { + static func str2data(_ string: String) -> Data { + return Data(string.utf8) + } + + static func data2str(_ data: Data) -> String { + return data.map { String(format: "%02hhx", $0) }.joined() + } + + static func md5(data: Data) -> Data { + return Data(Insecure.MD5.hash(data: data)) + } + + static func md5(string: String) -> String { + let stringData = CryptoEncoder.str2data(string) + let md5Data = md5(data: stringData) + return data2str(md5Data) + } + + static func base64(data: Data) -> String { + return data.base64EncodedString() + } + + static func base64(data: Data) -> Data { + return data.base64EncodedData() + } + + static func base64(string: String) -> String { + let stringData = CryptoEncoder.str2data(string) + return stringData.base64EncodedString() + } + + +// +// static func md5(_ string: String) -> Data { +// let data = Insecure.MD5.hash(data: Data(string.utf8)) +// return data.map { +// String(format: "%02hhx", $0) +// }.joined() +// } +// +// static func md5Base64(_ string: String) -> String { +// let digest = Insecure.MD5.hash(data: Data(string.utf8)) +// return Data(digest).base64EncodedString() +// } +// +// static func base64(_ string: String) -> String { +// let inputData = Data(string.utf8) +// return inputData.base64EncodedString() +// } + +} diff --git a/liltr/Utils/KeyboardShortcut/KeyboardShortcut.swift b/liltr/Utils/KeyboardShortcut/KeyboardShortcut.swift new file mode 100644 index 0000000..04f2afe --- /dev/null +++ b/liltr/Utils/KeyboardShortcut/KeyboardShortcut.swift @@ -0,0 +1,32 @@ +import KeyboardShortcuts +import SwiftUI + +func string2Shortcut(_ str: String) -> KeyboardShortcut? { + if (str.count == 0) { + return nil + } + + var modifiers: SwiftUI.EventModifiers = [] + + if str.contains("⌘") { + modifiers.update(with: EventModifiers.command) + } + + if str.contains("⌃") { + modifiers.update(with: EventModifiers.control) + } + + if str.contains("⌥") { + modifiers.update(with: EventModifiers.option) + } + + if str.contains("⇧") { + modifiers.update(with: EventModifiers.shift) + } + + if str.contains("⇪") { + modifiers.update(with: EventModifiers.capsLock) + } + + return KeyboardShortcut(KeyEquivalent(str.last!), modifiers: modifiers) +} diff --git a/liltr/Utils/Language/Language.swift b/liltr/Utils/Language/Language.swift new file mode 100644 index 0000000..5939c09 --- /dev/null +++ b/liltr/Utils/Language/Language.swift @@ -0,0 +1,16 @@ +struct Language { + var code: String + var flag: String + var name: String + + var shortCode: String { + return String(code.split(separator: "-")[0]) + } + + init(code: String, flag: String, name: String) { + self.code = code + self.flag = flag + self.name = name + } +} + diff --git a/liltr/Utils/Language/LanguageManager.swift b/liltr/Utils/Language/LanguageManager.swift new file mode 100644 index 0000000..8da8df3 --- /dev/null +++ b/liltr/Utils/Language/LanguageManager.swift @@ -0,0 +1,92 @@ +import Foundation +import NaturalLanguage + +let LANGUAGE_ARRAY = [ + Language(code: "zh-CN", flag: "🇨🇳", name: "简体中文"), + Language(code: "en-US", flag: "🇺🇸", name: "English"), + Language(code: "ja-JP", flag: "🇯🇵", name: "日本語"), + Language(code: "ko-KR", flag: "🇰🇷", name: "한국어"), + Language(code: "fr-FR", flag: "🇫🇷", name: "Français"), + Language(code: "es-ES", flag: "🇪🇸", name: "Español"), + Language(code: "pt-PT", flag: "🇵🇹", name: "Português"), + Language(code: "it-IT", flag: "🇮🇹", name: "Italiano"), + Language(code: "de-DE", flag: "🇩🇪", name: "Deutsch"), + Language(code: "tr-TR", flag: "🇹🇷", name: "Türkçe"), + Language(code: "th-TH", flag: "🇹🇭", name: "ไทย"), + Language(code: "ar-AE", flag: "🇸🇦", name: "العربية"), + Language(code: "id-ID", flag: "🇮🇩", name: "Bahasa Indonesia"), + Language(code: "ms-MY", flag: "🇲🇾", name: "Bahasa Melayu"), + Language(code: "vi-VN", flag: "🇻🇳", name: "Tiếng Việt"), + Language(code: "hi-IN", flag: "🇮🇳", name: "हिन्दी") +] + +let LANGUAGE_DICT = Dictionary(uniqueKeysWithValues: LANGUAGE_ARRAY.map{ ($0.code, $0) }) + +class LanguageManager { + static var primaryLanguage: Language { + return LanguageManager.getLanguageByCode(Defaults.shared.primaryLanguage)! + } + + static var secondaryLanguage: Language { + return LanguageManager.getLanguageByCode(Defaults.shared.secondaryLanguage)! + } + + static func getShortCode(_ code: String) -> String { + if (code.contains("-")) { + return String(code.split(separator: "-")[0]) + } else if (code.contains("_")) { + return String(code.split(separator: "-")[0]) + } + return code + } + + static func getLanguageByCode(_ code: String) -> Language? { + if (LANGUAGE_DICT[code] != nil) { + return LANGUAGE_DICT[code] + } + + let shortCode = getShortCode(code) + for language in LANGUAGE_ARRAY { + if shortCode == language.shortCode { + return language + } + } + + return nil + } + + static func getStandardCode(_ code: String) -> String? { + return getLanguageByCode(code)?.code + } + + static func getLanguageByContent(_ content: String) -> Language { + let recognizer = NLLanguageRecognizer() + recognizer.processString(content) + + if let language = recognizer.dominantLanguage { + let code = language.rawValue.description + debugPrint("[LanguageManager] getLanguageByContent", code) + return getLanguageByCode(code) ?? secondaryLanguage + } + + return secondaryLanguage + } + + static func getFromTo(_ source: String, _ oldTargetLanguage: Language?) -> (Language, Language)? { + if (source.isEmpty) { + return nil + } + + let recognizedLanguage = LanguageManager.getLanguageByContent(source) + var targetLanguage = oldTargetLanguage + if targetLanguage != nil && recognizedLanguage.shortCode == targetLanguage!.shortCode { + if oldTargetLanguage!.code != Defaults.shared.primaryLanguage { + targetLanguage = primaryLanguage + } else { + targetLanguage = secondaryLanguage + } + } + + return (recognizedLanguage, targetLanguage ?? recognizedLanguage) + } +} diff --git a/liltr/Utils/Language/SpeechManager.swift b/liltr/Utils/Language/SpeechManager.swift new file mode 100644 index 0000000..5b3cfc9 --- /dev/null +++ b/liltr/Utils/Language/SpeechManager.swift @@ -0,0 +1,55 @@ +import Foundation +import AVFoundation + +class SpeechManager { + private static let speechSynthesizer = AVSpeechSynthesizer() + + private static func getVoiceScore(voice: AVSpeechSynthesisVoice, language: Language) -> Int { + var score = 1 + if (voice.language == language.code) { + score += 10 + } + if (voice.quality == .premium) { + score += 8 + } else if (voice.quality == .enhanced) { + score += 5 + } + + return score + } + + static func stop(at boundary: AVSpeechBoundary = .immediate) { + speechSynthesizer.stopSpeaking(at: boundary) + } + + static func start(_ string: String, _ language: Language) { + if (string.isEmpty) { + return + } + + if (speechSynthesizer.isSpeaking) { + stop() + } else { + let speechUtterance: AVSpeechUtterance = AVSpeechUtterance(string: string) +// speechUtterance.rate = AVSpeechUtteranceMaximumSpeechRate / 2.5 + speechUtterance.voice = getVoiceByLanguage(language) + speechSynthesizer.speak(speechUtterance) + } + } + + static func getVoiceByLanguage(_ language: Language) -> AVSpeechSynthesisVoice? { + var resultScore: Int = 0 + var result: AVSpeechSynthesisVoice? = nil + for voice in AVSpeechSynthesisVoice.speechVoices() { + if (voice.language == language.code || LanguageManager.getShortCode(voice.language) == language.shortCode) { + let score = getVoiceScore(voice: voice, language: language) + if (score > resultScore) { + result = voice + resultScore = score + } + } + } + return result + } + +} diff --git a/liltr/Utils/Notification/NotificationManager.swift b/liltr/Utils/Notification/NotificationManager.swift new file mode 100644 index 0000000..1d07934 --- /dev/null +++ b/liltr/Utils/Notification/NotificationManager.swift @@ -0,0 +1,27 @@ +import UserNotifications + +func pushNotification(title: String, body: String) { + let categoryIdentifier = "translate" + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { (settings) in + if settings.authorizationStatus == .authorized { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.categoryIdentifier = categoryIdentifier + + let copy = UNNotificationAction(identifier: "copy", title: "Copy") + let speak = UNNotificationAction(identifier: "speak", title: "Speak") + let expand = UNNotificationAction(identifier: "expand", title: "Expand", options: [.foreground]) + let category = UNNotificationCategory(identifier: categoryIdentifier, actions: [copy, speak, expand], intentIdentifiers: []) + center.setNotificationCategories([category]) + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + center.add(request) + } else { + center.requestAuthorization(options: [.alert, .badge, .sound]) { success, error in + debugPrint("[pushNotification] request for authorization", success, error) + } + } + } +} diff --git a/liltr/Utils/OCR/OCRManager.swift b/liltr/Utils/OCR/OCRManager.swift new file mode 100644 index 0000000..f5f4e26 --- /dev/null +++ b/liltr/Utils/OCR/OCRManager.swift @@ -0,0 +1,80 @@ +import Foundation +import Vision +import SwiftUI + +// https://github.com/amebalabs/TRex/blob/main/TRex%20Core/TRex.swift +// https://developer.apple.com/documentation/vision/recognizing_text_in_images +class OCRManager { + public static let shared = OCRManager() + + private var _task: Process? + private var _taskId: String? + + private func resetTask() { + _task?.terminate() + _taskId = nil + _task = nil + } + + func capture() -> NSImage? { + resetTask() + + let taskId = String(Int(round(Date().timeIntervalSince1970))) + let tempPath = NSURL.fileURL(withPathComponents: [NSTemporaryDirectory(), "capture_\(taskId).png"])!.path() + _taskId = taskId + _task = Process() + _task?.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") + _task?.arguments = ["-i", tempPath] + + do { + try _task?.run() + } catch let error { + debugPrint("[OCRManager]", error) + resetTask() + return nil + } + + _task?.waitUntilExit() + if (_taskId != taskId) { + return nil + } + resetTask() + return NSImage(contentsOfFile: tempPath) + } + + private func _recoginizeTextHandler(request: VNRequest, error: Error?, cb: @escaping (String) -> Void) { + guard let observations = + request.results as? [VNRecognizedTextObservation] else { + return + } + if (observations.isEmpty) { + debugPrint("[OCR Manager] observations result is empty") + return + } + let recognizedStrings = observations.compactMap { observation in + return observation.topCandidates(1).first?.string + } + + cb(recognizedStrings.joined(separator: "\n")) + } + + func ocr(image: CGImage, cb: @escaping (String) -> Void) { + let requestHandler = VNImageRequestHandler(cgImage: image) + let request = VNRecognizeTextRequest { request, error in + self._recoginizeTextHandler(request: request, error: error, cb: cb) + } + request.automaticallyDetectsLanguage = true + do { + try requestHandler.perform([request]) + } catch { + debugPrint("[OCR Manager] Unable to perform the requests: \(error)") + } + } + + func captureWithOCR(cb: @escaping (String) -> Void) { + let image = self.capture() + if (image != nil) { + ocr(image: image!.cgImage(forProposedRect: nil, context: nil, hints: nil)!, cb: cb) + } + } +} diff --git a/liltr/Utils/Provider/Ali.swift b/liltr/Utils/Provider/Ali.swift new file mode 100644 index 0000000..f965959 --- /dev/null +++ b/liltr/Utils/Provider/Ali.swift @@ -0,0 +1,113 @@ + + +import Alamofire +import Foundation +import CryptoKit + +struct AliResult: Decodable { + let Translated: String? + let WordCount: String? + let DetectedLanguage: String? +} + +struct AliResponse: BaseResponse { + let Code: String? + let Message: String? + let RequestId: String? + let Data: AliResult? + + var target: String? { + if (Data?.Translated?.isEmpty == false) { + return Data!.Translated! + } + return nil + } + + var errorMessage: String? { + if (Message != nil) { + return "\(Code!): \(Message!)" + } + return nil + } +} + +let AliProviderName = "Ali" + +class AliProvider: BaseProvider { + static let shared = AliProvider() + let delay: DispatchTimeInterval = .seconds(1) + let name = AliProviderName + let apiUrl = "https://mt.cn-hangzhou.aliyuncs.com/api/translate/web/general" + + var ak = Defaults.shared.AliAK.isEmpty ? Bundle.main.infoDictionary!["AliAK"] as! String : Defaults.shared.AliAK + var sk = Defaults.shared.AliSK.isEmpty ? Bundle.main.infoDictionary!["AliSK"] as! String : Defaults.shared.AliSK + + private func _getDate() -> String { + let date = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "E, dd MMM yyyy HH:mm:ss z" + dateFormatter.locale = Locale(identifier: "en_UK") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + return dateFormatter.string(from: date) + } + + private func _getHeaders() -> HTTPHeaders { + let date = self._getDate() + let url = URL(string: apiUrl)! + let path = url.path + let uuid = String(Int(round(Date().timeIntervalSince1970))) + var headers = [ + "Accept": "application/json", + "Content-Type": "application/json;chrset=utf-8", + "Date": date, + "Host": "mt.cn-hangzhou.aliyuncs.com", + "x-acs-signature-method": "HMAC-SHA1", + "x-acs-signature-nonce": uuid, + ] + + let stringToSignArr: [String] = ["POST", headers["Accept"]!+"\n", headers["Content-Type"]!, headers["Date"]!, "x-acs-signature-method:HMAC-SHA1", "x-acs-signature-nonce:\(uuid)", path] + let stringToSign = stringToSignArr.joined(separator: "\n") + let signature = hmac_sha1(sk, stringToSign) + let authHeader = "acs \(ak):\(signature)" + headers.updateValue(authHeader, forKey: "Authorization") + + return dict2headers(dict: headers) + } + + private func hmac_sha1(_ key: String, _ content: String) -> String { + let _key = key.data(using: .utf8)! + let data = content.data(using: .utf8)! + let hmac = HMAC.authenticationCode(for: data, using: SymmetricKey(data: _key)) + return Data(hmac).base64EncodedString() + } + + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ _sourceLanguage: Language? , _ _targetLanguage: Language?) -> Void) -> Void { + let parameters: [String: String] = [ + "FormatType": "text", + "SourceText": source, + "SourceLanguage": from.shortCode, + "TargetLanguage": to.shortCode, + "Scene": "general", + ] + let headers = _getHeaders() + + debugPrint("[AliProvider] parameters:", parameters) + + AF.request(apiUrl, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers) + .cacheResponse(using: .cache) + .responseDecodable(of: AliResponse.self) { response in + if (response.error != nil) { + cb(response.error!.errorDescription!, nil, nil) + } else if (response.value?.errorMessage != nil) { + cb(response.value!.errorMessage!, nil, nil) + } else { + cb(response.value!.target!, from, to) + } + } + } + +} + + + diff --git a/liltr/Utils/Provider/AppleDictionary.swift b/liltr/Utils/Provider/AppleDictionary.swift new file mode 100644 index 0000000..4ca909a --- /dev/null +++ b/liltr/Utils/Provider/AppleDictionary.swift @@ -0,0 +1,122 @@ +import Alamofire +import SwiftUI +import Foundation + +func replaceLinks(html: String) -> String { + let regex = try! NSRegularExpression(pattern: "]*>(.*?)") + var result = html + for match in regex.matches(in: html, range: NSRange(0..\(captured)") + } + } + return result +} + +func extractFromHTML(html: String, tag: String) -> String { + do { + let pattern = "(?<=<\(tag)>)(.*?)(?=)" + let regex = try NSRegularExpression(pattern: pattern, options: .dotMatchesLineSeparators) + let nsString = html as NSString + let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsString.length)) + + guard let match = matches.first else { return "" } + let bodyContent = nsString.substring(with: match.range) + + if let data = bodyContent.data(using: .utf8), let decodedString = String(data: data, encoding: .utf8) { + return decodedString + } else { + throw NSError(domain: "Invalid encoding", code: 1, userInfo: nil) + } + } catch { + print("[extractFromHTML] process error: \(error)") + return "" + } +} + + +let AppleDictionaryProviderName = "AppleDictionary" + +// https://github.com/tisfeng/Easydict/blob/main/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md +// https://dictionaries.io/ +class AppleDictionaryProvider: BaseProvider { + static let shared = AppleDictionaryProvider() + + var delay: DispatchTimeInterval = .milliseconds(250) + let name = AppleDictionaryProviderName + + var dictionary: String { + return Defaults.shared.dictionary + } + + private func _lookupNative(_ word: String) -> String { + let cfWord = word as CFString + let range = DCSGetTermRangeInString(nil, cfWord, 0) + if let definition = DCSCopyTextDefinition(nil, cfWord, range) { + let definitionString = String(definition.takeRetainedValue()) + return definitionString.split(separator: " | ").joined(separator: "\n") + } else { + return ("No definition found for \(word)") + } + } + + private func _lookUpByDictionary(term: String, dictionary: String) -> TTTDictionaryEntry? { + let dictionary = TTTDictionary.init(named: dictionary) + let entries = dictionary.entries(forSearchTerm: term) as? [TTTDictionaryEntry] + return entries?.first + } + + func getDictionaries() -> [String] { + return TTTDictionary.availableDictionaries().map { item in + return item.name + } + } + + private func _handleHTML(_ html: String) -> String { + let head = extractFromHTML(html: html, tag: "head") + let body = extractFromHTML(html: html, tag: "body") + let trimmedBody = replaceLinks(html: body) + let result = """ + + +\(head)\(trimmedBody) +""" + return result + } + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + if (!dictionary.isEmpty) { + let result = _lookUpByDictionary(term: source, dictionary: dictionary) + if (result != nil) { + cb(_handleHTML(result!.htmlWithPopoverCSS), nil, nil) + return + } + } + let dictionaryNames = getDictionaries() + for name in dictionaryNames { + if name == dictionary { + continue + } + let result = _lookUpByDictionary(term: source, dictionary: DCSOxfordDictionaryOfEnglish) + if (result != nil) { + cb(_handleHTML(result!.htmlWithPopoverCSS), nil, nil) + break + } + } + cb("No definition found for \(source)", nil, nil) + } +} + + + diff --git a/liltr/Utils/Provider/Baidu.swift b/liltr/Utils/Provider/Baidu.swift new file mode 100644 index 0000000..f501f20 --- /dev/null +++ b/liltr/Utils/Provider/Baidu.swift @@ -0,0 +1,85 @@ +import Alamofire +import Foundation + +struct BaiduResult: Decodable { + let src: String? + let dst: String? +} + +struct BaiduResponse: BaseResponse { + let error_code: String? + let error_msg: String? + let trans_result: [BaiduResult]? + let from: String? + let to: String? + + var target: String? { + if (trans_result?.isEmpty == false) { + var result: [String] = [] + for item in trans_result! { + result.append(item.dst!) + } + return result.joined(separator: "\n") + } + return nil + } + + var errorMessage: String? { + if (error_msg != nil) { + return "\(error_code!): \(error_msg!)" + } + return nil + } +} + +let BaiduProviderName = "Baidu" + +class BaiduProvider: BaseProvider { + static let shared = BaiduProvider() + let name = BaiduProviderName + let delay: DispatchTimeInterval = .seconds(1) + let apiUrl = "https://fanyi-api.baidu.com/api/trans/vip/translate" + + var ak = Defaults.shared.BaiduAK.isEmpty ? Bundle.main.infoDictionary!["BaiduAK"] as! String : Defaults.shared.BaiduAK + var sk = Defaults.shared.BaiduSK.isEmpty ? Bundle.main.infoDictionary!["BaiduSK"] as! String : Defaults.shared.BaiduSK + + @Published var isTranslating = false + private let matchManager = MatchManager() + + private func _sign(q: String, salt: String) -> String { + let str1 = "\(ak)\(q)\(salt)\(sk)" + let str2 = CryptoEncoder.md5(string: str1).lowercased() + return str2 + } + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + let salt = String(Int(round(Date().timeIntervalSince1970))) + let sign = _sign(q: source, salt: salt) + let parameters: [String: String] = [ + "q": source, + "from": from.shortCode, + "to": to.shortCode, + "appid": ak, + "salt": salt, + "sign": sign + ] + + debugPrint("[BaiduProvider] parameters:", parameters) + + AF.request(apiUrl, method: .post, parameters: parameters, encoding: URLEncoding.default) + .cacheResponse(using: .cache) + .responseDecodable(of: BaiduResponse.self) { response in + if (response.error != nil) { + cb(response.error!.errorDescription!, nil, nil) + } else if (response.value?.errorMessage != nil) { + cb(response.value!.errorMessage!, nil, nil) + } else { + cb(response.value!.target!, from, to) + } + } + } + +} + + + diff --git a/liltr/Utils/Provider/BigHugeThesaurus.swift b/liltr/Utils/Provider/BigHugeThesaurus.swift new file mode 100644 index 0000000..169edc8 --- /dev/null +++ b/liltr/Utils/Provider/BigHugeThesaurus.swift @@ -0,0 +1,112 @@ +import Alamofire +import Foundation + +struct BigHugeThesaurusResult: Decodable { + let syn: [String]? + let ant: [String]? + let rel: [String]? + let sim: [String]? + let usr: [String]? +} + +struct BigHugeThesaurusResponse: BaseResponse { + let noun: BigHugeThesaurusResult? + let verb: BigHugeThesaurusResult? + let adjective: BigHugeThesaurusResult? + let adverb: BigHugeThesaurusResult? + + func parseResult(result: BigHugeThesaurusResult?) -> String { + if result == nil { + return "" + } + + var lines: [String] = [] + if (!(result?.syn?.isEmpty ?? true)) { + lines.append("• synonyms:") + lines.append("\t" + result!.syn!.joined(separator: " / ")) + } + if (!(result?.ant?.isEmpty ?? true)) { + lines.append("• antonyms:") + lines.append("\t" + result!.ant!.joined(separator: " / ")) + } + if (!(result?.rel?.isEmpty ?? true)) { + lines.append("• related:") + lines.append("\t" + result!.rel!.joined(separator: " / ")) + } + if (!(result?.sim?.isEmpty ?? true)) { + lines.append("• similar:") + lines.append("\t" + result!.sim!.joined(separator: " / ")) + } + if (!(result?.usr?.isEmpty ?? true)) { + lines.append("• suggestions:") + lines.append("\t" + result!.usr!.joined(separator: " / ")) + } + + return lines.joined(separator: "\n") + } + + var target: String? { + var parts: [String] = [] + let nounStr = self.parseResult(result: self.noun); + if (!nounStr.isEmpty) { + parts.append("[noun]") + parts.append(nounStr + "\n") + } + let verbStr = self.parseResult(result: self.verb); + if (!verbStr.isEmpty) { + parts.append("[verb]") + parts.append(verbStr + "\n") + } + let adjStr = self.parseResult(result: self.adjective); + if (!adjStr.isEmpty) { + parts.append("[adj]") + parts.append(adjStr + "\n") + } + let advStr = self.parseResult(result: self.adverb); + if (!advStr.isEmpty) { + parts.append("[adv]") + parts.append(advStr + "\n") + } + + return parts.joined(separator: "\n") + } + + var errorMessage: String? { + return nil + } +} + +let BigHugeThesaurusProviderName = "BigHugeThesaurus" + +class BigHugeThesaurusProvider: BaseProvider { + static let shared = BigHugeThesaurusProvider() + + let name = BigHugeThesaurusProviderName + let delay: DispatchTimeInterval = .microseconds(250) + let apiUrl = "https://words.bighugelabs.com/api/2" + + var ak = Defaults.shared.BigHugeThesaurusAK.isEmpty ? "" : Defaults.shared.BigHugeThesaurusAK + var sk = Defaults.shared.BigHugeThesaurusSK.isEmpty ? Bundle.main.infoDictionary!["BigHugeThesaurusSK"] as! String : Defaults.shared.BigHugeThesaurusSK + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + let word = String(source.firstWord ?? "") + if (word.isEmpty) { + return + } + + AF.request("\(apiUrl)/\(sk)/\(word)/json", method: .get) + .cacheResponse(using: .cache) + .responseDecodable(of: BigHugeThesaurusResponse.self) { response in + if (response.error != nil) { + cb(response.error!.errorDescription!, nil, nil) + } else if (response.value?.errorMessage != nil) { + cb(response.value!.errorMessage!, nil, nil) + } else { + cb(response.value!.target!, from, to) + } + } + } +} + + + diff --git a/liltr/Utils/Provider/NiuTrans.swift b/liltr/Utils/Provider/NiuTrans.swift new file mode 100644 index 0000000..316b8c8 --- /dev/null +++ b/liltr/Utils/Provider/NiuTrans.swift @@ -0,0 +1,61 @@ +import Alamofire +import Foundation + +struct NiuTransResponse: BaseResponse { + let error_code: String? + let error_msg: String? + let tgt_text: String? + let from: String + let to: String + let src_text: String? + + var target: String? { + return tgt_text + } + + var errorMessage: String? { + if (error_msg != nil && !error_msg!.isEmpty) { + return "\(error_code!): \(error_msg!)" + } + return nil + } +} + +let NiuTransProviderName = "NiuTrans" + +class NiuTransProvider: BaseProvider { + static let shared = NiuTransProvider() + + let name = NiuTransProviderName + let delay: DispatchTimeInterval = .microseconds(250) + let apiUrl = "https://api.niutrans.com/NiuTransServer/translation" + + var ak = Defaults.shared.NiuTransAK.isEmpty ? "" : Defaults.shared.NiuTransAK + var sk = Defaults.shared.NiuTransSK.isEmpty ? Bundle.main.infoDictionary!["NiuTransSK"] as! String : Defaults.shared.NiuTransSK + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + let parameters: [String: String] = [ + "apikey": sk, + "src_text": source, + "from": from.shortCode, + "to": to.shortCode, + ] + + debugPrint("[NiuTransProvider] parameters:", parameters) + + AF.request(apiUrl, method: .post, parameters: parameters, encoding: JSONEncoding.default) + .cacheResponse(using: .cache) + .responseDecodable(of: NiuTransResponse.self) { response in + if (response.error != nil) { + cb(response.error!.errorDescription!, nil, nil) + } else if (response.value?.errorMessage != nil) { + cb(response.value!.errorMessage!, nil, nil) + } else { + cb(response.value!.target!, from, to) + } + } + } +} + + + diff --git a/liltr/Utils/Provider/ProviderManager.swift b/liltr/Utils/Provider/ProviderManager.swift new file mode 100644 index 0000000..680ba3e --- /dev/null +++ b/liltr/Utils/Provider/ProviderManager.swift @@ -0,0 +1,104 @@ +import Foundation + +protocol BaseProvider: ObservableObject { + var delay: DispatchTimeInterval { get } + var name: String { get } + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void +} + +protocol BaseResponse: Decodable { + var target: String? { get } + var errorMessage: String? { get } +} + +let PROVIDER_ARRAY: [any BaseProvider] = [NiuTransProvider.shared, BaiduProvider.shared, VolcengineProvider.shared, AliProvider.shared, AppleDictionaryProvider.shared, BigHugeThesaurusProvider.shared] +let PROVIDER_DICT: [String: any BaseProvider] = Dictionary(uniqueKeysWithValues: PROVIDER_ARRAY.map{ ($0.name, $0) }) + + +struct ProviderCallbackData { + let target: String + let source: String + let sourceLanguage: Language? + let targetLanguage: Language? + let providerName: String + + var isDictionary: Bool { + return providerName == AppleDictionaryProviderName + } + + init(_ result: String, _ source: String, sourceLanguage: Language? = nil, targetLanguage: Language? = nil, providerName: String) { + self.target = result + self.source = source + self.sourceLanguage = sourceLanguage + self.targetLanguage = targetLanguage + self.providerName = providerName + } +} + +class ProviderManager: ObservableObject { + static let shared = ProviderManager() + private var resultCache: [String: ProviderCallbackData] = [:] + private var curQuery: String = "" + + @Published var name = PROVIDER_DICT[Defaults.shared.primaryProvider]!.name + @Published var usePrimary = true + @Published var isTranslating = false + + init() {} + + var provider: any BaseProvider { + return usePrimary ? PROVIDER_DICT[Defaults.shared.primaryProvider]! : PROVIDER_DICT[Defaults.shared.secondaryProvider]! + } + + func switchProvider() { + usePrimary = !usePrimary + name = provider.name + } + + func translate(_ source: String, _ targetLanguage: Language?, _ cb: @escaping (_ data: ProviderCallbackData) -> Void) { + var cur = targetLanguage == nil ? AppleDictionaryProvider.shared : provider + let transformedSource = Defaults.shared.preProcessSource ? TextHandler.handle(source) : source + if (transformedSource.isEmpty) { + return + } +// if (regexMatched(transformedSource, "^\\b\\w+\\b$")) { +// cur = AppleDictionaryProvider.shared +// } + let query = "\(CryptoEncoder.base64(string: source))_\(targetLanguage?.code ?? "nil")_\(cur.name)_\(cur.name == AppleDictionaryProviderName ? Defaults.shared.dictionary : "nil")" + + func _callback(_ target: String, _ _sourceLanguage: Language? = nil, _ _targetLanguage: Language? = nil) { + if (query == curQuery) { + let data = ProviderCallbackData(target, source, sourceLanguage: _sourceLanguage, targetLanguage: _targetLanguage?.name == _sourceLanguage?.name ? nil : _targetLanguage, providerName: cur.name) + cb(data) + resultCache[query] = data + isTranslating = false + curQuery = "" + } + } + + if (resultCache[query] != nil) { + cb(resultCache[query]!) + return + } + + isTranslating = true + curQuery = query + + let fromTo = LanguageManager.getFromTo(transformedSource, targetLanguage) + if (fromTo == nil) { + _callback("Source text can not be recognized") + return + } + let (from, to) = fromTo! + + debugPrint("[ProviderManager#translate]", [ + "name": cur.name, + "from": from.code, + "to": to.code, + "source": transformedSource + ]) + + return cur.translate(source: transformedSource, from: from, to: to, cb: _callback) + } +} diff --git a/liltr/Utils/Provider/Volcengine.swift b/liltr/Utils/Provider/Volcengine.swift new file mode 100644 index 0000000..f83f52f --- /dev/null +++ b/liltr/Utils/Provider/Volcengine.swift @@ -0,0 +1,173 @@ +import Alamofire +import CryptoKit +import Foundation + +struct VolcengineError: Decodable { + let Code: String? + let Message: String? +} + +struct VolcengineResponseMetadata: Decodable { + let RequestId: String? + let Action: String? + let Version: String? + let Service: String? + let Region: String? + let Error: VolcengineError? +} + +struct VolcengineTranslation: Decodable { + let Translation: String + let DetectedSourceLanguage: String +} + +struct VolcengineResponse: BaseResponse { + let ResponseMetadata: VolcengineResponseMetadata + let TranslationList: [VolcengineTranslation]? + + var target: String? { + if (TranslationList?.isEmpty == false) { + var result: [String] = [] + for item in TranslationList! { + result.append(item.Translation) + } + return result.joined(separator: "\n") + } + + return nil + } + + var errorMessage: String? { + if (ResponseMetadata.Error?.Message != nil) { + return "\(ResponseMetadata.Error!.Code!): \(ResponseMetadata.Error!.Message!)" + } + return nil + } +} + +let VolcengineProviderName = "Volcengine" + +class VolcengineProvider: BaseProvider { + static let shared = VolcengineProvider() + + let name = VolcengineProviderName + let delay: DispatchTimeInterval = .microseconds(300) + var apiUrl: String { + return "https://\(self.host)\(self.uri)?\(self.queryString)" + } + + var ak: String { + return Defaults.shared.VolcengineAK.isEmpty ? Bundle.main.infoDictionary!["VolcengineAK"] as! String : Defaults.shared.VolcengineAK + } + + var sk: String { + return Defaults.shared.VolcengineSK.isEmpty ? Bundle.main.infoDictionary!["VolcengineSK"] as! String : Defaults.shared.VolcengineSK + } + + private let host = "translate.volcengineapi.com" + private let uri = "/" + private let queryString = "Action=TranslateText&Version=2020-06-01" + private let region = "cn-north-1" + private let service = "translate" + + private func _getSignedHeaders(headers: [String: String]) -> String { + var result: [String] = [] + for (key, _) in headers { + result.append(key.lowercased()) + } + result = result.sorted { $0 < $1 } + return result.joined(separator: ";") + } + + private func _getCanonicalHeaders(headers: [String: String]) -> String { + var result: [String] = [] + for (key, value) in headers { + result.append("\(key.lowercased()):\(value.trimmingCharacters(in: .whitespaces))") + } + result = result.sorted { $0 < $1 } + return result.joined(separator: "\n") + "\n" + } + + private func _getXDate() -> String { + let date = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + let xDate = dateFormatter.string(from: date) + return xDate + } + + private func _sign(contentHashed: String, headers: [String: String]) -> String { + // step 1 + let method = "POST" + let canonicalHeaders = _getCanonicalHeaders(headers: headers) + let signedHeaders = _getSignedHeaders(headers: headers) + let canoicalRequest = [method, uri, queryString, canonicalHeaders, signedHeaders, contentHashed].joined(separator: "\n") + + // step 2 + let algorithm = "HMAC-SHA256" + let xDate = headers["X-Date"]! + let shortDate = String(xDate.prefix(8)) + let credentialScope = [shortDate, region, service, "request"].joined(separator: "/") + let canonicalRequestHashed = _hashSha256(content: canoicalRequest) + let stringToSign = [algorithm, xDate, credentialScope, canonicalRequestHashed].joined(separator: "\n") + + // step 3 + let kDate = _hmacSha256(sk.data(using: .utf8)!, shortDate) + let kRegion = _hmacSha256(kDate, region) + let kService = _hmacSha256(kRegion, service) + let kSigning = _hmacSha256(kService, "request") + let signature = CryptoEncoder.data2str(_hmacSha256(kSigning, stringToSign)) + let authorization = "HMAC-SHA256 Credential=\(ak)/\(credentialScope), SignedHeaders=\(signedHeaders), Signature=\(signature)" + + return authorization + } + + func _hmacSha256(_ key: Data, _ content: String) -> Data { + let hmac = HMAC.authenticationCode(for: content.data(using: .utf8)!, using: SymmetricKey(data: key)) + return Data(hmac) + } + + func _hashSha256(content: String) -> String { + let digest = SHA256.hash(data: content.data(using: .utf8)!) + return digest.compactMap { String(format: "%02x", $0) }.joined() + } + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + let parameters: [String: Any] = [ + "SourceLanguage": from.shortCode, + "TargetLanguage": to.shortCode, + "TextList": source.split(separator: "\n"), + ] + + let date = _getXDate() + let contentHashed = _hashSha256(content: String(data: try! JSONSerialization.data(withJSONObject: parameters, options: []), encoding: .utf8)!) + + var headers = [ + "Content-Type": "application/json", + "Host": host, + "X-Date": date, + ] + + let authorization = _sign(contentHashed: contentHashed, headers: headers) + headers.updateValue(authorization, forKey: "Authorization") + + debugPrint("[VolcengineProvider] parameters:", parameters, headers) + + AF.request(apiUrl, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: dict2headers(dict: headers)) + .cacheResponse(using: .cache) + .responseDecodable(of: VolcengineResponse.self) { response in + if (response.error != nil) { + cb(response.error!.errorDescription!, nil, nil) + } else if (response.value?.errorMessage != nil) { + cb(response.value!.errorMessage!, nil, nil) + } else { + cb(response.value!.target!, from, to) + } + } + } + +} + + + diff --git a/liltr/Utils/SchemeURL/SchemeURLManager.swift b/liltr/Utils/SchemeURL/SchemeURLManager.swift new file mode 100644 index 0000000..a969887 --- /dev/null +++ b/liltr/Utils/SchemeURL/SchemeURLManager.swift @@ -0,0 +1,22 @@ +import Foundation + +enum SchemeAction: String { + case translateInWindow = "translate-in-window" + case settings = "settings" + case update = "update" +} + +class SchemeURLManager { + static func getUrlByAction(_ action: SchemeAction, querys: Dictionary = [:]) -> URL { + var components = URLComponents() + components.scheme = APP_NAME + components.host = action.rawValue + components.queryItems = querys.map { + URLQueryItem(name: $0, + value: $1.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + ) + } + + return components.url! + } +} diff --git a/liltr/Utils/TTTDictionary/TTTDictionary.cpp b/liltr/Utils/TTTDictionary/TTTDictionary.cpp new file mode 100644 index 0000000..a940f6b --- /dev/null +++ b/liltr/Utils/TTTDictionary/TTTDictionary.cpp @@ -0,0 +1,8 @@ +// +// TTTDictionary.cpp +// liltr +// +// Created by Rya on 2024/1/15. +// + +#include diff --git a/liltr/Utils/TTTDictionary/TTTDictionary.h b/liltr/Utils/TTTDictionary/TTTDictionary.h new file mode 100644 index 0000000..8310a58 --- /dev/null +++ b/liltr/Utils/TTTDictionary/TTTDictionary.h @@ -0,0 +1,157 @@ +// TTTDictionary.h +// +// Copyright (c) 2014 Mattt Thompson +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + TTTDictionaryEntry + */ +@interface TTTDictionaryEntry : NSObject + +/// 词头 +@property (readonly, nonatomic, copy) NSString *headword; + +/// innerText of HTML +@property (readonly, nonatomic, copy) NSString *text; + +@property (readonly, nonatomic, copy) NSString *HTML; +@property (readonly, nonatomic, copy) NSString *HTMLWithAppCSS; +@property (readonly, nonatomic, copy) NSString *HTMLWithPopoverCSS; + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - + +NS_ASSUME_NONNULL_BEGIN + +/// Search word type +typedef NS_ENUM(NSUInteger, TTTDictionarySearchType) { + TTTDictionarySearchTypeExactMatch = 0, // exact match + TTTDictionarySearchTypePrefixMatch = 1, // forward match (prefix match) + TTTDictionarySearchTypeLeadingMatch = 2, // partial query match (matching (leading) part of query; including ignoring diacritics, four tones in Chinese, etc) +}; + + +/** + TTTDictionary + */ +@interface TTTDictionary : NSObject + +/** + CFBundleDisplayName in dict's info.plist + */ +@property (readonly, nonatomic, copy) NSString *name; + +/** + CFBundleName in dict's info.plist + */ +@property (readonly, nonatomic, copy) NSString *shortName; + +@property (readonly, nonatomic, assign) BOOL isUserDictionary; + +@property (readonly, nonatomic, copy, nullable) NSString *identifier; +@property (readonly, nonatomic, strong) NSURL *dictionaryURL; + +/// key: EZLanguage, value: language dict name +//@property (class, readonly, nonatomic, copy) MMOrderedDictionary *languageToDictionaryNameMap; + +/// Get dict with CFBundleDisplayName ++ (instancetype)dictionaryNamed:(NSString *)name; + ++ (NSSet *)availableDictionaries; + ++ (NSArray *)activeDictionaries; + +/// Dictionary directory URL, path is ~/Library/Dictionaries/ ++ (NSURL *)userDictionaryDirectoryURL; + +// Default searchType is exact match, 0 +- (NSArray *)entriesForSearchTerm:(NSString *)term; + +- (NSArray *)entriesForSearchTerm:(NSString *)term searchType:(TTTDictionarySearchType)searchType; + +@end + +/// @name Constants + +// Simplified Chinese +extern NSString * const DCSSimplifiedChineseDictionaryName; +extern NSString * const DCSSimplifiedChineseIdiomDictionaryName; +extern NSString * const DCSSimplifiedChineseThesaurusDictionaryName; +extern NSString * const DCSSimplifiedChinese_EnglishDictionaryName; +extern NSString * const DCSSimplifiedChinese_JapaneseDictionaryName; + +// Traditional Chinese +extern NSString * const DCSTraditionalChineseDictionaryName; +extern NSString * const DCSTraditionalChineseHongkongDictionaryName; +extern NSString * const DCSTraditionalChinese_EnglishDictionaryName; +extern NSString * const DCSTraditionalChinese_EnglishIdiomDictionaryName; + +// English +extern NSString * const DCSNewOxfordAmericanDictionaryName; +extern NSString * const DCSOxfordAmericanWritersThesaurus; +extern NSString * const DCSOxfordDictionaryOfEnglish; +extern NSString * const DCSOxfordThesaurusOfEnglish; + +// Japanese +extern NSString * const DCSJapaneseDictionaryName; +extern NSString * const DCSJapanese_EnglishDictionaryName; + +// French +extern NSString * const DCSFrenchDictionaryName; +extern NSString * const DCSFrench_EnglishDictionaryName; + +// German +extern NSString * const DCSGermanDictionaryName; +extern NSString * const DCSGerman_EnglishDictionaryName; + +// Italian +extern NSString * const DCSItalianDictionaryName; +extern NSString * const DCSItalian_EnglishDictionaryName; + +// Spanish +extern NSString * const DCSSpanishDictionaryName; +extern NSString * const DCSSpanish_EnglishDictionaryName; + +// Portuguese +extern NSString * const DCSPortugueseDictionaryName; +extern NSString * const DCSPortuguese_EnglishDictionaryName; + +// Dutch +extern NSString * const DCSDutchDictionaryName; +extern NSString * const DCSDutch_EnglishDictionaryName; + +// Korean +extern NSString * const DCSKoreanDictionaryName; +extern NSString * const DCSKorean_EnglishDictionaryName; + + +extern NSString * const DCSWikipediaDictionaryName; +extern NSString * const DCSAppleDictionaryName; + +NS_ASSUME_NONNULL_END diff --git a/liltr/Utils/TTTDictionary/TTTDictionary.m b/liltr/Utils/TTTDictionary/TTTDictionary.m new file mode 100644 index 0000000..ef4a6d7 --- /dev/null +++ b/liltr/Utils/TTTDictionary/TTTDictionary.m @@ -0,0 +1,387 @@ +// TTTDictionary.m +// +// Copyright (c) 2014 Mattt Thompson +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "TTTDictionary.h" + +#import + +// User dictionary directory. +NSString *const DCSUserDictionaryDirectoryPath = @"~/Library/Dictionaries/"; + +// Just a soft link to /System/Library/Assets/com_apple_MobileAsset_DictionaryServices_dictionaryOSX/AssetData +NSString *const DCSUserContainerDictionaryDirectoryPath = @"~/Library/Containers/com.apple.Dictionary/Data/Library/Dictionaries/"; + +/// System dictionary directory. +NSString *const DCSSystemDictionayDirectoryPath = @"/System/Library/"; +// /System/Library/AssetsV2/com_apple_MobileAsset_DictionaryServices_dictionaryOSX/a6f0aae08e94e25f6f35a0bd6d06cbf525157b2e.asset/AssetData/Apple%20Dictionary.dictionary + + +// Simplified Chinese +NSString *const DCSSimplifiedChineseDictionaryName = @"现代汉语规范词典"; // 简体中文 +NSString *const DCSSimplifiedChineseIdiomDictionaryName = @"汉语成语词典"; // 简体中文成语 +NSString *const DCSSimplifiedChineseThesaurusDictionaryName = @"现代汉语同义词典"; // 简体中文同义词词典 +NSString *const DCSSimplifiedChinese_EnglishDictionaryName = @"牛津英汉汉英词典"; // 简体中文-英文 +NSString *const DCSSimplifiedChinese_JapaneseDictionaryName = @"超級クラウン中日辞典 / クラウン日中辞典"; // 简体中文-日文 + +// Traditional Chinese +NSString *const DCSTraditionalChineseDictionaryName = @"五南國語活用辭典"; // 繁体中文 +NSString *const DCSTraditionalChineseHongkongDictionaryName = @"商務新詞典(全新版)"; // 繁体中文(香港) +NSString *const DCSTraditionalChinese_EnglishDictionaryName = @"譯典通英漢雙向字典"; // 繁体中文-英文 +NSString *const DCSTraditionalChinese_EnglishIdiomDictionaryName = @"漢英對照成語詞典"; // 繁体中文-英文习语 + +// English +NSString *const DCSNewOxfordAmericanDictionaryName = @"New Oxford American Dictionary"; // 美式英文 +NSString *const DCSOxfordAmericanWritersThesaurus = @"Oxford American Writer’s Thesaurus"; // 美式英文同义词词典 +NSString *const DCSOxfordDictionaryOfEnglish = @"Oxford Dictionary of English"; // 英式英文 +NSString *const DCSOxfordThesaurusOfEnglish = @"Oxford Thesaurus of English"; // 英式英文同义词词典 + +// Japanese +NSString *const DCSJapaneseDictionaryName = @"スーパー大辞林"; // 日文 +NSString *const DCSJapanese_EnglishDictionaryName = @"ウィズダム英和辞典 / ウィズダム和英辞典"; // 日文-英文 + +// French +NSString *const DCSFrenchDictionaryName = @"Multidictionnaire de la langue française"; // 法文 +NSString *const DCSFrench_EnglishDictionaryName = @"Oxford-Hachette French Dictionary"; // 法文-英文 +NSString *const DCSFrench_GermanDictionaryName = @"ONS Großwörterbuch Französisch Deutsch"; // 法文-德文 + +// German +NSString *const DCSGermanDictionaryName = @"Duden-Wissensnetz deutsche Sprache"; // 德文 +NSString *const DCSGerman_EnglishDictionaryName = @"Oxford German Dictionary"; // 德文-英文 + +// Italian +NSString *const DCSItalianDictionaryName = @"Dizionario italiano da un affiliato di Oxford University Press"; // 意大利文 +NSString *const DCSItalian_EnglishDictionaryName = @"Oxford Paravia Il Dizionario inglese - italiano/italiano - inglese"; // 意大利文-英文 + +// Spanish +NSString *const DCSSpanishDictionaryName = @"Diccionario General de la Lengua Española Vox"; // 西班牙文 +NSString *const DCSSpanish_EnglishDictionaryName = @"Gran Diccionario Oxford - Español-Inglés • Inglés-Español"; // 西班牙文-英文 + +// Portugues +NSString *const DCSPortugueseDictionaryName = @"Dicionário de Português licenciado para Oxford University Press"; // 葡萄牙文 +NSString *const DCSPortuguese_EnglishDictionaryName = @"Oxford Portuguese Dictionary - Português-Inglês • Inglês-Português"; // 葡萄牙文-英文 + +// Dutch +NSString *const DCSDutchDictionaryName = @"Prisma woordenboek Nederlands"; // 荷兰文 +NSString *const DCSDutch_EnglishDictionaryName = @"Prisma Handwoordenboek Engels"; // 荷兰文-英文 + +// Korean +NSString *const DCSKoreanDictionaryName = @"New Ace Korean Language Dictionary"; // 韩文 +NSString *const DCSKorean_EnglishDictionaryName = @"뉴에이스 영한사전 / 뉴에이스 한영사전"; // 韩文-英文 + +NSString *const DCSWikipediaDictionaryName = @"维基百科"; +NSString *const DCSAppleDictionaryName = @"Apple 词典"; + +typedef NS_ENUM(NSInteger, TTTDictionaryRecordVersion) { + TTTDictionaryVersionHTML = 0, + TTTDictionaryVersionHTMLWithAppCSS = 1, + TTTDictionaryVersionHTMLWithPopoverCSS = 2, + TTTDictionaryVersionText = 3, +}; + +#pragma mark - + +extern CFArrayRef DCSCopyAvailableDictionaries(void); +extern CFStringRef DCSDictionaryGetName(DCSDictionaryRef dictionary); +extern CFStringRef DCSDictionaryGetShortName(DCSDictionaryRef dictionary); +extern DCSDictionaryRef DCSDictionaryCreate(CFURLRef url); +// extern CFArrayRef DCSCopyRecordsForSearchString(DCSDictionaryRef dictionary, CFStringRef string, void *, void *); + +extern CFDictionaryRef DCSCopyDefinitionMarkup(DCSDictionaryRef dictionary, CFStringRef record); +extern CFStringRef DCSRecordCopyData(CFTypeRef record, long version); +extern CFStringRef DCSRecordCopyDataURL(CFTypeRef record); +extern CFStringRef DCSRecordGetAnchor(CFTypeRef record); +extern CFStringRef DCSRecordGetAssociatedObj(CFTypeRef record); +extern CFStringRef DCSRecordGetHeadword(CFTypeRef record); +extern CFStringRef DCSRecordGetRawHeadword(CFTypeRef record); +extern CFStringRef DCSRecordGetString(CFTypeRef record); +extern DCSDictionaryRef DCSRecordGetSubDictionary(CFTypeRef record); +extern CFStringRef DCSRecordGetTitle(CFTypeRef record); + + +// Ref: https://discussions.apple.com/thread/6616776?answerId=26923349022#26923349022 and https://github.com/lipidity/CLIMac/blob/master/src/dictctl.c#L12 +extern CFArrayRef DCSGetActiveDictionaries(void); +// extern CFSetRef DCSCopyAvailableDictionaries(void); +extern DCSDictionaryRef DCSGetDefaultDictionary(void); +extern DCSDictionaryRef DCSGetDefaultThesaurus(void); +extern DCSDictionaryRef DCSDictionaryCreate(CFURLRef); +extern CFURLRef DCSDictionaryGetURL(DCSDictionaryRef); +// extern CFStringRef DCSDictionaryGetName(DCSDictionaryRef); +extern CFStringRef DCSDictionaryGetIdentifier(DCSDictionaryRef); + +/** + # extern CFArrayRef DCSCopyRecordsForSearchString (DCSDictionaryRef, CFStringRef, unsigned long long, long long) + # unsigned long long method + # 0 = exact match + # 1 = forward match (prefix match) + # 2 = partial query match (matching (leading) part of query; including ignoring diacritics, four tones in Chinese, etc) + # >=3 = ? (exact match?) + # + # long long max_record_count + */ + +extern CFArrayRef DCSCopyRecordsForSearchString(DCSDictionaryRef, CFStringRef, unsigned long long, long long); + + +#pragma mark - + +@interface TTTDictionaryEntry () +@property (readwrite, nonatomic, copy) NSString *headword; +@property (readwrite, nonatomic, copy) NSString *text; +@property (readwrite, nonatomic, copy) NSString *HTML; +@property (readwrite, nonatomic, copy) NSString *HTMLWithAppCSS; +@property (readwrite, nonatomic, copy) NSString *HTMLWithPopoverCSS; + +@end + +@implementation TTTDictionaryEntry + +- (instancetype)initWithRecordRef:(CFTypeRef)record + dictionaryRef:(DCSDictionaryRef)dictionary { + self = [self init]; + if (!self && record) { + return nil; + } + + // ???: __bridge_transfer will cause crash, but why? + self.headword = (__bridge NSString *)DCSRecordGetHeadword(record); + if (self.headword) { + self.text = (__bridge_transfer NSString *)DCSRecordCopyData(record, TTTDictionaryVersionText); + } + + self.HTML = (__bridge_transfer NSString *)DCSRecordCopyData(record, (long)TTTDictionaryVersionHTML); + self.HTMLWithAppCSS = (__bridge_transfer NSString *)DCSRecordCopyData(record, (long)TTTDictionaryVersionHTMLWithAppCSS); + self.HTMLWithPopoverCSS = (__bridge_transfer NSString *)DCSRecordCopyData(record, (long)TTTDictionaryVersionHTMLWithPopoverCSS); + + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, headword: %@>", NSStringFromClass([self class]), self, self.headword]; +} + +@end + +@interface TTTDictionary () + +@property (readwrite, nonatomic, assign) DCSDictionaryRef dictionary; +@property (readwrite, nonatomic, copy) NSString *name; +@property (readwrite, nonatomic, copy) NSString *shortName; + +/// CFBundleIdentifier +@property (readwrite, nonatomic, copy, nullable) NSString *identifier; +@property (readwrite, nonatomic, strong) NSURL *dictionaryURL; + +@end + +@implementation TTTDictionary + ++ (instancetype)dictionaryNamed:(NSString *)name { + static NSDictionary *_availableDictionariesKeyedByName = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *mutableAvailableDictionariesKeyedByName = [NSMutableDictionary dictionaryWithCapacity:[[self availableDictionaries] count]]; + for (TTTDictionary *dictionary in [self availableDictionaries]) { + mutableAvailableDictionariesKeyedByName[dictionary.name] = dictionary; + } + + _availableDictionariesKeyedByName = [NSDictionary dictionaryWithDictionary:mutableAvailableDictionariesKeyedByName]; + }); + + return _availableDictionariesKeyedByName[name]; +} + ++ (NSSet *)availableDictionaries { + // Cost < 0.1s + static NSSet *_availableDictionaries = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableSet *mutableDictionaries = [NSMutableSet set]; + for (id dictionary in (__bridge_transfer NSArray *)DCSCopyAvailableDictionaries()) { + [mutableDictionaries addObject:[[TTTDictionary alloc] initWithDictionaryRef:(__bridge DCSDictionaryRef)dictionary]]; + } + _availableDictionaries = [NSSet setWithSet:mutableDictionaries]; + }); + return _availableDictionaries; +} + +/// Active dictionaries are dictionaries that are currently enabled in Dictionary.app ++ (NSArray *)activeDictionaries { + // !!!: DCSGetActiveDictionaries() can only invoke once, otherwise it will crash. So we must use static variable to cache the result. + + static NSArray *_activeDictionaries = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableArray *mutableActiveDictionaries = [NSMutableArray array]; + NSArray *activeDictionaries = (__bridge_transfer NSArray *)DCSGetActiveDictionaries(); + for (id dictionary in activeDictionaries) { + [mutableActiveDictionaries addObject:[[TTTDictionary alloc] initWithDictionaryRef:(__bridge DCSDictionaryRef)dictionary]]; + } + _activeDictionaries = [NSArray arrayWithArray:mutableActiveDictionaries]; + }); + + return _activeDictionaries; +} + ++ (NSURL *)userDictionaryDirectoryURL { + NSString *userDictPath = [DCSUserDictionaryDirectoryPath stringByExpandingTildeInPath]; + NSURL *dictionaryDirectoryURL = [NSURL fileURLWithPath:userDictPath isDirectory:YES]; + return dictionaryDirectoryURL; +} + +///// key: EZLanguage, value: language dict name +//+ (MMOrderedDictionary *)languageToDictionaryNameMap { +// static MMOrderedDictionary *_languageToDictionaryNameMap = nil; +// static dispatch_once_t onceToken; +// dispatch_once(&onceToken, ^{ +// _languageToDictionaryNameMap = [[MMOrderedDictionary alloc] initWithKeysAndObjects: +// EZLanguageSimplifiedChinese, DCSSimplifiedChineseDictionaryName, +// EZLanguageTraditionalChinese, DCSTraditionalChineseDictionaryName, +// EZLanguageEnglish, DCSNewOxfordAmericanDictionaryName, +// EZLanguageJapanese, DCSJapaneseDictionaryName, +// EZLanguageKorean, DCSKoreanDictionaryName, +// EZLanguageFrench, DCSFrenchDictionaryName, +// EZLanguageGerman, DCSGermanDictionaryName, +// EZLanguageItalian, DCSItalianDictionaryName, +// EZLanguageSpanish, DCSSpanishDictionaryName, +// EZLanguagePortuguese, DCSPortugueseDictionaryName, +// EZLanguageDutch, DCSDutchDictionaryName, +// nil]; +// }); +// return _languageToDictionaryNameMap; +//} + ++ (NSArray *)supportedSystemDictionaryNames { + static NSArray *_allBuildInDictNames = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _allBuildInDictNames = @[ + DCSSimplifiedChineseDictionaryName, + DCSSimplifiedChineseIdiomDictionaryName, + DCSSimplifiedChineseThesaurusDictionaryName, + DCSSimplifiedChinese_EnglishDictionaryName, + DCSSimplifiedChinese_JapaneseDictionaryName, + DCSTraditionalChineseDictionaryName, + DCSTraditionalChineseHongkongDictionaryName, + DCSTraditionalChinese_EnglishDictionaryName, + DCSTraditionalChinese_EnglishIdiomDictionaryName, + DCSNewOxfordAmericanDictionaryName, + DCSOxfordAmericanWritersThesaurus, + DCSOxfordDictionaryOfEnglish, + DCSOxfordThesaurusOfEnglish, + DCSJapaneseDictionaryName, + DCSJapanese_EnglishDictionaryName, + DCSFrenchDictionaryName, + DCSFrench_EnglishDictionaryName, + DCSGermanDictionaryName, + DCSGerman_EnglishDictionaryName, + DCSItalianDictionaryName, + DCSItalian_EnglishDictionaryName, + DCSSpanishDictionaryName, + DCSSpanish_EnglishDictionaryName, + DCSPortugueseDictionaryName, + DCSPortuguese_EnglishDictionaryName, + DCSDutchDictionaryName, + DCSDutch_EnglishDictionaryName, + DCSKoreanDictionaryName, + DCSKorean_EnglishDictionaryName, + DCSWikipediaDictionaryName, + DCSAppleDictionaryName, + ]; + }); + + return _allBuildInDictNames; +} + +- (instancetype)initWithDictionaryRef:(DCSDictionaryRef)dictionary { + self = [self init]; + if (!self || !dictionary) { + return nil; + } + + self.dictionary = dictionary; + self.name = (__bridge_transfer NSString *)DCSDictionaryGetName(self.dictionary); + self.shortName = (__bridge_transfer NSString *)DCSDictionaryGetShortName(self.dictionary); + + self.identifier = (__bridge_transfer NSString *)(DCSDictionaryGetIdentifier(dictionary)); + self.dictionaryURL = (__bridge_transfer NSURL *)DCSDictionaryGetURL(dictionary); + + return self; +} + +/// User custom dictionary +- (BOOL)isUserDictionary { + BOOL isSystemDictionary = [[self.dictionaryURL URLByDeletingLastPathComponent].path hasPrefix:DCSSystemDictionayDirectoryPath]; + + return !isSystemDictionary; +} + + +- (NSArray *)entriesForSearchTerm:(NSString *)term { + return [self entriesForSearchTerm:term searchType:TTTDictionarySearchTypeExactMatch]; +} + +- (NSArray *)entriesForSearchTerm:(NSString *)term searchType:(TTTDictionarySearchType)searchType { + CFRange termRange = DCSGetTermRangeInString(self.dictionary, (__bridge CFStringRef)term, 0); + if (termRange.location == kCFNotFound) { + return nil; + } + + term = [term substringWithRange:NSMakeRange(termRange.location, termRange.length)]; + + NSArray *records = (__bridge_transfer NSArray *)DCSCopyRecordsForSearchString(self.dictionary, (__bridge CFStringRef)term, searchType, 0); + NSMutableArray *mutableEntries = [NSMutableArray arrayWithCapacity:[records count]]; + if (records) { + for (id record in records) { + TTTDictionaryEntry *entry = [[TTTDictionaryEntry alloc] initWithRecordRef:(__bridge CFTypeRef)record dictionaryRef:self.dictionary]; + if (entry) { + [mutableEntries addObject:entry]; + } + } + } + + return [NSArray arrayWithArray:mutableEntries]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, name: %@, shortName: %@, isUserDictionary: %d>", NSStringFromClass([self class]), self, self.name, self.shortName, self.isUserDictionary]; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[TTTDictionary class]]) { + return NO; + } + + return [self.name isEqualToString:[(TTTDictionary *)object name]]; +} + +- (NSUInteger)hash { + return [self.name hash]; +} + +@end diff --git a/liltr/Utils/TTTDictionary/liltr-Bridging-Header.h b/liltr/Utils/TTTDictionary/liltr-Bridging-Header.h new file mode 100644 index 0000000..f4f7b11 --- /dev/null +++ b/liltr/Utils/TTTDictionary/liltr-Bridging-Header.h @@ -0,0 +1,6 @@ +#ifndef liltr_Bridging_Header_h +#define liltr_Bridging_Header_h + +#import "TTTDictionary.h" + +#endif diff --git a/liltr/Utils/TextHandler/TextHandler.swift b/liltr/Utils/TextHandler/TextHandler.swift new file mode 100644 index 0000000..f15e003 --- /dev/null +++ b/liltr/Utils/TextHandler/TextHandler.swift @@ -0,0 +1,61 @@ +import Foundation + +class TextHandler { + private static func _removeWhiteSpacesFromLine(_ line: String) -> String { + return line.trimmingCharacters(in: .whitespaces) + } + + private static func _removeCommentsFromLine(_ line: String) -> String { + let trimedLine = _removeWhiteSpacesFromLine(line) + if (trimedLine.hasPrefix("/") || trimedLine.hasPrefix("*")) { + return _removeCommentsFromLine(String(trimedLine.dropFirst())) + } else { + return trimedLine + } + } + + private static func _transformCamelCase(_ word: String) -> String { + if (word == word.uppercased()) { + return word + } + + let pattern = "(?<=.)([A-Z])" + let regex = try? NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: word.utf16.count) + let result = regex?.stringByReplacingMatches(in: word, options: [], range: range, withTemplate: " $1") + return result?.lowercased() ?? word + } + + private static func _transformSnakeCase(_ word: String) -> String { + return word.replacingOccurrences(of: "_", with: " ") + } + + private static func _transformWord(_ word: String) -> String { + return _transformSnakeCase(_transformCamelCase(word)) + } + + private static func _transformLine(_ line: String) -> String { + let trimedLine = _removeCommentsFromLine(line) + let words = trimedLine.split(separator: " ").map { word in + return _transformWord(String(word)) + } + return words.joined(separator: " ") + } + + static func handle(_ content: String) -> String { + let originalLines = content.split(separator: "\n").filter { line in + return !line.isEmpty + } + + let lines = originalLines.map { line in + return _transformLine(String(line)) + } + + var separator = " " + if (originalLines.first != nil && lines.first != nil && originalLines.first! == lines.first!) { + separator = "\n" + } + + return lines.filter { !$0.isEmpty }.joined(separator: separator) + } +} diff --git a/liltr/Utils/Window/WindowManager.swift b/liltr/Utils/Window/WindowManager.swift new file mode 100644 index 0000000..0557c90 --- /dev/null +++ b/liltr/Utils/Window/WindowManager.swift @@ -0,0 +1,46 @@ +import Foundation +import SwiftUI + +enum WindowID: String, Identifiable { + case translate + case settings + + var id: String { self.rawValue } +} + +class WindowManager { + static func getById(_ id: WindowID) -> NSWindow? { + let window = NSApp.windows.first(where: { $0.identifier?.rawValue == id.rawValue}) + if (window == nil) { + debugPrint("[WindowManager] get window \(id) failed") + } + return window + } + + static func float(id: WindowID, enable: Bool) { + let window = getById(id) + if (window != nil) { + window!.level = enable ? .floating : .normal + } + } + + static func open(openWindow: OpenWindowAction, id: WindowID) { + NSApplication.shared.activate(ignoringOtherApps: true) + openWindow(id: id.rawValue) + let window = getById(id) + if (window != nil) { + window!.makeKeyAndOrderFront(nil) + window!.orderFrontRegardless() + window!.setIsVisible(true) + NSApplication.shared.activate(ignoringOtherApps: true) + } + } + + static func setSize(id: WindowID, width: CGFloat, height: CGFloat) { + let window = getById(id) + if (window != nil) { + let origin = window!.frame.origin + window!.setFrame(NSRect(x: origin.x, y: origin.y + window!.frame.height - height, width: width, height: height), display: true, animate: false) + } + } +} diff --git a/liltr/Utils/extensions.swift b/liltr/Utils/extensions.swift new file mode 100644 index 0000000..91f074a --- /dev/null +++ b/liltr/Utils/extensions.swift @@ -0,0 +1,115 @@ +import SwiftUI + +public extension Color { +#if os(macOS) + static let backgroundColor = Color(NSColor.windowBackgroundColor) + static let secondaryBackground = Color(NSColor.underPageBackgroundColor) + static let tertiaryBackground = Color(NSColor.controlBackgroundColor) +#else + static let backgroundColor = Color(UIColor.systemBackground) + static let secondaryBackground = Color(UIColor.secondarySystemBackground) + static let tertiaryBackground = Color(UIColor.tertiarySystemBackground) +#endif +} + +extension Color { + var hexString: String { + let components = self.cgColor?.components + let r = components?[0] ?? 0 + let g = components?[1] ?? 0 + let b = components?[2] ?? 0 + let a = self.cgColor?.alpha ?? 1 + + let hexString = String(format: "#%02X%02X%02X%02X", (Int)(r * 255), (Int)(g * 255), (Int)(b * 255), (Int)(a * 255)) + return hexString + } +} + + +extension String: Error {} + +extension StringProtocol { + + var byLines: [SubSequence] { components(separated: .byLines) } + var byWords: [SubSequence] { components(separated: .byWords) } + + func components(separated options: String.EnumerationOptions)-> [SubSequence] { + var components: [SubSequence] = [] + enumerateSubstrings(in: startIndex..., options: options) { _, range, _, _ in components.append(self[range]) } + return components + } + + var firstWord: SubSequence? { + var word: SubSequence? + enumerateSubstrings(in: startIndex..., options: .byWords) { _, range, _, stop in + word = self[range] + stop = true + } + return word + } + var firstLine: SubSequence? { + var line: SubSequence? + enumerateSubstrings(in: startIndex..., options: .byLines) { _, range, _, stop in + line = self[range] + stop = true + } + return line + } +} + +extension View { + func bottomFade(fadeLength:CGFloat = 20) -> some View { + return mask( + VStack(spacing: 0) { + + Rectangle().fill(Color.backgroundColor) + + LinearGradient(gradient: Gradient( + colors: [Color.backgroundColor.opacity(0), Color.backgroundColor]), + startPoint: .bottom, endPoint: .top + ) + .frame(height: fadeLength) + } + ) + } +} + +extension View { + public func cursor(_ cursor: NSCursor) -> some View { + if #available(macOS 13.0, *) { + return self.onContinuousHover { phase in + switch phase { + case .active(_): + cursor.push() + case .ended: + NSCursor.pop() + } + } + } else { + return self.onHover { inside in + if inside { + cursor.push() + } else { + NSCursor.pop() + } + } + } + } +} + +//extension WindowGroup { +// init(_ titleKey: LocalizedStringKey, uniqueWindow: W, @ViewBuilder content: @escaping () -> C) +// where W.ID == String, Content == PresentedWindowContent { +// self.init(titleKey, id: uniqueWindow.id, for: String.self) { _ in +// content() +// } defaultValue: { +// uniqueWindow.id +// } +// } +//} +// +//extension OpenWindowAction { +// func callAsFunction(_ window: W) where W.ID == String { +// self.callAsFunction(id: window.id, value: window.id) +// } +//} diff --git a/liltr/Utils/views.swift b/liltr/Utils/views.swift new file mode 100644 index 0000000..3351a96 --- /dev/null +++ b/liltr/Utils/views.swift @@ -0,0 +1,46 @@ +import SwiftUI + +struct BlurWindow: NSViewRepresentable { + func updateNSView(_ nsView: NSVisualEffectView, context: Context) { + // + } + + func makeNSView(context: Context) -> NSVisualEffectView { + let view = NSVisualEffectView() + view.blendingMode = .behindWindow + + return view + } +} + +class SizeHolder { + private var base: Float + + var fontSize: Float { + return base * 6 + } + + var iconSize: Float { + return fontSize * 1.5 + } + + var radiusSize: Float { + return fontSize / 2 + } + + var innerGapSize: Float { + return base + } + + var gapSize: Float { + return base * 2 + } + + var outerGapSize: Float { + return base * 4 + } + + init(base: Float? = nil) { + self.base = base ?? 2 + } +} diff --git a/liltr/appDelegate.swift b/liltr/appDelegate.swift new file mode 100644 index 0000000..8066711 --- /dev/null +++ b/liltr/appDelegate.swift @@ -0,0 +1,39 @@ +import AppKit +import UserNotifications +import SwiftUI + +class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + UNUserNotificationCenter.current().delegate = self + } +} + + +extension AppDelegate: UNUserNotificationCenterDelegate { + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.banner, .badge, .sound]) + } + + + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + let content = response.notification.request.content + if content.categoryIdentifier == "translate" { + switch response.actionIdentifier { + case "copy": + copyToPasteboard(content.body) + break + case "speak": + let language = LanguageManager.getLanguageByContent(content.body) + SpeechManager.start(content.body, language) + break + case "expand": + NSWorkspace.shared.open(SchemeURLManager.getUrlByAction(SchemeAction.translateInWindow, querys: ["src": content.title])) + break + default: + break + } + } + completionHandler() + } +} diff --git a/liltr/liltr.entitlements b/liltr/liltr.entitlements new file mode 100644 index 0000000..33297e7 --- /dev/null +++ b/liltr/liltr.entitlements @@ -0,0 +1,20 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.device.audio-input + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.temporary-exception.apple-events + + com.apple.TextEdit + + + diff --git a/liltr/liltrApp.swift b/liltr/liltrApp.swift new file mode 100644 index 0000000..295142a --- /dev/null +++ b/liltr/liltrApp.swift @@ -0,0 +1,133 @@ +import SwiftUI +import SwiftData +import KeyboardShortcuts +import UserNotifications +import Sparkle + +let APP_NAME = Bundle.main.infoDictionary!["APP_NAME"] as! String + +@main +struct liltrApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @Default(\.menuIconSymbol) var menuIconSymbol + private let updaterController: SPUStandardUpdaterController + + init() { + updaterController = SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil) + } + + var body: some Scene { + MenuBarExtra() { + AppMenu() + } label: { + let imageDefinedByUser = NSImage(systemSymbolName: menuIconSymbol, accessibilityDescription: APP_NAME) + let imageDefault: NSImage = { + let ratio = $0.size.height / $0.size.width + $0.size.height = 18 + $0.size.width = 18 / ratio + return $0 + }(NSImage(named: "monochrome.fill")!) + Image(nsImage: imageDefinedByUser ?? imageDefault) + } + + Window("Translate", id: WindowID.translate.id) { + TranslateView() + .frame(minWidth: 220, minHeight: 300) + } + .defaultSize(width: 300, height: 500) + .windowStyle(.hiddenTitleBar) + .handlesExternalEvents(matching: Set(arrayLiteral: SchemeAction.translateInWindow.rawValue)) + + + Window("Settings", id: WindowID.settings.id) { + SettingsView(updater: updaterController.updater) + .ignoresSafeArea(edges: .top) + .fixedSize() + } + .windowResizability(.contentSize) + .windowStyle(.hiddenTitleBar) + .handlesExternalEvents(matching: Set(arrayLiteral: SchemeAction.settings.rawValue)) + } +} + +struct AppMenu: View { + @Environment(\.openWindow) private var openWindow + @Environment(\.openURL) private var openURL + @Default(\.hotKey) var hotKey + @Default(\.ocrHotKey) var ocrHotKey + @Default(\.primaryLanguage) var primaryLanguage + @Default(\.hotKeyTriggerInNotification) var hotKeyTriggerInNotification + @Default(\.preProcessSource) var preProcessSource + + + private func _translateInNotification(text: String) { + ProviderManager.shared.translate(text, LanguageManager.getLanguageByCode(primaryLanguage)!) { data in + pushNotification(title: data.source, body: data.target) + } + } + + private func _gotoTranslate(text: String? = nil) { + openURL(SchemeURLManager.getUrlByAction(SchemeAction.translateInWindow, querys: ["src": text ?? ""])) + } + + private func _gotoSettings() { + openURL(SchemeURLManager.getUrlByAction(SchemeAction.settings)) + } + + private func _quit() { + NSApplication.shared.terminate(nil) + } + + // MARK: hotkey + private func _onHotKeyTranslate() { + let text = getSelectedText() ?? "" + + if (!text.isEmpty && Defaults.shared.hotKeyTriggerInNotification) { + _translateInNotification(text: text) + } else { + _gotoTranslate(text: text) + } + } + + private func _onHotKeyOCR() { + OCRManager.shared.captureWithOCR { text in + if (Defaults.shared.hotKeyTriggerInNotification) { + _translateInNotification(text: text) + } else { + _gotoTranslate(text: text) + } + } + } + + init() { + KeyboardShortcuts.onKeyUp(for: .translate, action: _onHotKeyTranslate) + KeyboardShortcuts.onKeyUp(for: .ocr, action: _onHotKeyOCR) + } + + var body: some View { + VStack { + Button(action: _onHotKeyTranslate, label: { Text("Translate") }) + .keyboardShortcut(string2Shortcut(hotKey)) + + Button(action: _onHotKeyOCR, label: { Text("OCR Translate") }) + .keyboardShortcut(string2Shortcut(ocrHotKey)) + + Button(action: _gotoSettings, label: { Text("Settings...") }) + + Divider() + + Toggle(isOn: $hotKeyTriggerInNotification, label: { + Text("In-Notification Mode") + }).toggleStyle(.checkbox) + + Toggle(isOn: $preProcessSource, label: { + Text("Preprocess Source Text") + }).toggleStyle(.checkbox) + + Divider() + + Button(action: _quit, label: { Text("Quit") }) + .keyboardShortcut("q") + } + } +} diff --git a/liltr/userDefaults.swift b/liltr/userDefaults.swift new file mode 100644 index 0000000..12ea4bd --- /dev/null +++ b/liltr/userDefaults.swift @@ -0,0 +1,68 @@ +import SwiftUI + +// https://fatbobman.com/zh/posts/appstorage/ +public class Defaults: ObservableObject { + @AppStorage("launchAtLogin") public var launchAtLogin = false + @AppStorage("menuIconSymbol") public var menuIconSymbol = "" + @AppStorage("floatOnTop") public var floatOnTop = false + + // MARK: HotKey + @AppStorage("hotKey") public var hotKey = "" + @AppStorage("ocrHotKey") public var ocrHotKey = "" + @AppStorage("hotKeyTriggerInNotification") public var hotKeyTriggerInNotification = true + + // MARK: Language + @AppStorage("primaryLanguage") public var primaryLanguage = LANGUAGE_ARRAY[0].code + @AppStorage("secondaryLanguage") public var secondaryLanguage = LANGUAGE_ARRAY[1].code + + // MARK: Provider + @AppStorage("primaryProvider") public var primaryProvider = NiuTransProviderName + @AppStorage("secondaryProvider") public var secondaryProvider = VolcengineProviderName + // niuTrans + @AppStorage("\(NiuTransProviderName)AK") public var NiuTransAK = "" + @AppStorage("\(NiuTransProviderName)SK") public var NiuTransSK = "" + // baidu + @AppStorage("\(BaiduProviderName)AK") public var BaiduAK = "" + @AppStorage("\(BaiduProviderName)SK") public var BaiduSK = "" + // volcengine + @AppStorage("\(VolcengineProviderName)AK") public var VolcengineAK = "" + @AppStorage("\(VolcengineProviderName)SK") public var VolcengineSK = "" + // ali + @AppStorage("\(AliProviderName)AK") public var AliAK = "" + @AppStorage("\(AliProviderName)SK") public var AliSK = "" + // big huge + @AppStorage("\(BigHugeThesaurusProviderName)AK") public var BigHugeThesaurusAK = "" + @AppStorage("\(BigHugeThesaurusProviderName)SK") public var BigHugeThesaurusSK = "" + + // MARK: Dictionary + @AppStorage("dictionary") public var dictionary = DCSOxfordDictionaryOfEnglish + + // MARK: Advanced + @AppStorage("preProcessSource") public var preProcessSource = true + + public static let shared = Defaults() +} + +@propertyWrapper +public struct Default: DynamicProperty { + @ObservedObject private var defaults: Defaults + private let keyPath: ReferenceWritableKeyPath + public init(_ keyPath: ReferenceWritableKeyPath, defaults: Defaults = .shared) { + self.keyPath = keyPath + self.defaults = defaults + } + + public var wrappedValue: T { + get { defaults[keyPath: keyPath] } + nonmutating set { defaults[keyPath: keyPath] = newValue } + } + + public var projectedValue: Binding { + Binding( + get: { defaults[keyPath: keyPath] }, + set: { value in + defaults[keyPath: keyPath] = value + } + ) + } +} diff --git a/scripts/commit.sh b/scripts/commit.sh new file mode 100755 index 0000000..9f4c59d --- /dev/null +++ b/scripts/commit.sh @@ -0,0 +1,7 @@ +version="$(cat $VERSION_FILE_PATH)" + +git config --global user.name github-actions +git config --global user.email github-actions@github.com +git add . +git commit -m "chore: auto release $version" +git push \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..a756e93 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -exu + +env | sort + +scripts/update_version.sh +scripts/update_secret.sh + +xcodebuild -project liltr.xcodeproj -scheme liltr -derivedDataPath $XCODE_BUILD_DIR -configuration Release +file $XCODE_BUILD_PATH/liltr.app/Contents/MacOS/liltr.app + +scripts/update_appcast.sh +scripts/commit.sh + +# https://github.com/lwouis/alt-tab-macos/blob/b8833b74eb367217c628acf58ca9a459c21dd028/scripts/travis.sh#L24 \ No newline at end of file diff --git a/scripts/sign_update b/scripts/sign_update new file mode 100755 index 0000000..90bd66b Binary files /dev/null and b/scripts/sign_update differ diff --git a/scripts/update_appcast.sh b/scripts/update_appcast.sh new file mode 100755 index 0000000..c1150b9 --- /dev/null +++ b/scripts/update_appcast.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -exu + +version="$(cat $VERSION_FILE_PATH)" +minimumSystemVersion="$(awk -F ' = ' '/MACOSX_DEPLOYMENT_TARGET/ { print $2; exit }' < $PROJECT_PATH)" +zipName="$APP_NAME-$version.zip" +edSignatureAndLength=$(scripts/sign_update -s $SPARKLE_ED_PRIVATE_KEY "$XCODE_BUILD_PATH/$zipName") +date="$(date +'%a, %d %b %Y %H:%M:%S %z')" + +echo " + + Version $version + $date + $minimumSystemVersion + https://alt-tab-macos.netlify.app/changelog-bare + + +" > ITEM.txt + +sed -i '' -e "/<\/language>/r ITEM.txt" appcast.xml \ No newline at end of file diff --git a/scripts/update_secret.sh b/scripts/update_secret.sh new file mode 100755 index 0000000..51c8887 --- /dev/null +++ b/scripts/update_secret.sh @@ -0,0 +1,11 @@ +set -exu + +# update info.plist +/usr/libexec/PlistBuddy -c "Set :AliAK $ALI_AK" "$INFO_PLIST_PATH" +/usr/libexec/PlistBuddy -c "Set :AliSK $ALI_SK" "$INFO_PLIST_PATH" +/usr/libexec/PlistBuddy -c "Set :BaiduAK $BAIDU_AK" "$INFO_PLIST_PATH" +/usr/libexec/PlistBuddy -c "Set :BaiduSK $BAIDU_SK" "$INFO_PLIST_PATH" +/usr/libexec/PlistBuddy -c "Set :VolcengineAK $VOLCENGINE_AK" "$INFO_PLIST_PATH" +/usr/libexec/PlistBuddy -c "Set :VolcengineSK $VOLCENGINE_SK" "$INFO_PLIST_PATH" +/usr/libexec/PlistBuddy -c "Set :NiuTransSK $NIUTRANS_SK" "$INFO_PLIST_PATH" +/usr/libexec/PlistBuddy -c "Set :BigHugeThesaurusSK $BIGHUGETHESAURUS_SK" "$INFO_PLIST_PATH" diff --git a/scripts/update_version.sh b/scripts/update_version.sh new file mode 100755 index 0000000..c08f863 --- /dev/null +++ b/scripts/update_version.sh @@ -0,0 +1,14 @@ +set -exu + +# get old version +VERSION=$(/usr/libexec/PlistBuddy -c "Print VERSION" "$INFO_PLIST_PATH") + +# get new version +NEXT_VERSION=$(npx semver -i patch $VERSION) +echo "$NEXT_VERSION" > $VERSION_FILE_PATH + +# update info.plist +/usr/libexec/PlistBuddy -c "Set :VERSION $NEXT_VERSION" "$INFO_PLIST_PATH" + + +