├── MinaOTP-MAC ├── MinaOtp.icns ├── Assets.xcassets │ ├── Contents.json │ ├── open.imageset │ │ ├── open@1x.png │ │ ├── open@2x.png │ │ └── Contents.json │ ├── close.imageset │ │ ├── close@1x.png │ │ ├── close@2x.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon_16x16.png │ │ ├── icon_32x32.png │ │ ├── icon_128x128.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_512x512@2x.png │ │ └── Contents.json │ ├── statusIcon.imageset │ │ ├── iconBlack@1x.png │ │ └── iconBlack@2x.png │ ├── image_normal.imageset │ │ ├── image_normal@1x.png │ │ ├── image_normal@2x.png │ │ └── Contents.json │ ├── image_selected.imageset │ │ ├── image_selected@1x.png │ │ ├── image_selected@2x.png │ │ └── Contents.json │ └── StatusIcon.imageset │ │ └── Contents.json ├── en.lproj │ ├── InfoPlist.strings │ ├── Localizable.strings │ └── MainMenu.strings ├── zh-Hans.lproj │ ├── InfoPlist.strings │ ├── Localizable.strings │ └── MainMenu.strings ├── MinaOTP_MAC.entitlements ├── Utils.h ├── Magnet │ ├── BoolExtensions.swift │ ├── Magnet.h │ ├── Info.plist │ ├── HotKey.swift │ ├── KeyCombo.swift │ ├── KeyCodeTransformer.swift │ ├── KeyTransformer.swift │ └── HotKeyCenter.swift ├── OTPGenerator │ ├── GeneratorTotp.h │ ├── Base32Addition.h │ ├── GeneratorTotp.m │ └── Base32Addition.m ├── MinaOTP-MAC-Bridging-Header.h ├── CSTableRowView.swift ├── MinaOTP-MAC.entitlements ├── Info.plist ├── ShowTips.swift ├── StatusItemView.swift ├── ProgressView.swift ├── Utils.m ├── CellView.swift ├── DataManager.swift ├── ScanWindow.swift ├── Tools.swift ├── EditPopoverViewController.swift ├── AppDelegate.swift ├── AddWindow.swift ├── FlatButton.swift └── PopoverViewController.swift ├── MinaOTP-MAC.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ ├── Four.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ ├── wubin.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ ├── rawlings.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── wujianming.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ ├── wubin.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ ├── Four.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ ├── wujianming.xcuserdatad │ │ └── xcschemes │ │ │ ├── xcschememanagement.plist │ │ │ └── MinaOTP-MAC.xcscheme │ └── rawlings.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcshareddata │ └── xcschemes │ └── MinaOTP-MAC.xcscheme ├── .github └── workflows │ └── swift.yml ├── MinaOTP-MACTests ├── Info.plist └── MinaOTP_MACTests.swift ├── MinaOTP-MACUITests ├── Info.plist └── MinaOTP_MACUITests.swift ├── README_zh.md └── README.md /MinaOTP-MAC/MinaOtp.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/MinaOtp.icns -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/open.imageset/open@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/open.imageset/open@1x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/open.imageset/open@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/open.imageset/open@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/close.imageset/close@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/close.imageset/close@1x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/close.imageset/close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/close.imageset/close@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/statusIcon.imageset/iconBlack@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/statusIcon.imageset/iconBlack@1x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/statusIcon.imageset/iconBlack@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/statusIcon.imageset/iconBlack@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/image_normal.imageset/image_normal@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/image_normal.imageset/image_normal@1x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/image_normal.imageset/image_normal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/image_normal.imageset/image_normal@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/image_selected.imageset/image_selected@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/image_selected.imageset/image_selected@1x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/image_selected.imageset/image_selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC/Assets.xcassets/image_selected.imageset/image_selected@2x.png -------------------------------------------------------------------------------- /MinaOTP-MAC/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | MinaOTP-MAC 4 | 5 | Created by 武建明 on 2018/8/13. 6 | Copyright © 2018年 Four_w. All rights reserved. 7 | */ 8 | CFBundleDisplayName = "MinaOTP"; 9 | -------------------------------------------------------------------------------- /MinaOTP-MAC/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | MinaOTP-MAC 4 | 5 | Created by 武建明 on 2018/8/13. 6 | Copyright © 2018年 Four_w. All rights reserved. 7 | */ 8 | CFBundleDisplayName = "MinaOTP"; 9 | -------------------------------------------------------------------------------- /MinaOTP-MAC/MinaOTP_MAC.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/project.xcworkspace/xcuserdata/Four.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC.xcodeproj/project.xcworkspace/xcuserdata/Four.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/project.xcworkspace/xcuserdata/wubin.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC.xcodeproj/project.xcworkspace/xcuserdata/wubin.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/project.xcworkspace/xcuserdata/rawlings.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC.xcodeproj/project.xcworkspace/xcuserdata/rawlings.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/project.xcworkspace/xcuserdata/wujianming.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-MAC/HEAD/MinaOTP-MAC.xcodeproj/project.xcworkspace/xcuserdata/wujianming.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MinaOTP-MAC/Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // QRCodeUtils.h 3 | // ShadowsocksX-NG 4 | // 5 | // Created by 邱宇舟 on 16/6/8. 6 | // Copyright © 2016年 qiuyuzhou. All rights reserved. 7 | // 8 | 9 | #ifndef QRCodeUtils_h 10 | #define QRCodeUtils_h 11 | 12 | void ScanQRCodeOnScreen(); 13 | 14 | #endif /* QRCodeUtils_h */ 15 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Magnet/BoolExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolExtensions.swift 3 | // Magnet 4 | // 5 | // Created by 古林俊佑 on 2018/09/22. 6 | // Copyright © 2018年 Shunsuke Furubayashi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bool { 12 | var intValue: Int { 13 | return NSNumber(value: self).intValue 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MinaOTP-MAC/OTPGenerator/GeneratorTotp.h: -------------------------------------------------------------------------------- 1 | // 2 | // GeneratorTotp.h 3 | // Mina_OTP_OC 4 | // 5 | // Created by 武建明 on 2018/2/23. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface GeneratorTotp : NSObject 12 | 13 | + (NSString *)generateOTPForSecret:(NSString *)secret; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: MinaOTP-MAC 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /MinaOTP-MAC/MinaOTP-MAC-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // MinaOTP-MAC-Bridging-Header.h 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/3. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | #ifndef MinaOTP_MAC_Bridging_Header_h 10 | #define MinaOTP_MAC_Bridging_Header_h 11 | 12 | #import "GeneratorTotp.h" 13 | #import "Utils.h" 14 | 15 | #endif /* MinaOTP_MAC_Bridging_Header_h */ 16 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/xcuserdata/wubin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MinaOTP-MAC.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/xcuserdata/Four.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MinaOTP-MAC.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/open.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "open@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "open@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/image_normal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image_normal@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "image_normal@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/image_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image_selected@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "image_selected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "close@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "close@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Magnet/Magnet.h: -------------------------------------------------------------------------------- 1 | // 2 | // Magnet.h 3 | // Magnet 4 | // 5 | // Created by 古林俊佑 on 2016/03/09. 6 | // Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Magnet. 12 | FOUNDATION_EXPORT double MagnetVersionNumber; 13 | 14 | //! Project version string for Magnet. 15 | FOUNDATION_EXPORT const unsigned char MagnetVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/StatusIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "iconBlack@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "iconBlack@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MinaOTP-MAC/OTPGenerator/Base32Addition.h: -------------------------------------------------------------------------------- 1 | // 2 | // Base32Addition.h 3 | // Mina_OTP_OC 4 | // 5 | // Created by 武建明 on 2018/4/11. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #define NSBase32StringEncoding 0x4D467E32 12 | 13 | @interface NSString (Base32Addition) 14 | +(NSString *)stringFromBase32String:(NSString *)base32String; 15 | -(NSString *)base32String; 16 | @end 17 | 18 | @interface NSData (Base32Addition) 19 | +(NSData *)dataWithBase32String:(NSString *)base32String; 20 | -(NSString *)base32String; 21 | @end 22 | 23 | @interface Base32Addition : NSObject 24 | 25 | +(NSData *)dataFromBase32String:(NSString *)base32String; 26 | +(NSString *)base32StringFromData:(NSData *)data; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /MinaOTP-MACTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MinaOTP-MACUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MinaOTP-MAC/CSTableRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSTableRowView.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/6. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | class CSTableRowView: NSTableRowView { 11 | 12 | override func drawSelection(in dirtyRect: NSRect) { 13 | 14 | } 15 | override func drawSeparator(in dirtyRect: NSRect) { 16 | var sepRect = self.bounds 17 | sepRect.origin.y = sepRect.maxY-1 18 | sepRect.size.height = 1; 19 | sepRect = NSIntersectionRect(sepRect, dirtyRect); 20 | if (!NSIsEmptyRect(sepRect)) { 21 | NSColor.init(red: 0.92, green: 0.92, blue: 0.92, alpha: 1).set() 22 | }else{ 23 | NSColor.clear.set() 24 | } 25 | __NSRectFill(sepRect); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MinaOTP-MAC/MinaOTP-MAC.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.aps-environment 6 | development 7 | com.apple.developer.icloud-container-identifiers 8 | 9 | iCloud.com.jiangren.minaotp 10 | 11 | com.apple.developer.icloud-services 12 | 13 | CloudKit 14 | 15 | com.apple.security.app-sandbox 16 | 17 | com.apple.security.files.user-selected.read-write 18 | 19 | com.apple.security.network.client 20 | 21 | com.apple.security.network.server 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/xcuserdata/wujianming.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MinaOTP-MAC.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 20FBA95221106EB10017BFC3 16 | 17 | primary 18 | 19 | 20 | 20FBA96221106EB20017BFC3 21 | 22 | primary 23 | 24 | 25 | 20FBA96D21106EB20017BFC3 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/xcuserdata/rawlings.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MinaOTP-MAC.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 20FBA95221106EB10017BFC3 16 | 17 | primary 18 | 19 | 20 | 20FBA96221106EB20017BFC3 21 | 22 | primary 23 | 24 | 25 | 20FBA96D21106EB20017BFC3 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Magnet/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /MinaOTP-MACTests/MinaOTP_MACTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MinaOTP_MACTests.swift 3 | // MinaOTP-MACTests 4 | // 5 | // Created by 武建明 on 2018/7/31. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | //@testable import MinaOTP_MAC 11 | 12 | class MinaOTP_MACTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | LSUIElement 26 | 27 | NSHumanReadableCopyright 28 | Copyright © 2018年 Four_w. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /MinaOTP-MAC/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MinaOTP-MAC 4 | 5 | Created by 武建明 on 2018/8/13. 6 | Copyright © 2018年 Four_w. All rights reserved. 7 | */ 8 | "add" = "添加"; 9 | "export" = "导出"; 10 | "import" = "导入"; 11 | "add_title" = "添加二步验证"; 12 | "scan_title" = "扫描二维码图片"; 13 | "choose_qr_image_btn_title" = "选取二维码图片"; 14 | "scan_qr_image_btn_title" = "扫描二维码图片"; 15 | "scan_qr_success" = "扫描成功, 1秒后将自动关闭"; 16 | "save" = "保存"; 17 | "cancel" = "取消"; 18 | "remark_placeholder" = "请输入remark:"; 19 | "issuer_placeholder" = "请输入issuer:"; 20 | "secret_placeholder" = "请输入secret:"; 21 | "add_success_tip" = "添加成功,请返回程序查看"; 22 | "image_error_tip" = "非二维码图片"; 23 | "image_content_error_tip" = "图片内容格式不正确"; 24 | "tool_tip" = "祝:每天开心,代码无BUG"; 25 | "export_help" = "下载导入文件格式"; 26 | "help" = "帮助"; 27 | "exit" = "退出"; 28 | "export_title" = "选择文件导出位置"; 29 | "delete" = "删除"; 30 | "edit" = "编辑"; 31 | "export_sucess" = "导出成功"; 32 | "export_failure" = "导出失败,请重试"; 33 | "parse_failed" = "导入失败,请提供正确的Json格式"; 34 | "choose_title" = "选择"; 35 | "copy_success" = "复制成功"; 36 | "check_releases" = "检测更新"; 37 | "confirm" = "确定"; 38 | "has_new_version" = "呀哈哈! 你发现了新版本更新。\n请前往[https://github.com/MinaOTP/MinaOTP-MAC/releases]进行更新吧!"; 39 | "no_new_version" = "呀哈哈! 你的版本是最新的哦!"; 40 | 41 | -------------------------------------------------------------------------------- /MinaOTP-MAC/ShowTips.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowTips.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/6. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ShowTips: NSObject { 12 | 13 | public func showTip(message: String, view: NSView) { 14 | self.tipTextField.stringValue = " \(message) " 15 | tipTextField.sizeToFit() 16 | tipTextField.frame = NSRect(x: (view.bounds.size.width-tipTextField.bounds.size.width)/2, y: (view.bounds.size.height-tipTextField.bounds.size.height)/2, width: tipTextField.bounds.size.width, height: tipTextField.bounds.size.height+5) 17 | view.addSubview(tipTextField) 18 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 19 | self.tipTextField.removeFromSuperview() 20 | } 21 | } 22 | lazy var tipTextField:NSTextField! = { 23 | let tip = NSTextField.init(frame: NSRect(x: 100, y: 100, width: 100, height: 30)) 24 | tip.wantsLayer = true 25 | tip.textColor = NSColor.white 26 | tip.isBordered = false 27 | tip.backgroundColor = NSColor.init(red: 0, green: 0, blue: 0, alpha: 0.5) 28 | tip.font = NSFont.systemFont(ofSize: 18) 29 | tip.layer?.cornerRadius = 2 30 | return tip 31 | }() 32 | } 33 | -------------------------------------------------------------------------------- /MinaOTP-MACUITests/MinaOTP_MACUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MinaOTP_MACUITests.swift 3 | // MinaOTP-MACUITests 4 | // 5 | // Created by 武建明 on 2018/7/31. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class MinaOTP_MACUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /MinaOTP-MAC/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MinaOTP-MAC 4 | 5 | Created by 武建明 on 2018/8/13. 6 | Copyright © 2018年 Four_w. All rights reserved. 7 | */ 8 | "add" = "Add"; 9 | "export" = "Export"; 10 | "import" = "Import"; 11 | "add_title" = "Add Two-factor Authentication"; 12 | "scan_title" = "Scan Qr Code Image"; 13 | "choose_qr_image_btn_title" = "Choose Qr Code Image"; 14 | "scan_qr_image_btn_title" = "Scan Qr Code Image"; 15 | "scan_qr_success" = "Scan is successful, will automatically shut down after 1 second"; 16 | "save" = "Save"; 17 | "cancel" = "Cancel"; 18 | "remark_placeholder" = "Please enter the remark:"; 19 | "issuer_placeholder" = "Please enter the issuer:"; 20 | "secret_placeholder" = "Please enter the secret:"; 21 | "add_success_tip" = "Added successfully, please return it to look at it"; 22 | "image_error_tip" = "It's not qr code image"; 23 | "image_content_error_tip" = "Qr code image content format is not correct"; 24 | "tool_tip" = "Wish you have a nice day"; 25 | "export_help" = "Download the format of import file"; 26 | "help" = "Help"; 27 | "exit" = "Exit"; 28 | "export_title" = "Select the file export position"; 29 | "delete" = "Delete"; 30 | "edit" = "Edit"; 31 | "export_sucess" = "Exported successfully"; 32 | "export_failure" = "Export of failure, please try again"; 33 | "parse_failed" = "Parse failed, please provide the correct json content"; 34 | "choose_title" = "Choose"; 35 | "copy_success" = "Copy Success"; 36 | "check_releases" = "Check Update"; 37 | "confirm" = "Confirm"; 38 | "has_new_version" = "Ah Ha Ha! You have found the new version! \n Please go to [https://github.com/MinaOTP/MinaOTP-MAC/releases]! to update"; 39 | "no_new_version" = "Ah Ha Ha! Your version is the latest!"; 40 | 41 | -------------------------------------------------------------------------------- /MinaOTP-MAC/StatusItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusItemView.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/8. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | class StatusItemView: NSControl { 11 | 12 | var statusItem = NSStatusItem.init() 13 | let image = NSImage(named: "icon") 14 | weak var delegate : StatusItemViewDelegate? 15 | 16 | override init(frame frameRect: NSRect) { 17 | super.init(frame: NSZeroRect) 18 | self.wantsLayer = true 19 | self.layer?.backgroundColor = NSColor.clear.cgColor 20 | } 21 | 22 | required init?(coder decoder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | override func draw(_ dirtyRect: NSRect) { 26 | super.draw(dirtyRect) 27 | statusItem.drawStatusBarBackground(in: self.bounds, withHighlight: false) 28 | let imageRect = NSInsetRect(NSRect(x: (self.bounds.size.width-18)/2, y: (self.bounds.size.height-18)/2, width: 18, height: 18), 0, 0) 29 | self.image?.draw(in: imageRect, from: NSZeroRect, operation: .color, fraction: 1) 30 | } 31 | override func moveDown(_ sender: Any?) { 32 | self.setNeedsDisplay() 33 | } 34 | override func rightMouseDown(with event: NSEvent) { 35 | // NSApp.sendAction(righeAction, to: self.target, from: self) 36 | self.setNeedsDisplay() 37 | self.delegate?.rightMouseDownAction() 38 | } 39 | override func mouseDown(with event: NSEvent) { 40 | self.setNeedsDisplay() 41 | self.delegate?.leftMouseDownAction() 42 | } 43 | 44 | } 45 | protocol StatusItemViewDelegate: class { 46 | func rightMouseDownAction() 47 | func leftMouseDownAction() 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /MinaOTP-MAC/OTPGenerator/GeneratorTotp.m: -------------------------------------------------------------------------------- 1 | // 2 | // GeneratorTotp.m 3 | // Mina_OTP_OC 4 | // 5 | // Created by 武建明 on 2018/2/23. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | #import "GeneratorTotp.h" 10 | #include 11 | #include 12 | #import "Base32Addition.h" 13 | 14 | static NSUInteger kPinModTable[] = { 15 | 0, 16 | 10, 17 | 100, 18 | 1000, 19 | 10000, 20 | 100000, 21 | 1000000, 22 | 10000000, 23 | 100000000, 24 | }; 25 | 26 | @implementation GeneratorTotp 27 | 28 | + (NSTimeInterval)defaultPeriod { 29 | return 30; 30 | } 31 | + (NSString *)generateOTPForSecret:(NSString *)secret{ 32 | 33 | CCHmacAlgorithm alg = kCCHmacAlgSHA1; 34 | NSUInteger hashLength = CC_SHA1_DIGEST_LENGTH; 35 | 36 | NSData *secretData = [NSData dataWithBase32String:secret]; 37 | 38 | NSDate *date = [NSDate date]; 39 | NSLog(@"%@",date); 40 | NSTimeInterval seconds = [date timeIntervalSince1970]; 41 | 42 | uint64_t counter = (uint64_t)(seconds / [self defaultPeriod]); 43 | 44 | counter = NSSwapHostLongLongToBig(counter); 45 | NSData *counterData = [NSData dataWithBytes:&counter 46 | length:sizeof(counter)]; 47 | 48 | NSMutableData *hash = [NSMutableData dataWithLength:hashLength]; 49 | CCHmacContext ctx; 50 | CCHmacInit(&ctx, alg, [secretData bytes], [secretData length]); 51 | CCHmacUpdate(&ctx, [counterData bytes], [counterData length]); 52 | CCHmacFinal(&ctx, [hash mutableBytes]); 53 | const char *ptr = [hash bytes]; 54 | unsigned char offset = ptr[hashLength-1] & 0x0f; 55 | 56 | uint32_t truncatedHash = 57 | NSSwapBigIntToHost(*((uint32_t *)&ptr[offset])) & 0x7fffffff; 58 | uint32_t pinValue = truncatedHash % kPinModTable[6]; 59 | 60 | NSString *str = [NSString stringWithFormat:@"%0*u", (int)6, pinValue];; 61 | 62 | while (str.length < 6) { 63 | str = [NSString stringWithFormat:@"0%@",str]; 64 | } 65 | return str; 66 | } 67 | @end 68 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # MinaOTP-MAC 2 | 3 | [![](https://img.shields.io/badge/platform-osx-red.svg)](https://github.com/MinaOTP/MinaOTP-MAC) [![](https://img.shields.io/github/release/MinaOTP/MinaOTP-MAC.svg)](https://github.com/MinaOTP/MinaOTP-MAC/releases) [![](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/MinaOTP/MinaOTP-MTP) 4 | 6 | MinaOTP-MAC 是一款运行在macOS上的App. 是基于[RFC6238](https://tools.ietf.org/html/rfc6238)算法, 算法实现语言是 `Objective-C`。App功能开发基于Swift 7 | 8 | 该App能够很方便的为你提供二步验证码。包括对Token的添加、编辑、删除、导入、导出等功能 9 | 10 | ## 依赖 11 | 12 | - macOS 10.10+ 13 | - Xcode 9.4.1+ 14 | - Swift 4.1 15 | 16 | ### 软件截图 17 | 18 | 首页 19 | 20 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_10.png) 21 | 22 | 鼠标右键点击弹出删除和编辑 23 | 24 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_09.jpeg) 25 | 26 | 编辑 27 | 28 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_05.png) 29 | 30 | 鼠标右键点击状态栏Icon弹出帮助页面 31 | 32 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_07.png) 33 | 34 | 添加Token 35 | 36 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_08.jpeg) 37 | 38 | 扫描二维码 39 | 40 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_02.png) 41 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_03.png) 42 | 43 | 44 | ### 功能 45 | 46 | * 生成 2FA token 47 | * 选择一个二维码图片,识别并添加 48 | * 扫描一个二维码图片,识别并添加 49 | * 手动添加 50 | * 编辑所有信息 51 | * 删除 52 | * 导出token信息,生成本地的Json文件 53 | * 从本地的Json文件,添加Token信息 54 | * 快捷键: ⌘⌘ (双击 ⌘ 键) 55 | 56 | ### 将来 57 | * [x] 同步到icloud 58 | * [ ] 添加快捷键用来复制token 59 | 60 | ### 感谢 61 | [FlatButton](https://github.com/OskarGroth/FlatButton) 62 | [Magnet](https://github.com/Clipy/Magnet) 63 | Thanks for their great work. 64 | 65 | ### [README](README.md) -------------------------------------------------------------------------------- /MinaOTP-MAC/ProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressView.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/3. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ProgressView: NSView { 12 | 13 | override init(frame frameRect: NSRect) { 14 | super.init(frame: frameRect) 15 | print("已经创建") 16 | self.wantsLayer = true 17 | self.layer?.addSublayer(self.bottomShapeLayer) 18 | self.layer?.addSublayer(self.topShapeLayer) 19 | } 20 | 21 | required init?(coder decoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | lazy var bottomShapeLayer: CAShapeLayer = { 26 | let layer = CAShapeLayer.init() 27 | layer.frame = self.bounds 28 | layer.fillColor = NSColor.clear.cgColor 29 | layer.lineWidth = 2 30 | layer.lineJoin = CAShapeLayerLineJoin.miter 31 | layer.lineCap = CAShapeLayerLineCap.square 32 | layer.strokeColor = NSColor.init(red: 0.9, green: 0.9, blue: 0.9, alpha: 1).cgColor 33 | layer.path = self.bezierPath.cgPath 34 | layer.strokeEnd = 1 35 | return layer 36 | }() 37 | lazy var topShapeLayer: CAShapeLayer = { 38 | let layer = CAShapeLayer.init() 39 | layer.fillColor = NSColor.clear.cgColor 40 | layer.frame = self.bounds 41 | layer.lineWidth = 2 42 | layer.lineJoin = CAShapeLayerLineJoin.miter 43 | layer.lineCap = CAShapeLayerLineCap.square 44 | layer.strokeColor = NSColor.mainColor.cgColor 45 | layer.path = self.bezierPath.cgPath 46 | layer.strokeEnd = 0 47 | return layer 48 | }() 49 | lazy var bezierPath: NSBezierPath = { 50 | let path = NSBezierPath.init() 51 | path.move(to: CGPoint(x: 0, y: self.bounds.size.height/2)) 52 | path.line(to: CGPoint(x: self.bounds.size.width, y: self.bounds.size.height/2)) 53 | path.stroke() 54 | return path 55 | }() 56 | func setProgress(value: CGFloat) { 57 | // if value>0.85 { 58 | // self.topShapeLayer.strokeColor = NSColor.red.cgColor 59 | // 60 | // }else{ 61 | // self.topShapeLayer.strokeColor = NSColor.green.cgColor 62 | // } 63 | self.topShapeLayer.strokeEnd = value 64 | self.topShapeLayer.strokeColor = NSColor.mainColor.cgColor 65 | // self.topShapeLayer.strokeColor = NSColor.init(red: value, green: (1.0-value)*3, blue: 0, alpha: 1).cgColor 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MinaOTP-MAC 2 | 3 | [![](https://img.shields.io/badge/platform-osx-red.svg)](https://github.com/MinaOTP/MinaOTP-MAC) [![](https://img.shields.io/github/release/MinaOTP/MinaOTP-MAC.svg)](https://github.com/MinaOTP/MinaOTP-MAC/releases) [![](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/MinaOTP/MinaOTP-MTP) 4 | 6 | MinaOTP-MAC is a two-factor authentication tray app that runs at macOS. It's based on [RFC6238](https://tools.ietf.org/html/rfc6238), and the algorithm was implement by `Objective-C` 7 | 8 | The program will generate secure dynamic 2FA tokens for you, and the `add`, `edit`, `remove`,`import`, `export` are pretty convenient. 9 | 10 | ## Requirements 11 | 12 | - macOS 10.10+ 13 | - Xcode 9.4.1+ 14 | - Swift 4.1 15 | 16 | ### Software Screenshot 17 | 18 | Home 19 | 20 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_10.png) 21 | 22 | right click the item to delete or edit 23 | 24 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_09.jpeg) 25 | 26 | Edit 27 | 28 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_05.png) 29 | 30 | right click the statusbar to get help 31 | 32 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_07.png) 33 | 34 | Add Token 35 | 36 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_08.jpeg) 37 | 38 | Scan Qr Code 39 | 40 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_02.png) 41 | ![screenshot](https://raw.githubusercontent.com/wjmwjmwb/GitImage/master/MinaOtp-Guide/guide_en_03.png) 42 | 43 | 44 | ### Feature 45 | 46 | * Generate the 2FA token 47 | * Choose a qrcode_image to add a new token 48 | * Scan a qrcode_image to add a new token 49 | * Add a new token mannually 50 | * Edit the issuer and remark info 51 | * Remove a existed token 52 | * Backup datas to local json files 53 | * Import datas from local json files 54 | * Hotkey: ⌘⌘ (double click ⌘) 55 | 56 | ### Todo 57 | * [x] sync to icloud 58 | * [ ] add hotkey to copy token 59 | 60 | ### Acknowledgements 61 | [FlatButton](https://github.com/OskarGroth/FlatButton) 62 | [Magnet](https://github.com/Clipy/Magnet) 63 | Thanks for their great work. 64 | 65 | ### [中文文档](README_zh.md) -------------------------------------------------------------------------------- /MinaOTP-MAC/Utils.m: -------------------------------------------------------------------------------- 1 | // 2 | // QRCodeUtils.m 3 | // ShadowsocksX-NG 4 | // 5 | // Created by 邱宇舟 on 16/6/8. 6 | // Copyright © 2016年 qiuyuzhou. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | void ScanQRCodeOnScreen() { 13 | /* displays[] Quartz display ID's */ 14 | CGDirectDisplayID *displays = nil; 15 | 16 | CGError err = CGDisplayNoErr; 17 | CGDisplayCount dspCount = 0; 18 | 19 | /* How many active displays do we have? */ 20 | err = CGGetActiveDisplayList(0, NULL, &dspCount); 21 | 22 | /* If we are getting an error here then their won't be much to display. */ 23 | if(err != CGDisplayNoErr) 24 | { 25 | NSLog(@"Could not get active display count (%d)\n", err); 26 | return; 27 | } 28 | 29 | /* Allocate enough memory to hold all the display IDs we have. */ 30 | displays = calloc((size_t)dspCount, sizeof(CGDirectDisplayID)); 31 | 32 | // Get the list of active displays 33 | err = CGGetActiveDisplayList(dspCount, 34 | displays, 35 | &dspCount); 36 | 37 | /* More error-checking here. */ 38 | if(err != CGDisplayNoErr) 39 | { 40 | NSLog(@"Could not get active display list (%d)\n", err); 41 | return; 42 | } 43 | 44 | // NSMutableArray* foundSSUrls = [NSMutableArray array]; 45 | 46 | CIDetector *detector = [CIDetector detectorOfType:@"CIDetectorTypeQRCode" 47 | context:nil 48 | options:@{ CIDetectorAccuracy:CIDetectorAccuracyHigh }]; 49 | 50 | for (unsigned int displaysIndex = 0; displaysIndex < dspCount; displaysIndex++){ 51 | /* Make a snapshot image of the current display. */ 52 | CGImageRef image = CGDisplayCreateImage(displays[displaysIndex]); 53 | NSArray *features = [detector featuresInImage:[CIImage imageWithCGImage:image]]; 54 | 55 | NSLog(@"=====features:\n%@", features); 56 | 57 | for (CIQRCodeFeature *feature in features) { 58 | NSLog(@"%@", feature.messageString); 59 | // if ( [feature.messageString hasPrefix:@"ss://"] ) 60 | // { 61 | // NSURL *url = [NSURL URLWithString:feature.messageString]; 62 | // if (url) { 63 | // [foundSSUrls addObject:url]; 64 | // } 65 | // } 66 | } 67 | CGImageRelease(image); 68 | } 69 | 70 | free(displays); 71 | 72 | // [[NSNotificationCenter defaultCenter] 73 | // postNotificationName:@"NOTIFY_FOUND_SS_URL" 74 | // object:nil 75 | // userInfo: @{ @"urls": foundSSUrls, 76 | // @"source": @"qrcode" 77 | // } 78 | // ]; 79 | } 80 | -------------------------------------------------------------------------------- /MinaOTP-MAC/CellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellView.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/3. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class CellView: NSView { 12 | override init(frame frameRect: NSRect) { 13 | super.init(frame: frameRect) 14 | self.addSubview(self.codeTextField) 15 | self.addSubview(self.issuerTextField) 16 | self.addSubview(self.remarkTextField) 17 | // self.addSubview(self.hotKeyTextField) 18 | self.wantsLayer = true 19 | self.layer?.backgroundColor = NSColor.clear.cgColor 20 | self.layer?.addSublayer(self.lineLayer) 21 | print(self.bounds.size.height/2) 22 | } 23 | 24 | lazy var codeTextField: NSTextField = { 25 | let lab = NSTextField.init(frame: CGRect(x: 198, y: 30, width: 90, height: 20)) 26 | lab.textColor = .labelColor //NSColor(red: 0, green: 0, blue: 0, alpha: 0.8) 27 | lab.stringValue = "" 28 | lab.isBordered = false 29 | lab.isEditable = false 30 | lab.font = NSFont.boldSystemFont(ofSize: 20) 31 | lab.alignment = NSTextAlignment.right 32 | lab.backgroundColor = NSColor.clear 33 | // lab.backgroundColor = NSColor.red 34 | return lab 35 | }() 36 | lazy var remarkTextField: NSTextField = { 37 | let lab = NSTextField.init(frame: CGRect(x: 12, y: 30, width: 160, height: 20)) 38 | lab.textColor = .labelColor //NSColor(red: 0, green: 0, blue: 0, alpha: 0.8) 39 | lab.stringValue = "" 40 | lab.isBordered = false 41 | lab.isEditable = false 42 | lab.backgroundColor = NSColor.clear 43 | lab.alignment = NSTextAlignment.left 44 | lab.font = NSFont.systemFont(ofSize: 14) 45 | // lab.backgroundColor = NSColor.red 46 | return lab 47 | }() 48 | lazy var issuerTextField: NSTextField = { 49 | let lab = NSTextField.init(frame: CGRect(x: 14, y: 10, width: 100, height: 12)) 50 | lab.textColor = .labelColor //NSColor(red: 0, green: 0, blue: 0, alpha: 0.8) 51 | lab.stringValue = "" 52 | lab.isBordered = false 53 | lab.isEditable = false 54 | lab.backgroundColor = NSColor.clear 55 | lab.alignment = NSTextAlignment.left 56 | lab.font = NSFont.systemFont(ofSize: 10) 57 | return lab 58 | }() 59 | lazy var hotKeyTextField: NSTextField = { 60 | let lab = NSTextField.init(frame: CGRect(x: 263, y: 10, width: 25, height: 12)) 61 | lab.textColor = .labelColor // NSColor(red: 0, green: 0, blue: 0, alpha: 0.8) 62 | lab.stringValue = "" 63 | lab.isBordered = false 64 | lab.isEditable = false 65 | lab.backgroundColor = NSColor.clear 66 | lab.alignment = NSTextAlignment.right 67 | lab.font = NSFont.systemFont(ofSize: 10) 68 | return lab 69 | }() 70 | 71 | lazy var lineLayer: CALayer = { 72 | let line = CALayer() 73 | line.frame = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: 1) 74 | line.backgroundColor = NSColor.init(red: 0.6, green: 0.6, blue: 0.6, alpha: 0.5).cgColor 75 | return line 76 | }() 77 | required init?(coder decoder: NSCoder) { 78 | fatalError("init(coder:) has not been implemented") 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Magnet/HotKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotKey.swift 3 | // Magnet 4 | // 5 | // Created by 古林俊佑 on 2016/03/09. 6 | // Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Carbon 11 | 12 | public final class HotKey: Equatable { 13 | 14 | // MARK: - Properties 15 | public let identifier: String 16 | public let keyCombo: KeyCombo 17 | public let callback: ((HotKey) -> Void)? 18 | public let target: AnyObject? 19 | public let action: Selector? 20 | public let actionQueue: ActionQueue 21 | 22 | var hotKeyId: UInt32? 23 | var hotKeyRef: EventHotKeyRef? 24 | 25 | // MARK: - Enum Value 26 | public enum ActionQueue { 27 | case main 28 | case session 29 | 30 | public func execute(closure: @escaping () -> Void) { 31 | switch self { 32 | case .main: 33 | DispatchQueue.main.async { 34 | closure() 35 | } 36 | case .session: 37 | closure() 38 | } 39 | } 40 | } 41 | 42 | // MARK: - Initialize 43 | public init(identifier: String, keyCombo: KeyCombo, target: AnyObject, action: Selector, actionQueue: ActionQueue = .main) { 44 | self.identifier = identifier 45 | self.keyCombo = keyCombo 46 | self.callback = nil 47 | self.target = target 48 | self.action = action 49 | self.actionQueue = actionQueue 50 | } 51 | 52 | public init(identifier: String, keyCombo: KeyCombo, actionQueue: ActionQueue = .main, handler: @escaping ((HotKey) -> Void)) { 53 | self.identifier = identifier 54 | self.keyCombo = keyCombo 55 | self.callback = handler 56 | self.target = nil 57 | self.action = nil 58 | self.actionQueue = actionQueue 59 | } 60 | 61 | } 62 | 63 | // MARK: - Invoke 64 | public extension HotKey { 65 | func invoke() { 66 | guard let callback = self.callback else { 67 | guard let target = self.target as? NSObject, let selector = self.action else { return } 68 | guard target.responds(to: selector) else { return } 69 | actionQueue.execute { [weak self] in 70 | guard let wSelf = self else { return } 71 | target.perform(selector, with: wSelf) 72 | } 73 | return 74 | } 75 | actionQueue.execute { [weak self] in 76 | guard let wSelf = self else { return } 77 | callback(wSelf) 78 | } 79 | } 80 | } 81 | 82 | // MARK: - Register & UnRegister 83 | public extension HotKey { 84 | @discardableResult 85 | func register() -> Bool { 86 | return HotKeyCenter.shared.register(with: self) 87 | } 88 | 89 | func unregister() { 90 | return HotKeyCenter.shared.unregister(with: self) 91 | } 92 | } 93 | 94 | // MARK: - Equatable 95 | public func == (lhs: HotKey, rhs: HotKey) -> Bool { 96 | return lhs.identifier == rhs.identifier && 97 | lhs.keyCombo == rhs.keyCombo && 98 | lhs.hotKeyId == rhs.hotKeyId && 99 | lhs.hotKeyRef == rhs.hotKeyRef 100 | } 101 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Magnet/KeyCombo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyCombo.swift 3 | // Magnet 4 | // 5 | // Created by 古林俊佑 on 2016/03/09. 6 | // Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Carbon 11 | 12 | public final class KeyCombo: NSObject, NSCopying, NSCoding, Codable { 13 | 14 | // MARK: - Properties 15 | public let keyCode: Int 16 | public let modifiers: Int 17 | public let doubledModifiers: Bool 18 | public var characters: String { 19 | if doubledModifiers { return "" } 20 | return KeyCodeTransformer.shared.transformValue(keyCode, carbonModifiers: modifiers) 21 | } 22 | 23 | // MARK: - Initialize 24 | public init?(keyCode: Int, carbonModifiers: Int) { 25 | if keyCode < 0 || carbonModifiers < 0 { return nil } 26 | 27 | if KeyTransformer.containsFunctionKey(keyCode) { 28 | self.modifiers = Int(UInt(carbonModifiers) | NSEvent.ModifierFlags.function.rawValue) 29 | } else { 30 | self.modifiers = carbonModifiers 31 | } 32 | self.keyCode = keyCode 33 | self.doubledModifiers = false 34 | } 35 | 36 | public init?(keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) { 37 | if keyCode < 0 || !KeyTransformer.supportedCocoaFlags(cocoaModifiers) { return nil } 38 | 39 | if KeyTransformer.containsFunctionKey(keyCode) { 40 | self.modifiers = Int(UInt(KeyTransformer.carbonFlags(from: cocoaModifiers)) | NSEvent.ModifierFlags.function.rawValue) 41 | } else { 42 | self.modifiers = KeyTransformer.carbonFlags(from: cocoaModifiers) 43 | } 44 | self.keyCode = keyCode 45 | self.doubledModifiers = false 46 | } 47 | 48 | public init?(doubledCarbonModifiers modifiers: Int) { 49 | if !KeyTransformer.singleCarbonFlags(modifiers) { return nil } 50 | 51 | self.keyCode = 0 52 | self.modifiers = modifiers 53 | self.doubledModifiers = true 54 | } 55 | 56 | public init?(doubledCocoaModifiers modifiers: NSEvent.ModifierFlags) { 57 | if !KeyTransformer.singleCocoaFlags(modifiers) { return nil } 58 | 59 | self.keyCode = 0 60 | self.modifiers = KeyTransformer.carbonFlags(from: modifiers) 61 | self.doubledModifiers = true 62 | } 63 | 64 | public func copy(with zone: NSZone?) -> Any { 65 | if doubledModifiers { 66 | return KeyCombo(doubledCarbonModifiers: modifiers)! 67 | } else { 68 | return KeyCombo(keyCode: keyCode, carbonModifiers: modifiers)! 69 | } 70 | } 71 | 72 | public init?(coder aDecoder: NSCoder) { 73 | self.keyCode = aDecoder.decodeInteger(forKey: "keyCode") 74 | self.modifiers = aDecoder.decodeInteger(forKey: "modifiers") 75 | self.doubledModifiers = aDecoder.decodeBool(forKey: "doubledModifiers") 76 | } 77 | 78 | public func encode(with aCoder: NSCoder) { 79 | aCoder.encode(keyCode, forKey: "keyCode") 80 | aCoder.encode(modifiers, forKey: "modifiers") 81 | aCoder.encode(doubledModifiers, forKey: "doubledModifiers") 82 | } 83 | 84 | // MARK: - Equatable 85 | public override func isEqual(_ object: Any?) -> Bool { 86 | guard let keyCombo = object as? KeyCombo else { return false } 87 | return keyCode == keyCombo.keyCode && 88 | modifiers == keyCombo.modifiers && 89 | doubledModifiers == keyCombo.doubledModifiers 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/xcshareddata/xcschemes/MinaOTP-MAC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /MinaOTP-MAC.xcodeproj/xcuserdata/wujianming.xcuserdatad/xcschemes/MinaOTP-MAC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Magnet/KeyCodeTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyCodeTransformer.swift 3 | // Magnet 4 | // 5 | // Created by 古林俊佑 on 2016/06/26. 6 | // Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | import Carbon 12 | 13 | open class KeyCodeTransformer { 14 | // MARK: - Properties 15 | public static let shared = KeyCodeTransformer() 16 | } 17 | 18 | // MARK: - Transform 19 | public extension KeyCodeTransformer { 20 | func transformValue(_ keyCode: Int, carbonModifiers: Int) -> String { 21 | return transformValue(keyCode, modifiers: carbonModifiers) 22 | } 23 | 24 | func transformValue(_ keyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) -> String { 25 | return transformValue(keyCode, modifiers: KeyTransformer.carbonFlags(from: cocoaModifiers)) 26 | } 27 | 28 | fileprivate func transformValue(_ keyCode: Int, modifiers: Int) -> String { 29 | // Return Special KeyCode 30 | if let unmappedString = transformSpecialKeyCode(keyCode) { 31 | return unmappedString 32 | } 33 | 34 | let source = TISCopyCurrentASCIICapableKeyboardLayoutInputSource().takeUnretainedValue() 35 | let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) 36 | let dataRef = unsafeBitCast(layoutData, to: CFData.self) 37 | 38 | let keyLayout = unsafeBitCast(CFDataGetBytePtr(dataRef), to: UnsafePointer.self) 39 | 40 | let keyTranslateOptions = OptionBits(CoreServices.kUCKeyTranslateNoDeadKeysBit) 41 | var deadKeyState: UInt32 = 0 42 | let maxChars = 256 43 | var chars = [UniChar](repeating: 0, count: maxChars) 44 | var length = 0 45 | 46 | let error = CoreServices.UCKeyTranslate(keyLayout, 47 | UInt16(keyCode), 48 | UInt16(CoreServices.kUCKeyActionDisplay), 49 | UInt32(modifiers), 50 | UInt32(LMGetKbdType()), 51 | keyTranslateOptions, 52 | &deadKeyState, 53 | maxChars, 54 | &length, 55 | &chars) 56 | 57 | if error != noErr { return "" } 58 | 59 | return NSString(characters: &chars, length: length).uppercased 60 | } 61 | 62 | fileprivate func transformSpecialKeyCode(_ keyCode: Int) -> String? { 63 | return specialKeyCodeStrings[keyCode] 64 | } 65 | } 66 | 67 | // MARK: - Mapping 68 | private extension KeyCodeTransformer { 69 | var specialKeyCodeStrings: [Int: String] { 70 | return [ 71 | kVK_F1: "F1", 72 | kVK_F2: "F2", 73 | kVK_F3: "F3", 74 | kVK_F4: "F4", 75 | kVK_F5: "F5", 76 | kVK_F6: "F6", 77 | kVK_F7: "F7", 78 | kVK_F8: "F8", 79 | kVK_F9: "F9", 80 | kVK_F10: "F10", 81 | kVK_F11: "F11", 82 | kVK_F12: "F12", 83 | kVK_F13: "F13", 84 | kVK_F14: "F14", 85 | kVK_F15: "F15", 86 | kVK_F16: "F16", 87 | kVK_F17: "F17", 88 | kVK_F18: "F18", 89 | kVK_F19: "F19", 90 | kVK_F20: "F20", 91 | kVK_Space: "Space", 92 | kVK_Delete: string(from: 0x232B), // ⌫ 93 | kVK_ForwardDelete: string(from: 0x2326), // ⌦ 94 | kVK_ANSI_Keypad0: string(from: 0x2327), // ⌧ 95 | kVK_LeftArrow: string(from: 0x2190), // ← 96 | kVK_RightArrow: string(from: 0x2192), // → 97 | kVK_UpArrow: string(from: 0x2191), // ↑ 98 | kVK_DownArrow: string(from: 0x2193), // ↓ 99 | kVK_End: string(from: 0x2198), // ↘ 100 | kVK_Home: string(from: 0x2196), // ↖ 101 | kVK_Escape: string(from: 0x238B), // ⎋ 102 | kVK_PageDown: string(from: 0x21DF), // ⇟ 103 | kVK_PageUp: string(from: 0x21DE), // ⇞ 104 | kVK_Return: string(from: 0x21A9), // ↩ 105 | kVK_ANSI_KeypadEnter: string(from: 0x2305), // ⌅ 106 | kVK_Tab: string(from: 0x21E5), // ⇥ 107 | kVK_Help: "?⃝" 108 | ] 109 | } 110 | } 111 | 112 | // MARK: - Charactor 113 | private extension KeyCodeTransformer { 114 | func string(from char: unichar) -> String { 115 | return String(format: "%C", char) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /MinaOTP-MAC/DataManager.swift: -------------------------------------------------------------------------------- 1 | import CloudKit 2 | 3 | 4 | private let ALERT_OFF = "ALERT_OFF" 5 | private let RECORD_KEY = "MinaOtp" 6 | private let recordID = CKRecord.ID(recordName: RECORD_KEY) 7 | 8 | final class DataManager { 9 | 10 | fileprivate init() { 11 | ///forbide to create instance of helper class 12 | } 13 | 14 | fileprivate static var cloudDatabase: CKDatabase { 15 | return CKContainer.default().privateCloudDatabase 16 | } 17 | 18 | static func initial(_ whenDataUpdate: (() -> Void)?) { 19 | checkLoginStatus { isLogged in 20 | print("iCloud isLogged: " + isLogged.description) 21 | if isLogged { 22 | fetchRemote { (totps, error) in 23 | print("***** remote fetched *****") 24 | print(totps) 25 | if let error = error { 26 | print(error) 27 | } else { 28 | UserDefaults.standard.set(totps, forKey: RECORD_KEY) 29 | if let whenDataUpdate = whenDataUpdate { 30 | DispatchQueue.main.async { 31 | whenDataUpdate() 32 | } 33 | } 34 | } 35 | } 36 | } else { 37 | let alertOff = UserDefaults.standard.value(forKey: ALERT_OFF) as? Bool ?? false 38 | if (!alertOff) { 39 | showAlert() 40 | } 41 | } 42 | } 43 | } 44 | 45 | static func get() -> [String] { 46 | return UserDefaults.standard.value(forKey: RECORD_KEY) as? [String] ?? [] 47 | } 48 | 49 | static func save(_ totps: [String]) { 50 | UserDefaults.standard.set(totps, forKey: RECORD_KEY) 51 | updateRemote(totps) { (saved, error) in 52 | if let error = error { 53 | print(error) 54 | } else { 55 | print("***** remote saved *****") 56 | print(saved) 57 | } 58 | } 59 | } 60 | } 61 | 62 | 63 | fileprivate extension DataManager { 64 | 65 | static func createRecord(_ totps: [String], _ completion: @escaping ([String], NSError?) -> Void) { 66 | let record = CKRecord(recordType: RECORD_KEY, recordID: recordID) 67 | record.setValue(totps, forKey: RECORD_KEY) 68 | cloudDatabase.save(record) { (savedRecord, error) in 69 | DispatchQueue.main.async { 70 | completion(savedRecord?.object(forKey: RECORD_KEY) as? [String] ?? [], error as NSError?) 71 | } 72 | } 73 | } 74 | 75 | static func fetchRemote(_ completion: @escaping ([String], NSError?) -> Void) { 76 | cloudDatabase.fetch(withRecordID: recordID) { (record, error) in 77 | DispatchQueue.main.async { 78 | completion(record?.object(forKey: RECORD_KEY) as? [String] ?? [], error as NSError?) 79 | } 80 | } 81 | } 82 | 83 | static func updateRemote(_ totps: [String], _ completion: @escaping ([String], NSError?) -> Void) { 84 | cloudDatabase.fetch(withRecordID: recordID) { record, error in 85 | guard let record = record else { 86 | createRecord(totps, completion) 87 | return 88 | } 89 | record.setValue(totps, forKey: RECORD_KEY) 90 | self.cloudDatabase.save(record) { savedRecord, error in 91 | DispatchQueue.main.async { 92 | completion(savedRecord?.object(forKey: RECORD_KEY) as? [String] ?? [], error as NSError?) 93 | } 94 | } 95 | } 96 | } 97 | 98 | static func checkLoginStatus(_ handler: @escaping (_ islogged: Bool) -> Void) { 99 | CKContainer.default().accountStatus{ accountStatus, error in 100 | if let error = error { 101 | print(error.localizedDescription) 102 | } 103 | DispatchQueue.main.async { 104 | handler(accountStatus == .available) 105 | } 106 | } 107 | } 108 | 109 | static func showAlert() { 110 | // let alert = UIAlertController(title: "iCloud", message: "iCloud is unavailable, please login and try again if you need sync feature. or click [Cancel] for local usage", preferredStyle: .alert) 111 | // let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in 112 | // UserDefaults.standard.set(true, forKey: ALERT_OFF) 113 | // } 114 | // let settings = UIAlertAction(title: "Settings", style: .default) { _ in 115 | // guard let url = URL(string:"App-Prefs:root=General") else { return } 116 | // UIApplication.shared.openURL(url) 117 | // } 118 | // alert.addAction(cancel) 119 | // alert.addAction(settings) 120 | // UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: true) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /MinaOTP-MAC/ScanWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScanWindow.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/17. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import CoreImage 11 | import Foundation 12 | 13 | 14 | class ScanWindow: NSWindow, NSWindowDelegate{ 15 | 16 | let imgView = NSImageView.init() 17 | weak var scanDelegate : ScanWindowDelegate? 18 | 19 | override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { 20 | super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) 21 | config() 22 | self.delegate = self 23 | } 24 | 25 | private func config() { 26 | self.center() 27 | self.toolbar?.isVisible = false 28 | 29 | self.styleMask = [.titled, .closable, .resizable] 30 | self.title = NSLocalizedString("scan_title", comment: "") 31 | 32 | self.standardWindowButton(.miniaturizeButton)?.isHidden = true 33 | self.standardWindowButton(.zoomButton)?.isHidden = true 34 | 35 | self.isOpaque = false 36 | self.backgroundColor = NSColor.clear 37 | self.titlebarAppearsTransparent = true 38 | self.backgroundColor = NSColor(calibratedRed: 0.0, green: 0.0, blue: 0.0, alpha: 0.1) 39 | self.isMovableByWindowBackground = true 40 | 41 | imgView.wantsLayer = true 42 | imgView.translatesAutoresizingMaskIntoConstraints = false 43 | imgView.layer?.backgroundColor = NSColor.clear.cgColor 44 | imgView.image = NSImage.init(named: "image_normal") 45 | imgView.imageAlignment = .alignCenter 46 | imgView.imageScaling = .scaleProportionallyUpOrDown 47 | 48 | self.contentView?.addSubview(imgView) 49 | 50 | let heigtConstraint = NSLayoutConstraint(item: imgView, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.contentView, attribute:NSLayoutConstraint.Attribute.height, multiplier: 1.0, constant: 0) 51 | let weightConstraint = NSLayoutConstraint(item: imgView, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.contentView, attribute:NSLayoutConstraint.Attribute.width, multiplier: 1.0, constant: 0) 52 | 53 | let leftConstraint = NSLayoutConstraint(item: imgView, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.contentView, attribute:NSLayoutConstraint.Attribute.left, multiplier: 1.0, constant: 0) 54 | let topConstraint = NSLayoutConstraint(item: imgView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.contentView, attribute:NSLayoutConstraint.Attribute.top, multiplier: 1.0, constant: 0) 55 | self.contentView?.addConstraints([leftConstraint, topConstraint, heigtConstraint, weightConstraint]) 56 | 57 | } 58 | 59 | func windowDidMove(_ notification: Notification) { 60 | print("windowDidMove") 61 | 62 | let screenRect = NSScreen.main?.visibleFrame 63 | let dif = (screenRect?.size.height)!-self.frame.origin.y-self.frame.size.height+23 64 | 65 | let cgImage = CGWindowListCreateImage(CGRect(x: self.frame.origin.x, y: dif, width: self.frame.size.width, height: self.frame.size.height), .optionOnScreenBelowWindow, CGWindowID(self.windowNumber), .bestResolution) 66 | let detector = CIDetector.init(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy:CIDetectorAccuracyHigh]) 67 | let ciImage = CIImage.init(cgImage: cgImage!) 68 | 69 | let feature = detector?.features(in: ciImage) 70 | if (feature?.count)! > 0 { 71 | let f = feature?.first as! CIQRCodeFeature 72 | if f.messageString?.contains("otpauth://totp/") == false || f.messageString?.contains("secret=") == false || f.messageString?.contains("issuer=") == false{ 73 | imgView.image = NSImage.init(named: "image_normal") 74 | }else{ 75 | imgView.image = NSImage.init(named: "image_selected") 76 | self.delegate = nil 77 | self.title = NSLocalizedString("scan_qr_success", comment: "") 78 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { 79 | self.scanDelegate?.scanSuccess(code: f.messageString!) 80 | self.close() 81 | } 82 | } 83 | }else{ 84 | imgView.image = NSImage.init(named: "image_normal") 85 | } 86 | } 87 | func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize { 88 | var toHeight = frameSize.height 89 | if toHeight > 700 { 90 | toHeight = 700 91 | } 92 | if toHeight < 300 { 93 | toHeight = 300 94 | } 95 | return NSSize.init(width: toHeight, height: toHeight) 96 | } 97 | } 98 | 99 | protocol ScanWindowDelegate: class { 100 | func scanSuccess(code:String) 101 | } 102 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Magnet/KeyTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyTransformer.swift 3 | // Magnet 4 | // 5 | // Created by 古林俊佑 on 2016/06/18. 6 | // Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Carbon 11 | 12 | public final class KeyTransformer {} 13 | 14 | // MARK: - Cocoa & Carbon 15 | public extension KeyTransformer { 16 | static func cocoaFlags(from carbonFlags: Int) -> NSEvent.ModifierFlags { 17 | var cocoaFlags: NSEvent.ModifierFlags = NSEvent.ModifierFlags(rawValue: 0) 18 | 19 | if (carbonFlags & cmdKey) != 0 { 20 | cocoaFlags.insert(.command) 21 | } 22 | if (carbonFlags & optionKey) != 0 { 23 | cocoaFlags.insert(.option) 24 | } 25 | if (carbonFlags & controlKey) != 0 { 26 | cocoaFlags.insert(.control) 27 | } 28 | if (carbonFlags & shiftKey) != 0 { 29 | cocoaFlags.insert(.shift) 30 | } 31 | 32 | return cocoaFlags 33 | } 34 | 35 | static func carbonFlags(from cocoaFlags: NSEvent.ModifierFlags) -> Int { 36 | var carbonFlags: Int = 0 37 | 38 | if cocoaFlags.contains(.command) { 39 | carbonFlags |= cmdKey 40 | } 41 | if cocoaFlags.contains(.option) { 42 | carbonFlags |= optionKey 43 | } 44 | if cocoaFlags.contains(.control) { 45 | carbonFlags |= controlKey 46 | } 47 | if cocoaFlags.contains(.shift) { 48 | carbonFlags |= shiftKey 49 | } 50 | 51 | return carbonFlags 52 | } 53 | 54 | static func supportedCarbonFlags(_ carbonFlags: Int) -> Bool { 55 | return cocoaFlags(from: carbonFlags).rawValue != 0 56 | } 57 | 58 | static func supportedCocoaFlags(_ cocoaFlogs: NSEvent.ModifierFlags) -> Bool { 59 | return carbonFlags(from: cocoaFlogs) != 0 60 | } 61 | 62 | static func singleCarbonFlags(_ carbonFlags: Int) -> Bool { 63 | let commandSelected = (carbonFlags & cmdKey) != 0 64 | let optionSelected = (carbonFlags & optionKey) != 0 65 | let controlSelected = (carbonFlags & controlKey) != 0 66 | let shiftSelected = (carbonFlags & shiftKey) != 0 67 | let hash = commandSelected.intValue + optionSelected.intValue + controlSelected.intValue + shiftSelected.intValue 68 | return hash == 1 69 | } 70 | 71 | static func singleCocoaFlags(_ cocoaFlags: NSEvent.ModifierFlags) -> Bool { 72 | let commandSelected = cocoaFlags.contains(.command) 73 | let optionSelected = cocoaFlags.contains(.option) 74 | let controlSelected = cocoaFlags.contains(.control) 75 | let shiftSelected = cocoaFlags.contains(.shift) 76 | let hash = commandSelected.intValue + optionSelected.intValue + controlSelected.intValue + shiftSelected.intValue 77 | return hash == 1 78 | } 79 | } 80 | 81 | // MARK: - Function 82 | public extension KeyTransformer { 83 | static func containsFunctionKey(_ keyCode: Int) -> Bool { 84 | switch keyCode { 85 | case kVK_F1: fallthrough 86 | case kVK_F2: fallthrough 87 | case kVK_F3: fallthrough 88 | case kVK_F4: fallthrough 89 | case kVK_F5: fallthrough 90 | case kVK_F6: fallthrough 91 | case kVK_F7: fallthrough 92 | case kVK_F8: fallthrough 93 | case kVK_F9: fallthrough 94 | case kVK_F10: fallthrough 95 | case kVK_F11: fallthrough 96 | case kVK_F12: fallthrough 97 | case kVK_F13: fallthrough 98 | case kVK_F14: fallthrough 99 | case kVK_F15: fallthrough 100 | case kVK_F16: fallthrough 101 | case kVK_F17: fallthrough 102 | case kVK_F18: fallthrough 103 | case kVK_F19: fallthrough 104 | case kVK_F20: 105 | return true 106 | default: 107 | return false 108 | } 109 | } 110 | } 111 | 112 | // MARK: - Modifiers 113 | public extension KeyTransformer { 114 | static func modifiersToString(_ carbonModifiers: Int) -> [String] { 115 | var strings = [String]() 116 | 117 | if (carbonModifiers & cmdKey) != 0 { 118 | strings.append("⌘") 119 | } 120 | if (carbonModifiers & optionKey) != 0 { 121 | strings.append("⌥") 122 | } 123 | if (carbonModifiers & controlKey) != 0 { 124 | strings.append("⌃") 125 | } 126 | if (carbonModifiers & shiftKey) != 0 { 127 | strings.append("⇧") 128 | } 129 | 130 | return strings 131 | } 132 | 133 | static func modifiersToString(_ cocoaModifiers: NSEvent.ModifierFlags) -> [String] { 134 | var strings = [String]() 135 | 136 | if cocoaModifiers.contains(.command) { 137 | strings.append("⌘") 138 | } 139 | if cocoaModifiers.contains(.option) { 140 | strings.append("⌥") 141 | } 142 | if cocoaModifiers.contains(.control) { 143 | strings.append("⌃") 144 | } 145 | if cocoaModifiers.contains(.shift) { 146 | strings.append("⇧") 147 | } 148 | 149 | return strings 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tools.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/1. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | import CoreImage 12 | 13 | 14 | class CustomFlatButton{ 15 | func customFlatButton(frame: CGRect, title: String) -> FlatButton { 16 | let button = FlatButton.init(frame: frame) 17 | button.title = title 18 | button.font = NSFont.boldSystemFont(ofSize: 12) 19 | button.setButtonType(.momentaryChange) 20 | button.textColor = NSColor.white 21 | button.cornerRadius = 4 22 | button.borderColor = NSColor.mainColor 23 | button.borderWidth = 1 24 | button.activeBorderColor = NSColor.mainColor 25 | button.buttonColor = NSColor.mainColor 26 | button.focusRingType = .none 27 | 28 | return button 29 | } 30 | } 31 | 32 | extension NSColor { 33 | 34 | class var mainColor: NSColor { 35 | let color = NSColor.systemBlue 36 | // let color = NSColor.init(red: 0.00, green: 0.56, blue: 0.98, alpha: 1.00) 37 | return color 38 | } 39 | 40 | convenience init(hex: String) { 41 | let scanner = Scanner(string: hex) 42 | scanner.scanLocation = 0 43 | 44 | var rgbValue: UInt64 = 0 45 | 46 | scanner.scanHexInt64(&rgbValue) 47 | 48 | let r = (rgbValue & 0xff0000) >> 16 49 | let g = (rgbValue & 0xff00) >> 8 50 | let b = rgbValue & 0xff 51 | 52 | self.init( 53 | red: CGFloat(r) / 0xff, 54 | green: CGFloat(g) / 0xff, 55 | blue: CGFloat(b) / 0xff, alpha: 1 56 | ) 57 | } 58 | } 59 | 60 | extension NSBezierPath { 61 | 62 | public var cgPath: CGPath { 63 | let path = CGMutablePath() 64 | var points = [CGPoint](repeating: .zero, count: 3) 65 | 66 | for i in 0 ..< self.elementCount { 67 | let type = self.element(at: i, associatedPoints: &points) 68 | switch type { 69 | case .moveTo: 70 | path.move(to: points[0]) 71 | case .lineTo: 72 | path.addLine(to: points[0]) 73 | case .curveTo: 74 | path.addCurve(to: points[2], control1: points[0], control2: points[1]) 75 | case .closePath: 76 | path.closeSubpath() 77 | @unknown default: 78 | fatalError("Unknown path type.") 79 | } 80 | } 81 | 82 | return path 83 | } 84 | } 85 | 86 | class Tools: NSObject { 87 | 88 | public func showAlert(message: String) { 89 | 90 | let alert: NSAlert = NSAlert() 91 | alert.messageText = message 92 | alert.addButton(withTitle: NSLocalizedString("confirm", comment: "")) 93 | alert.alertStyle = NSAlert.Style.informational 94 | alert.runModal() 95 | } 96 | 97 | public func generateTextField(frame: NSRect, textColor: NSColor, text: String, font: CGFloat) -> NSTextField { 98 | let lab = NSTextField.init(frame: frame) 99 | lab.wantsLayer = true 100 | lab.textColor = textColor 101 | lab.stringValue = text 102 | lab.isBordered = false 103 | lab.backgroundColor = NSColor.clear 104 | lab.font = NSFont.systemFont(ofSize: font) 105 | // lab.layer?.borderWidth = 1 106 | // lab.layer?.borderColor = NSColor.orange.cgColor 107 | return lab 108 | } 109 | 110 | func totpDictionaryFormat(code: String) -> Dictionary { 111 | 112 | let url = code 113 | let params = NSMutableDictionary() 114 | if url.contains("otpauth://totp/") == false || url.contains("issuer") == false || url.contains("secret") == false{ 115 | return params as! Dictionary 116 | } 117 | var index = url.firstIndex(of: "?") 118 | if index == nil { 119 | return params as! Dictionary 120 | } 121 | // 获取otpauth://totp/部分 并替换为remark 122 | let otpauth = url.prefix(upTo: index!) 123 | let otpauthIndx = otpauth.index(otpauth.startIndex, offsetBy: "otpauth://totp/".count) 124 | let remark = otpauth.suffix(from: Optional.init(otpauthIndx)!) 125 | params.setValue(remark, forKey: "remark") 126 | // 获取问号后面部分的字符串 127 | index = url.index(index!, offsetBy: 1) 128 | let parametersString = url.suffix(from: index!) 129 | // 解析参数 130 | let urlComponents = parametersString.components(separatedBy: "&") 131 | for keyValuePair in urlComponents { 132 | let pairComponents = keyValuePair.components(separatedBy: "=") 133 | let key = pairComponents.first 134 | let value = pairComponents.last 135 | if key == nil || value == nil { 136 | continue 137 | } 138 | params.setValue(value, forKey: key!) 139 | } 140 | return params as! Dictionary 141 | } 142 | 143 | func totpStringFormat(remark: String, issuer:String, secret:String) -> String { 144 | return "otpauth://totp/\(remark)?secret=\(secret)&issuer=\(issuer)" 145 | } 146 | 147 | } 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /MinaOTP-MAC/EditPopoverViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditPopoverViewController.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/6. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class EditPopoverViewController: NSViewController, NSTextFieldDelegate { 12 | 13 | let cancelButton = CustomFlatButton().customFlatButton(frame: NSRect(x: 12, y: 12, width: 48, height: 24), title: NSLocalizedString("cancel", comment: "")) 14 | let saveButton = CustomFlatButton().customFlatButton(frame: NSRect(x: 140, y: 12, width: 48, height: 24), title: NSLocalizedString("save", comment: "")) 15 | var editRow:Int = -1 16 | let textColor: NSColor = .labelColor //NSColor(calibratedRed: 0, green: 0, blue: 0, alpha: 0.8) 17 | 18 | override func loadView() { 19 | self.view = NSView(frame: CGRect(x: 0, y: 0, width: 200, height: 250)) 20 | } 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | self.config() 25 | let allItems = DataManager.get() 26 | let totpDic = Tools().totpDictionaryFormat(code: allItems[editRow]) 27 | remarkTextField.stringValue = (totpDic["remark"] as? String)! 28 | issuerTextField.stringValue = (totpDic["issuer"] as? String)! 29 | secretTextFiled.stringValue = (totpDic["secret"] as? String)! 30 | } 31 | override func awakeFromNib() { 32 | 33 | } 34 | private func config() { 35 | self.view.addSubview(cancelButton) 36 | self.view.addSubview(saveButton) 37 | self.view.addSubview(self.remarkTitleTextField) 38 | self.view.addSubview(self.remarkTextField) 39 | self.view.addSubview(self.issuerTitleTextField) 40 | self.view.addSubview(self.issuerTextField) 41 | self.view.addSubview(self.secretTitleTextField) 42 | self.view.addSubview(self.secretTextFiled) 43 | 44 | cancelButton.target = self 45 | saveButton.target = self 46 | cancelButton.action = #selector(self.cancelButtonAction) 47 | saveButton.action = #selector(self.saveButtonAction(button:)) 48 | 49 | saveButton.isEnabled = false 50 | } 51 | lazy var remarkTitleTextField: NSTextField = { 52 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 220, width: 180, height: 18), textColor: textColor, text: NSLocalizedString("remark_placeholder", comment: ""), font: 10) 53 | lab.isEditable = false 54 | lab.delegate = self 55 | return lab 56 | }() 57 | lazy var remarkTextField: NSTextField = { 58 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 185, width: 180, height: 35), textColor: textColor, text: "", font: 12) 59 | lab.delegate = self 60 | lab.isBordered = true 61 | lab.focusRingType = .none 62 | lab.isBezeled = true 63 | lab.bezelStyle = .squareBezel 64 | lab.layer?.borderWidth = 1 65 | lab.layer?.borderColor = NSColor.mainColor.cgColor 66 | return lab 67 | }() 68 | lazy var issuerTitleTextField: NSTextField = { 69 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 160, width: 180, height: 18), textColor: textColor, text: NSLocalizedString("issuer_placeholder", comment: ""), font: 10) 70 | lab.isEditable = false 71 | return lab 72 | }() 73 | lazy var issuerTextField: NSTextField = { 74 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 125, width: 180, height: 35), textColor: textColor, text: "", font: 12) 75 | lab.delegate = self 76 | lab.delegate = self 77 | lab.isBordered = true 78 | lab.focusRingType = .none 79 | lab.isBezeled = true 80 | lab.bezelStyle = .squareBezel 81 | lab.layer?.borderWidth = 1 82 | lab.layer?.borderColor = NSColor.mainColor.cgColor 83 | return lab 84 | }() 85 | lazy var secretTitleTextField: NSTextField = { 86 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 100, width: 180, height: 18), textColor: textColor, text: NSLocalizedString("secret_placeholder", comment: ""), font: 10) 87 | lab.isEditable = false 88 | lab.delegate = self 89 | return lab 90 | }() 91 | lazy var secretTextFiled: NSTextField = { 92 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 55, width: 180, height: 45), textColor: textColor, text: "", font: 12) 93 | lab.delegate = self 94 | lab.isBordered = true 95 | lab.focusRingType = .none 96 | lab.isBezeled = true 97 | lab.bezelStyle = .squareBezel 98 | lab.layer?.borderWidth = 1 99 | lab.layer?.borderColor = NSColor.mainColor.cgColor 100 | return lab 101 | }() 102 | func controlTextDidChange(_ obj: Notification) { 103 | if self.remarkTextField.stringValue.count == 0 || self.issuerTextField.stringValue.count == 0 || self.secretTextFiled.stringValue.count == 0{ 104 | saveButton.isEnabled = false 105 | }else{ 106 | print("可以输入了") 107 | saveButton.isEnabled = true 108 | } 109 | 110 | } 111 | @objc func cancelButtonAction() { 112 | print("cancelButtonAction") 113 | NotificationCenter.default.post(name: NSNotification.Name("reloadData"), object: self, userInfo: ["type":"edit_cancel"]) 114 | } 115 | 116 | @objc func saveButtonAction(button: NSButton) { 117 | print("saveButtonAction") 118 | let otp = Tools().totpStringFormat(remark: self.remarkTextField.stringValue, issuer: self.issuerTextField.stringValue, secret: self.secretTextFiled.stringValue) 119 | 120 | // 将数据保存到UserDefaults 121 | var allItems = DataManager.get() 122 | allItems.remove(at: editRow) 123 | allItems.insert(otp, at: editRow) 124 | DataManager.save(allItems) 125 | NotificationCenter.default.post(name: NSNotification.Name("reloadData"), object: self, userInfo: ["type":"edit_save"]) 126 | } 127 | 128 | override func viewWillAppear() { 129 | super.viewWillAppear() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /MinaOTP-MAC/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/7/31. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Carbon 11 | import Foundation 12 | 13 | @NSApplicationMain 14 | 15 | class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSMenuDelegate{ 16 | 17 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) 18 | let popover = NSPopover() 19 | let popoverVC = PopoverViewController() 20 | 21 | struct GitHubInfoModel:Codable { 22 | var tag_name:String 23 | var name:String 24 | var html_url:String 25 | } 26 | 27 | func applicationDidFinishLaunching(_ aNotification: Notification) { 28 | 29 | if let button = statusItem.button{ 30 | button.image = NSImage(named: "close") 31 | button.image?.size = NSSize(width: 20, height: 20) 32 | button.action = #selector(AppDelegate.mouseDownAction) 33 | button.sendAction(on: [.leftMouseDown, .rightMouseDown]) 34 | button.target = self 35 | button.toolTip = NSLocalizedString("tool_tip", comment: "") 36 | } 37 | statusItem.highlightMode = true 38 | 39 | popover.behavior = .transient 40 | //popover.appearance = NSAppearance.init(named: .vibrantLight) 41 | popover.contentViewController = popoverVC 42 | popover.delegate = self 43 | NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { (event) in 44 | if event.type == .leftMouseDown || event.type == .rightMouseDown{ 45 | self.statusItem.button?.state = NSControl.StateValue.off 46 | if self.popover.isShown { 47 | self.popover.close() 48 | } 49 | } 50 | } 51 | addHotKey() 52 | } 53 | @objc func mouseDownAction() { 54 | let event = NSApp.currentEvent 55 | if event?.type == .leftMouseDown{ 56 | statusItem.button?.state = NSControl.StateValue.off 57 | leftMouseDownAction() 58 | }else if event?.type == .rightMouseDown{ 59 | rightMouseDownAction() 60 | }else{ 61 | print("谁点的") 62 | } 63 | } 64 | func leftMouseDownAction() { 65 | popover.show(relativeTo: (statusItem.button?.bounds)!, of: (statusItem.button)!, preferredEdge: NSRectEdge.maxY) 66 | } 67 | func rightMouseDownAction() { 68 | let menu = NSMenu.init() 69 | menu.delegate = self 70 | menu.addItem(withTitle: NSLocalizedString("export_help", comment: ""), action: #selector(exportDemoAction), keyEquivalent: "") 71 | menu.addItem(withTitle: NSLocalizedString("check_releases", comment: ""), action: #selector(checkReleases), keyEquivalent: "") 72 | menu.addItem(withTitle: NSLocalizedString("help", comment: ""), action: #selector(helpAction), keyEquivalent: "") 73 | menu.addItem(withTitle: NSLocalizedString("exit", comment: ""), action: #selector(exitAction), keyEquivalent: "") 74 | statusItem.popUpMenu(menu) 75 | } 76 | @objc func helpAction() { 77 | NSWorkspace.shared.open(NSURL.init(string: "https://github.com/MinaOTP/MinaOTP-MAC")! as URL) 78 | } 79 | @objc func exitAction() { 80 | NSApp.terminate(nil) 81 | } 82 | @objc func checkReleases() { 83 | let requestUrl = NSURL.init(string: "https://api.github.com/repos/MinaOTP/MinaOTP-MAC/releases/latest") 84 | let request = URLRequest.init(url: requestUrl! as URL) 85 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 86 | 87 | DispatchQueue.main.async { 88 | self.dealRequestData(data: data!) 89 | } 90 | } 91 | task.resume() 92 | } 93 | func dealRequestData (data: Data){ 94 | let decoder = JSONDecoder() 95 | do { 96 | let model = try decoder.decode(GitHubInfoModel.self, from: data as Data) 97 | print(model.tag_name) 98 | let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String 99 | print(version) 100 | 101 | if version != model.tag_name { 102 | Tools().showAlert(message: NSLocalizedString("has_new_version", comment: "")) 103 | }else{ 104 | Tools().showAlert(message: NSLocalizedString("no_new_version", comment: "")) 105 | } 106 | 107 | } catch { 108 | print("error") 109 | } 110 | } 111 | @objc func exportDemoAction() { 112 | let savePanel = NSSavePanel() 113 | savePanel.title = NSLocalizedString("export_title", comment: "") 114 | savePanel.nameFieldStringValue = "minaOTP_example.json" 115 | savePanel.canCreateDirectories = true 116 | savePanel.isExtensionHidden = false 117 | let i = savePanel.runModal() 118 | if i == NSApplication.ModalResponse.OK { 119 | let temArray = [["remark":"remark_value", "secret":"secret_value", "issuer":"issuer_value"], ["remark":"remark_value", "secret":"secret_value", "issuer":"issuer_value"]] 120 | let temJsonData = try! JSONSerialization.data(withJSONObject: temArray, options: .prettyPrinted) 121 | let temJsonStr = String.init(data: temJsonData, encoding: .utf8) 122 | do { 123 | try temJsonStr?.write(to: savePanel.url!, atomically: true, encoding: .utf8) 124 | Tools().showAlert(message: NSLocalizedString("export_sucess", comment: "")) 125 | } catch { 126 | Tools().showAlert(message: NSLocalizedString("export_failure", comment: "")) 127 | } 128 | } 129 | 130 | } 131 | func popoverWillShow(_ notification: Notification) { 132 | NotificationCenter.default.post(name: NSNotification.Name("reloadData"), object: self, userInfo: ["type":"reload"]) 133 | statusItem.button?.image = NSImage.init(named: "open") 134 | statusItem.button?.image?.size = NSSize(width: 20, height: 20) 135 | 136 | } 137 | func popoverDidClose(_ notification: Notification) { 138 | NotificationCenter.default.post(name: NSNotification.Name("reloadData"), object: self, userInfo: ["type":"close"]) 139 | statusItem.button?.image = NSImage.init(named: "close") 140 | statusItem.button?.image?.size = NSSize(width: 20, height: 20) 141 | } 142 | 143 | func addHotKey() { 144 | 145 | guard let keyCombo = KeyCombo(doubledCocoaModifiers: .command) else { return } 146 | let hotKey = HotKey(identifier: "CommandDoubleTap", 147 | keyCombo: keyCombo, 148 | target: self, 149 | action: #selector(AppDelegate.tappedDoubleCommandKey)) 150 | hotKey.register() 151 | 152 | 153 | } 154 | @objc func tappedDoubleCommandKey() { 155 | print("11111111111111") 156 | if self.popover.isShown { 157 | self.popover.close() 158 | }else{ 159 | leftMouseDownAction() 160 | } 161 | } 162 | func removeHotKey() { 163 | 164 | } 165 | 166 | func applicationWillTerminate(_ aNotification: Notification) { 167 | // Insert code here to tear down your application 168 | } 169 | 170 | 171 | } 172 | 173 | -------------------------------------------------------------------------------- /MinaOTP-MAC/Magnet/HotKeyCenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotKeyCenter.swift 3 | // Magnet 4 | // 5 | // Created by 古林俊佑 on 2016/03/09. 6 | // Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Carbon 11 | 12 | public final class HotKeyCenter { 13 | 14 | // MARK: - Properties 15 | public static let shared = HotKeyCenter() 16 | fileprivate var hotKeys = [String: HotKey]() 17 | fileprivate var hotKeyMap = [NSNumber: HotKey]() 18 | fileprivate var hotKeyCount: UInt32 = 0 19 | 20 | fileprivate var tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0) 21 | fileprivate var multiModifiers = false 22 | 23 | // MARK: - Initialize 24 | init() { 25 | installEventHandler() 26 | observeApplicationTerminate() 27 | } 28 | 29 | } 30 | 31 | // MARK: - Register & Unregister 32 | public extension HotKeyCenter { 33 | func register(with hotKey: HotKey) -> Bool { 34 | guard !hotKeys.keys.contains(hotKey.identifier) else { return false } 35 | guard !hotKeys.values.contains(hotKey) else { return false } 36 | 37 | if !hotKey.keyCombo.doubledModifiers { 38 | // Normal HotKey 39 | let hotKeyId = EventHotKeyID(signature: UTGetOSTypeFromString("Magnet" as CFString), id: hotKeyCount) 40 | var carbonHotKey: EventHotKeyRef? = nil 41 | let error = RegisterEventHotKey(UInt32(hotKey.keyCombo.keyCode), 42 | UInt32(hotKey.keyCombo.modifiers), 43 | hotKeyId, 44 | GetEventDispatcherTarget(), 45 | 0, 46 | &carbonHotKey) 47 | if error != 0 { return false } 48 | 49 | hotKey.hotKeyId = hotKeyId.id 50 | hotKey.hotKeyRef = carbonHotKey 51 | } 52 | 53 | let kId = NSNumber(value: hotKeyCount as UInt32) 54 | hotKeyMap[kId] = hotKey 55 | hotKeyCount += 1 56 | 57 | hotKeys[hotKey.identifier] = hotKey 58 | 59 | return true 60 | } 61 | 62 | func unregister(with hotKey: HotKey) { 63 | guard hotKeys.values.contains(hotKey) else { return } 64 | 65 | if !hotKey.keyCombo.doubledModifiers { 66 | // Notmal HotKey 67 | guard let carbonHotKey = hotKey.hotKeyRef else { return } 68 | UnregisterEventHotKey(carbonHotKey) 69 | } 70 | 71 | hotKeys.removeValue(forKey: hotKey.identifier) 72 | 73 | hotKey.hotKeyId = nil 74 | hotKey.hotKeyRef = nil 75 | 76 | hotKeyMap 77 | .filter { $1 == hotKey } 78 | .map { $0.0 } 79 | .forEach { hotKeyMap.removeValue(forKey: $0) } 80 | } 81 | 82 | func unregisterHotKey(with identifier: String) { 83 | guard let hotKey = hotKeys[identifier] else { return } 84 | unregister(with: hotKey) 85 | } 86 | 87 | func unregisterAll() { 88 | hotKeys.forEach { unregister(with: $1) } 89 | } 90 | } 91 | 92 | // MARK: - Terminate 93 | extension HotKeyCenter { 94 | private func observeApplicationTerminate() { 95 | NotificationCenter.default.addObserver(self, 96 | selector: #selector(HotKeyCenter.applicationWillTerminate), 97 | name: NSApplication.willTerminateNotification, 98 | object: nil) 99 | } 100 | 101 | @objc func applicationWillTerminate() { 102 | unregisterAll() 103 | } 104 | } 105 | 106 | // MARK: - HotKey Events 107 | private extension HotKeyCenter { 108 | func installEventHandler() { 109 | // Press HotKey Event 110 | var pressedEventType = EventTypeSpec() 111 | pressedEventType.eventClass = OSType(kEventClassKeyboard) 112 | pressedEventType.eventKind = OSType(kEventHotKeyPressed) 113 | InstallEventHandler(GetEventDispatcherTarget(), { (_, inEvent, _) -> OSStatus in 114 | return HotKeyCenter.shared.sendCarbonEvent(inEvent!) 115 | }, 1, &pressedEventType, nil, nil) 116 | 117 | // Press Modifiers Event 118 | let mask = CGEventMask((1 << CGEventType.flagsChanged.rawValue)) 119 | let event = CGEvent.tapCreate(tap: .cghidEventTap, 120 | place: .headInsertEventTap, 121 | options: .listenOnly, 122 | eventsOfInterest: mask, 123 | callback: { (_, _, event, _) in return HotKeyCenter.shared.sendModifiersEvent(event) }, 124 | userInfo: nil) 125 | if event == nil { return } 126 | let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, event!, 0) 127 | CFRunLoopAddSource(CFRunLoopGetCurrent(), source, CFRunLoopMode.commonModes) 128 | CGEvent.tapEnable(tap: event!, enable: true) 129 | } 130 | 131 | func sendCarbonEvent(_ event: EventRef) -> OSStatus { 132 | assert(Int(GetEventClass(event)) == kEventClassKeyboard, "Unknown event class") 133 | 134 | var hotKeyId = EventHotKeyID() 135 | let error = GetEventParameter(event, 136 | EventParamName(kEventParamDirectObject), 137 | EventParamName(typeEventHotKeyID), 138 | nil, 139 | MemoryLayout.size, 140 | nil, 141 | &hotKeyId) 142 | 143 | if error != 0 { return error } 144 | 145 | assert(hotKeyId.signature == UTGetOSTypeFromString("Magnet" as CFString), "Invalid hot key id") 146 | 147 | let kId = NSNumber(value: hotKeyId.id as UInt32) 148 | let hotKey = hotKeyMap[kId] 149 | 150 | switch GetEventKind(event) { 151 | case EventParamName(kEventHotKeyPressed): 152 | hotKeyDown(hotKey) 153 | default: 154 | assert(false, "Unknown event kind") 155 | } 156 | 157 | return noErr 158 | } 159 | 160 | func hotKeyDown(_ hotKey: HotKey?) { 161 | guard let hotKey = hotKey else { return } 162 | hotKey.invoke() 163 | } 164 | } 165 | 166 | // MARK: - Double Tap Modifier Event 167 | private extension HotKeyCenter { 168 | func sendModifiersEvent(_ event: CGEvent) -> Unmanaged? { 169 | let flags = event.flags 170 | 171 | let commandTapped = flags.contains(.maskCommand) 172 | let shiftTapped = flags.contains(.maskShift) 173 | let controlTapped = flags.contains(.maskControl) 174 | let altTapped = flags.contains(.maskAlternate) 175 | 176 | // Only one modifier key 177 | let totalHash = commandTapped.intValue + altTapped.intValue + shiftTapped.intValue + controlTapped.intValue 178 | if totalHash == 0 { return Unmanaged.passUnretained(event) } 179 | if totalHash > 1 { 180 | multiModifiers = true 181 | return Unmanaged.passUnretained(event) 182 | } 183 | if multiModifiers { 184 | multiModifiers = false 185 | return Unmanaged.passUnretained(event) 186 | } 187 | 188 | if (tappedModifierKey.contains(.command) && commandTapped) || 189 | (tappedModifierKey.contains(.shift) && shiftTapped) || 190 | (tappedModifierKey.contains(.control) && controlTapped) || 191 | (tappedModifierKey.contains(.option) && altTapped) { 192 | doubleTapped(with: KeyTransformer.carbonFlags(from: tappedModifierKey)) 193 | tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0) 194 | } else { 195 | if commandTapped { 196 | tappedModifierKey = .command 197 | } else if shiftTapped { 198 | tappedModifierKey = .shift 199 | } else if controlTapped { 200 | tappedModifierKey = .control 201 | } else if altTapped { 202 | tappedModifierKey = .option 203 | } else { 204 | tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0) 205 | } 206 | } 207 | 208 | // Clean Flag 209 | let delay = 0.3 * Double(NSEC_PER_SEC) 210 | let time = DispatchTime.now() + Double(Int64(delay)) / Double(NSEC_PER_SEC) 211 | DispatchQueue.main.asyncAfter(deadline: time, execute: { [weak self] in 212 | self?.tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0) 213 | }) 214 | 215 | return Unmanaged.passUnretained(event) 216 | } 217 | 218 | func doubleTapped(with key: Int) { 219 | hotKeys.values 220 | .filter { $0.keyCombo.doubledModifiers && $0.keyCombo.modifiers == key } 221 | .forEach { $0.invoke() } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /MinaOTP-MAC/AddWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddWindowController.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/7. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class AddWindow: NSWindow, NSTextFieldDelegate, ScanWindowDelegate{ 12 | 13 | let cancelButton = CustomFlatButton().customFlatButton(frame: NSRect(x: 12, y: 12, width: 48, height: 24), title: NSLocalizedString("cancel", comment: "")) 14 | let saveButton = CustomFlatButton().customFlatButton(frame: NSRect(x: 420, y: 12, width: 48, height: 24), title: NSLocalizedString("save", comment: "")) 15 | let chooseButton = CustomFlatButton().customFlatButton(frame: NSRect(x: 318, y: 290, width: 150, height: 24), title: NSLocalizedString("choose_qr_image_btn_title", comment: "")) 16 | 17 | let scanButton = CustomFlatButton().customFlatButton(frame: NSRect(x: 12, y: 290, width: 150, height: 24), title: NSLocalizedString("scan_qr_image_btn_title", comment: "")) 18 | 19 | 20 | let textColor = NSColor.labelColor//.withAlphaComponent(0.8) 21 | 22 | override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { 23 | super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) 24 | config() 25 | } 26 | 27 | private func config() { 28 | self.center() 29 | self.toolbar?.isVisible = false 30 | 31 | self.styleMask = [.titled, .closable] 32 | self.title = NSLocalizedString("add_title", comment: "") 33 | 34 | self.standardWindowButton(.miniaturizeButton)?.isHidden = true 35 | self.standardWindowButton(.zoomButton)?.isHidden = true 36 | 37 | self.isOpaque = false 38 | //self.backgroundColor = NSColor.clear 39 | self.titlebarAppearsTransparent = true 40 | //self.backgroundColor = NSColor(calibratedRed: 1.0, green: 1.0, blue: 1.0, alpha: 0.9) 41 | self.isMovableByWindowBackground = true 42 | 43 | self.contentView?.addSubview(cancelButton) 44 | self.contentView?.addSubview(saveButton) 45 | self.contentView?.addSubview(chooseButton) 46 | self.contentView?.addSubview(scanButton) 47 | self.contentView?.addSubview(self.remarkTextField) 48 | self.contentView?.addSubview(self.issuerTextField) 49 | self.contentView?.addSubview(self.secretTextFiled) 50 | self.contentView?.addSubview(self.remarkTitleTextField) 51 | self.contentView?.addSubview(self.issuerTitleTextField) 52 | self.contentView?.addSubview(self.secretTitleTextField) 53 | 54 | cancelButton.target = self 55 | saveButton.target = self 56 | chooseButton.target = self 57 | scanButton.target = self 58 | cancelButton.action = #selector(self.cancelButtonAction) 59 | chooseButton.action = #selector(self.chooseButtonAction) 60 | saveButton.action = #selector(self.saveButtonAction(button:)) 61 | scanButton.action = #selector(self.scanButtonAction) 62 | saveButton.isEnabled = false 63 | } 64 | // MARK: - Lazy 65 | lazy var remarkTitleTextField: NSTextField = { 66 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 270, width: 200, height: 18), textColor: textColor, text: NSLocalizedString("remark_placeholder", comment: ""), font: 12) 67 | lab.isEditable = false 68 | lab.delegate = self 69 | return lab 70 | }() 71 | lazy var remarkTextField: NSTextField = { 72 | let lab = Tools().generateTextField(frame: NSRect(x: 13, y: 230, width: 455, height: 40), textColor: textColor, text: "", font: 12) 73 | lab.delegate = self 74 | lab.isBordered = true 75 | lab.focusRingType = .none 76 | lab.isBezeled = true 77 | lab.bezelStyle = .squareBezel 78 | lab.layer?.borderWidth = 1 79 | lab.layer?.borderColor = NSColor.mainColor.cgColor 80 | return lab 81 | }() 82 | lazy var issuerTitleTextField: NSTextField = { 83 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 200, width: 200, height: 18), textColor: textColor, text: NSLocalizedString("issuer_placeholder", comment: ""), font: 12) 84 | lab.isEditable = false 85 | lab.delegate = self 86 | return lab 87 | }() 88 | lazy var issuerTextField: NSTextField = { 89 | let lab = Tools().generateTextField(frame: NSRect(x: 13, y: 160, width: 455, height: 40), textColor: textColor, text: "", font: 12) 90 | lab.delegate = self 91 | lab.isBordered = true 92 | lab.focusRingType = .none 93 | lab.isBezeled = true 94 | lab.bezelStyle = .squareBezel 95 | lab.layer?.borderWidth = 1 96 | lab.layer?.borderColor = NSColor.mainColor.cgColor 97 | return lab 98 | }() 99 | lazy var secretTitleTextField: NSTextField = { 100 | let lab = Tools().generateTextField(frame: NSRect(x: 12, y: 130, width: 200, height: 18), textColor: textColor, text: NSLocalizedString("secret_placeholder", comment: ""), font: 12) 101 | lab.isEditable = false 102 | lab.delegate = self 103 | return lab 104 | }() 105 | lazy var secretTextFiled: NSTextField = { 106 | let lab = Tools().generateTextField(frame: NSRect(x: 13, y: 90, width: 455, height: 40), textColor: textColor, text: "", font: 12) 107 | lab.delegate = self 108 | lab.isBordered = true 109 | lab.focusRingType = .none 110 | lab.isBezeled = true 111 | lab.bezelStyle = .squareBezel 112 | lab.layer?.borderWidth = 1 113 | lab.layer?.borderColor = NSColor.mainColor.cgColor 114 | return lab 115 | }() 116 | // MARK: - FUNC 117 | func controlTextDidChange(_ obj: Notification) { 118 | if self.remarkTextField.stringValue.count == 0 || self.issuerTextField.stringValue.count == 0 || self.secretTextFiled.stringValue.count == 0{ 119 | saveButton.isEnabled = false 120 | }else{ 121 | saveButton.isEnabled = true 122 | } 123 | 124 | } 125 | @objc func cancelButtonAction() { 126 | self.close() 127 | } 128 | @objc func scanButtonAction() { 129 | print("扫描") 130 | // ScanQRCodeOnScreen() 131 | let temWindow = ScanWindow.init(contentRect: NSRect(x: 0, y: 0, width: 300, height: 300), styleMask: .closable, backing: .buffered, defer: true) 132 | let windowVC = NSWindowController.init(window: temWindow) 133 | temWindow.scanDelegate = self 134 | windowVC.window?.makeKeyAndOrderFront(nil) 135 | NSApp.activate(ignoringOtherApps: true) 136 | } 137 | @objc func saveButtonAction(button: NSButton) { 138 | let otp = Tools().totpStringFormat(remark: self.remarkTextField.stringValue, issuer: self.issuerTextField.stringValue, secret: self.secretTextFiled.stringValue) 139 | 140 | // 将数据保存到UserDefaults 141 | var allItems = DataManager.get() 142 | allItems.append(otp) 143 | DataManager.save(allItems) 144 | ShowTips().showTip(message: NSLocalizedString("add_success_tip", comment: ""), view: self.contentView!) 145 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { 146 | self.close() 147 | } 148 | } 149 | 150 | @objc func chooseButtonAction() { 151 | let openPanel = NSOpenPanel.init() 152 | openPanel.prompt = NSLocalizedString("choose_title", comment: "") 153 | openPanel.allowedFileTypes = NSImage.imageTypes 154 | let code = openPanel.runModal() 155 | if code.rawValue == 1{ 156 | self.readQRCode(qrImage: CIImage.init(contentsOf: openPanel.url!)!) 157 | } 158 | } 159 | 160 | func readQRCode(qrImage: CIImage) { 161 | 162 | let context = CIContext.init(options: nil) 163 | let detector = CIDetector.init(ofType: CIDetectorTypeQRCode, context: context, options: [CIDetectorAccuracy:CIDetectorAccuracyHigh]) 164 | let features = detector?.features(in: qrImage) 165 | if features?.count == 1{ 166 | let feature: CIQRCodeFeature = features?.first as! CIQRCodeFeature 167 | guard let result = feature.messageString else { return } 168 | self.otpFormat(code: result) 169 | }else{ 170 | Tools().showAlert(message: NSLocalizedString("image_error_tip", comment: "")) 171 | } 172 | } 173 | func otpFormat(code: String) { 174 | // otpauth://totp/UCloud:wujianming@licaifan.com?algorithm=SHA1&digits=6&issuer=UCloud&period=30&secret=N2IGDW2Y6XW3PCBX 175 | if code.contains("otpauth://totp/") == false || code.contains("secret=") == false || code.contains("issuer=") == false{ 176 | Tools().showAlert(message: NSLocalizedString("image_content_error_tip", comment: "")) 177 | return 178 | } 179 | let totpDic = Tools().totpDictionaryFormat(code: code) 180 | remarkTextField.stringValue = totpDic["remark"] as! String 181 | issuerTextField.stringValue = totpDic["issuer"] as! String 182 | secretTextFiled.stringValue = totpDic["secret"] as! String 183 | saveButton.isEnabled = true 184 | } 185 | //MARK: - ScanWindowDelegate 186 | func scanSuccess(code: String) { 187 | self.becomeFirstResponder() 188 | self.otpFormat(code: code) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /MinaOTP-MAC/OTPGenerator/Base32Addition.m: -------------------------------------------------------------------------------- 1 | // 2 | // Base32Addition.m 3 | // Mina_OTP_OC 4 | // 5 | // Created by 武建明 on 2018/4/11. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | #import "Base32Addition.h" 10 | 11 | @implementation Base32Addition 12 | +(NSData *)dataFromBase32String:(NSString *)encoding 13 | { 14 | NSData *data = nil; 15 | unsigned char *decodedBytes = NULL; 16 | @try { 17 | #define __ 255 18 | static char decodingTable[256] = { 19 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x00 - 0x0F 20 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x10 - 0x1F 21 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x20 - 0x2F 22 | __,__,26,27, 28,29,30,31, __,__,__,__, __, 0,__,__, // 0x30 - 0x3F 23 | __, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, // 0x40 - 0x4F 24 | 15,16,17,18, 19,20,21,22, 23,24,25,__, __,__,__,__, // 0x50 - 0x5F 25 | __, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, // 0x60 - 0x6F 26 | 15,16,17,18, 19,20,21,22, 23,24,25,__, __,__,__,__, // 0x70 - 0x7F 27 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x80 - 0x8F 28 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x90 - 0x9F 29 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xA0 - 0xAF 30 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xB0 - 0xBF 31 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xC0 - 0xCF 32 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xD0 - 0xDF 33 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xE0 - 0xEF 34 | __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xF0 - 0xFF 35 | }; 36 | static NSUInteger paddingAdjustment[8] = {0,1,1,1,2,3,3,4}; 37 | encoding = [encoding stringByReplacingOccurrencesOfString:@"=" withString:@""]; 38 | NSData *encodedData = [encoding dataUsingEncoding:NSASCIIStringEncoding]; 39 | unsigned char *encodedBytes = (unsigned char *)[encodedData bytes]; 40 | 41 | NSUInteger encodedLength = [encodedData length]; 42 | NSUInteger encodedBlocks = (encodedLength * 5) / 40; 43 | if( encodedLength % 8 != 0 ) { 44 | encodedBlocks++; 45 | } 46 | NSUInteger expectedDataLength = encodedBlocks * 5; 47 | 48 | decodedBytes = malloc(expectedDataLength); 49 | if( decodedBytes != NULL ) { 50 | 51 | unsigned char encodedByte1, encodedByte2, encodedByte3, encodedByte4; 52 | unsigned char encodedByte5, encodedByte6, encodedByte7, encodedByte8; 53 | NSUInteger encodedBytesToProcess = encodedLength; 54 | NSUInteger encodedBaseIndex = 0; 55 | NSUInteger decodedBaseIndex = 0; 56 | unsigned char encodedBlock[8] = {0,0,0,0,0,0,0,0}; 57 | NSUInteger encodedBlockIndex = 0; 58 | unsigned char c; 59 | while( encodedBytesToProcess-- >= 1 ) { 60 | c = encodedBytes[encodedBaseIndex++]; 61 | if( c == '=' ) break; // padding... 62 | 63 | c = decodingTable[c]; 64 | if( c == __ ) continue; 65 | 66 | encodedBlock[encodedBlockIndex++] = c; 67 | if( encodedBlockIndex == 8 ) { 68 | encodedByte1 = encodedBlock[0]; 69 | encodedByte2 = encodedBlock[1]; 70 | encodedByte3 = encodedBlock[2]; 71 | encodedByte4 = encodedBlock[3]; 72 | encodedByte5 = encodedBlock[4]; 73 | encodedByte6 = encodedBlock[5]; 74 | encodedByte7 = encodedBlock[6]; 75 | encodedByte8 = encodedBlock[7]; 76 | decodedBytes[decodedBaseIndex] = ((encodedByte1 << 3) & 0xF8) | ((encodedByte2 >> 2) & 0x07); 77 | decodedBytes[decodedBaseIndex+1] = ((encodedByte2 << 6) & 0xC0) | ((encodedByte3 << 1) & 0x3E) | ((encodedByte4 >> 4) & 0x01); 78 | decodedBytes[decodedBaseIndex+2] = ((encodedByte4 << 4) & 0xF0) | ((encodedByte5 >> 1) & 0x0F); 79 | decodedBytes[decodedBaseIndex+3] = ((encodedByte5 << 7) & 0x80) | ((encodedByte6 << 2) & 0x7C) | ((encodedByte7 >> 3) & 0x03); 80 | decodedBytes[decodedBaseIndex+4] = ((encodedByte7 << 5) & 0xE0) | (encodedByte8 & 0x1F); 81 | decodedBaseIndex += 5; 82 | encodedBlockIndex = 0; 83 | } 84 | } 85 | encodedByte7 = 0; 86 | encodedByte6 = 0; 87 | encodedByte5 = 0; 88 | encodedByte4 = 0; 89 | encodedByte3 = 0; 90 | encodedByte2 = 0; 91 | switch (encodedBlockIndex) { 92 | case 7: 93 | encodedByte7 = encodedBlock[6]; 94 | case 6: 95 | encodedByte6 = encodedBlock[5]; 96 | case 5: 97 | encodedByte5 = encodedBlock[4]; 98 | case 4: 99 | encodedByte4 = encodedBlock[3]; 100 | case 3: 101 | encodedByte3 = encodedBlock[2]; 102 | case 2: 103 | encodedByte2 = encodedBlock[1]; 104 | case 1: 105 | encodedByte1 = encodedBlock[0]; 106 | decodedBytes[decodedBaseIndex] = ((encodedByte1 << 3) & 0xF8) | ((encodedByte2 >> 2) & 0x07); 107 | decodedBytes[decodedBaseIndex+1] = ((encodedByte2 << 6) & 0xC0) | ((encodedByte3 << 1) & 0x3E) | ((encodedByte4 >> 4) & 0x01); 108 | decodedBytes[decodedBaseIndex+2] = ((encodedByte4 << 4) & 0xF0) | ((encodedByte5 >> 1) & 0x0F); 109 | decodedBytes[decodedBaseIndex+3] = ((encodedByte5 << 7) & 0x80) | ((encodedByte6 << 2) & 0x7C) | ((encodedByte7 >> 3) & 0x03); 110 | decodedBytes[decodedBaseIndex+4] = ((encodedByte7 << 5) & 0xE0); 111 | } 112 | decodedBaseIndex += paddingAdjustment[encodedBlockIndex]; 113 | data = [[NSData alloc] initWithBytes:decodedBytes length:decodedBaseIndex]; 114 | } 115 | } 116 | @catch (NSException *exception) { 117 | data = nil; 118 | NSLog(@"WARNING: error occured while decoding base 32 string: %@", exception); 119 | } 120 | @finally { 121 | if( decodedBytes != NULL ) { 122 | free( decodedBytes ); 123 | } 124 | } 125 | return data; 126 | } 127 | +(NSString *)base32StringFromData:(NSData *)data 128 | { 129 | NSString *encoding = nil; 130 | unsigned char *encodingBytes = NULL; 131 | @try { 132 | static char encodingTable[32] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 133 | static NSUInteger paddingTable[] = {0,6,4,3,1}; 134 | 135 | // Table 3: The Base 32 Alphabet 136 | // 137 | // Value Encoding Value Encoding Value Encoding Value Encoding 138 | // 0 A 9 J 18 S 27 3 139 | // 1 B 10 K 19 T 28 4 140 | // 2 C 11 L 20 U 29 5 141 | // 3 D 12 M 21 V 30 6 142 | // 4 E 13 N 22 W 31 7 143 | // 5 F 14 O 23 X 144 | // 6 G 15 P 24 Y (pad) = 145 | // 7 H 16 Q 25 Z 146 | // 8 I 17 R 26 2 147 | 148 | NSUInteger dataLength = [data length]; 149 | NSUInteger encodedBlocks = (dataLength * 8) / 40; 150 | NSUInteger padding = paddingTable[dataLength % 5]; 151 | if( padding > 0 ) encodedBlocks++; 152 | NSUInteger encodedLength = encodedBlocks * 8; 153 | 154 | encodingBytes = malloc(encodedLength); 155 | if( encodingBytes != NULL ) { 156 | NSUInteger rawBytesToProcess = dataLength; 157 | NSUInteger rawBaseIndex = 0; 158 | NSUInteger encodingBaseIndex = 0; 159 | unsigned char *rawBytes = (unsigned char *)[data bytes]; 160 | unsigned char rawByte1, rawByte2, rawByte3, rawByte4, rawByte5; 161 | while( rawBytesToProcess >= 5 ) { 162 | rawByte1 = rawBytes[rawBaseIndex]; 163 | rawByte2 = rawBytes[rawBaseIndex+1]; 164 | rawByte3 = rawBytes[rawBaseIndex+2]; 165 | rawByte4 = rawBytes[rawBaseIndex+3]; 166 | rawByte5 = rawBytes[rawBaseIndex+4]; 167 | encodingBytes[encodingBaseIndex] = encodingTable[((rawByte1 >> 3) & 0x1F)]; 168 | encodingBytes[encodingBaseIndex+1] = encodingTable[((rawByte1 << 2) & 0x1C) | ((rawByte2 >> 6) & 0x03) ]; 169 | encodingBytes[encodingBaseIndex+2] = encodingTable[((rawByte2 >> 1) & 0x1F)]; 170 | encodingBytes[encodingBaseIndex+3] = encodingTable[((rawByte2 << 4) & 0x10) | ((rawByte3 >> 4) & 0x0F)]; 171 | encodingBytes[encodingBaseIndex+4] = encodingTable[((rawByte3 << 1) & 0x1E) | ((rawByte4 >> 7) & 0x01)]; 172 | encodingBytes[encodingBaseIndex+5] = encodingTable[((rawByte4 >> 2) & 0x1F)]; 173 | encodingBytes[encodingBaseIndex+6] = encodingTable[((rawByte4 << 3) & 0x18) | ((rawByte5 >> 5) & 0x07)]; 174 | encodingBytes[encodingBaseIndex+7] = encodingTable[rawByte5 & 0x1F]; 175 | 176 | rawBaseIndex += 5; 177 | encodingBaseIndex += 8; 178 | rawBytesToProcess -= 5; 179 | } 180 | rawByte4 = 0; 181 | rawByte3 = 0; 182 | rawByte2 = 0; 183 | switch (dataLength-rawBaseIndex) { 184 | case 4: 185 | rawByte4 = rawBytes[rawBaseIndex+3]; 186 | case 3: 187 | rawByte3 = rawBytes[rawBaseIndex+2]; 188 | case 2: 189 | rawByte2 = rawBytes[rawBaseIndex+1]; 190 | case 1: 191 | rawByte1 = rawBytes[rawBaseIndex]; 192 | encodingBytes[encodingBaseIndex] = encodingTable[((rawByte1 >> 3) & 0x1F)]; 193 | encodingBytes[encodingBaseIndex+1] = encodingTable[((rawByte1 << 2) & 0x1C) | ((rawByte2 >> 6) & 0x03) ]; 194 | encodingBytes[encodingBaseIndex+2] = encodingTable[((rawByte2 >> 1) & 0x1F)]; 195 | encodingBytes[encodingBaseIndex+3] = encodingTable[((rawByte2 << 4) & 0x10) | ((rawByte3 >> 4) & 0x0F)]; 196 | encodingBytes[encodingBaseIndex+4] = encodingTable[((rawByte3 << 1) & 0x1E) | ((rawByte4 >> 7) & 0x01)]; 197 | encodingBytes[encodingBaseIndex+5] = encodingTable[((rawByte4 >> 2) & 0x1F)]; 198 | encodingBytes[encodingBaseIndex+6] = encodingTable[((rawByte4 << 3) & 0x18)]; 199 | // we can skip rawByte5 since we have a partial block it would always be 0 200 | break; 201 | } 202 | // compute location from where to begin inserting padding, it may overwrite some bytes from the partial block encoding 203 | // if their value was 0 (cases 1-3). 204 | encodingBaseIndex = encodedLength - padding; 205 | while( padding-- > 0 ) { 206 | encodingBytes[encodingBaseIndex++] = '='; 207 | } 208 | encoding = [[NSString alloc] initWithBytes:encodingBytes length:encodedLength encoding:NSASCIIStringEncoding]; 209 | } 210 | } 211 | @catch (NSException *exception) { 212 | encoding = nil; 213 | NSLog(@"WARNING: error occured while tring to encode base 32 data: %@", exception); 214 | } 215 | @finally { 216 | if( encodingBytes != NULL ) { 217 | free( encodingBytes ); 218 | } 219 | } 220 | return encoding; 221 | } 222 | 223 | @end 224 | 225 | @implementation NSString (Base32Addition) 226 | -(NSString *)base32String 227 | { 228 | NSData *utf8encoding = [self dataUsingEncoding:NSUTF8StringEncoding]; 229 | return [Base32Addition base32StringFromData:utf8encoding]; 230 | } 231 | +(NSString *)stringFromBase32String:(NSString *)base32String 232 | { 233 | NSData *utf8encoding = [Base32Addition dataFromBase32String:base32String]; 234 | return [[NSString alloc] initWithData:utf8encoding encoding:NSUTF8StringEncoding]; 235 | } 236 | @end 237 | 238 | @implementation NSData (Base32Addition) 239 | +(NSData *)dataWithBase32String:(NSString *)base32String 240 | { 241 | return [Base32Addition dataFromBase32String:base32String]; 242 | } 243 | -(NSString *)base32String 244 | { 245 | return [Base32Addition base32StringFromData:self]; 246 | } 247 | @end 248 | 249 | -------------------------------------------------------------------------------- /MinaOTP-MAC/FlatButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlatButton.swift 3 | // Disk Sensei 4 | // 5 | // Created by Oskar Groth on 02/08/16. 6 | // Copyright © 2016 Cindori. All rights reserved. 7 | // 8 | import Cocoa 9 | import QuartzCore 10 | 11 | internal extension CALayer { 12 | func animate(color: CGColor, keyPath: String, duration: Double) { 13 | if value(forKey: keyPath) as! CGColor? != color { 14 | let animation = CABasicAnimation(keyPath: keyPath) 15 | animation.toValue = color 16 | animation.fromValue = value(forKey: keyPath) 17 | animation.duration = duration 18 | animation.isRemovedOnCompletion = false 19 | animation.fillMode = CAMediaTimingFillMode.forwards 20 | add(animation, forKey: keyPath) 21 | setValue(color, forKey: keyPath) 22 | } 23 | } 24 | } 25 | 26 | //unused for now 27 | internal extension NSColor { 28 | func tintedColor() -> NSColor { 29 | var h = CGFloat(), s = CGFloat(), b = CGFloat(), a = CGFloat() 30 | let rgbColor = usingColorSpaceName(NSColorSpaceName.calibratedRGB) 31 | rgbColor?.getHue(&h, saturation: &s, brightness: &b, alpha: &a) 32 | return NSColor(hue: h, saturation: s, brightness: b == 0 ? 0.2 : b * 0.8, alpha: a) 33 | } 34 | } 35 | 36 | open class FlatButton: NSButton, CALayerDelegate { 37 | 38 | internal var containerLayer = CALayer() 39 | internal var iconLayer = CAShapeLayer() 40 | internal var alternateIconLayer = CAShapeLayer() 41 | internal var titleLayer = CATextLayer() 42 | internal var mouseDown = Bool() 43 | @IBInspectable public var momentary: Bool = true { 44 | didSet { 45 | animateColor(state == .on) 46 | } 47 | } 48 | @IBInspectable public var onAnimationDuration: Double = 0 49 | @IBInspectable public var offAnimationDuration: Double = 0.1 50 | @IBInspectable public var glowRadius: CGFloat = 0 { 51 | didSet { 52 | containerLayer.shadowRadius = glowRadius 53 | animateColor(state == .on) 54 | } 55 | } 56 | @IBInspectable public var glowOpacity: Float = 0 { 57 | didSet { 58 | containerLayer.shadowOpacity = glowOpacity 59 | animateColor(state == .on) 60 | } 61 | } 62 | @IBInspectable public var cornerRadius: CGFloat = 4 { 63 | didSet { 64 | layer?.cornerRadius = cornerRadius 65 | } 66 | } 67 | @IBInspectable public var borderWidth: CGFloat = 1 { 68 | didSet { 69 | layer?.borderWidth = borderWidth 70 | } 71 | } 72 | @IBInspectable public var borderColor: NSColor = NSColor.darkGray { 73 | didSet { 74 | animateColor(state == .on) 75 | } 76 | } 77 | @IBInspectable public var activeBorderColor: NSColor = NSColor.white { 78 | didSet { 79 | animateColor(state == .on) 80 | } 81 | } 82 | @IBInspectable public var buttonColor: NSColor = NSColor.white { 83 | didSet { 84 | animateColor(state == .on) 85 | } 86 | } 87 | @IBInspectable public var activeButtonColor: NSColor = NSColor.white { 88 | didSet { 89 | animateColor(state == .on) 90 | } 91 | } 92 | @IBInspectable public var iconColor: NSColor = NSColor.gray { 93 | didSet { 94 | animateColor(state == .on) 95 | } 96 | } 97 | @IBInspectable public var activeIconColor: NSColor = NSColor.black { 98 | didSet { 99 | animateColor(state == .on) 100 | } 101 | } 102 | @IBInspectable public var textColor: NSColor = NSColor.gray { 103 | didSet { 104 | animateColor(state == .on) 105 | } 106 | } 107 | @IBInspectable public var activeTextColor: NSColor = NSColor.gray { 108 | didSet { 109 | animateColor(state == .on) 110 | } 111 | } 112 | 113 | override open var title: String { 114 | didSet { 115 | setupTitle() 116 | } 117 | } 118 | override open var font: NSFont? { 119 | didSet { 120 | setupTitle() 121 | } 122 | } 123 | override open var frame: NSRect { 124 | didSet { 125 | positionTitleAndImage() 126 | } 127 | } 128 | override open var image: NSImage? { 129 | didSet { 130 | setupImage() 131 | } 132 | } 133 | override open var alternateImage: NSImage? { 134 | didSet { 135 | setupImage() 136 | } 137 | } 138 | override open var isEnabled: Bool { 139 | didSet { 140 | alphaValue = isEnabled ? 1 : 0.5 141 | } 142 | } 143 | 144 | 145 | // MARK: Setup & Initialization 146 | 147 | required public init?(coder: NSCoder) { 148 | super.init(coder: coder) 149 | setup() 150 | } 151 | 152 | override init(frame: NSRect) { 153 | super.init(frame: frame) 154 | setup() 155 | } 156 | 157 | internal func setup() { 158 | wantsLayer = true 159 | layer?.masksToBounds = false 160 | containerLayer.masksToBounds = false 161 | layer?.cornerRadius = 4 162 | layer?.borderWidth = 1 163 | layer?.delegate = self 164 | //containerLayer.backgroundColor = NSColor.blue.withAlphaComponent(0.1).cgColor 165 | //titleLayer.backgroundColor = NSColor.red.withAlphaComponent(0.2).cgColor 166 | titleLayer.delegate = self 167 | if let scale = window?.backingScaleFactor { 168 | titleLayer.contentsScale = scale 169 | } 170 | iconLayer.delegate = self 171 | alternateIconLayer.delegate = self 172 | iconLayer.masksToBounds = true 173 | alternateIconLayer.masksToBounds = true 174 | containerLayer.shadowOffset = NSSize.zero 175 | containerLayer.shadowColor = NSColor.clear.cgColor 176 | containerLayer.frame = NSMakeRect(0, 0, bounds.width, bounds.height) 177 | containerLayer.addSublayer(iconLayer) 178 | containerLayer.addSublayer(alternateIconLayer) 179 | containerLayer.addSublayer(titleLayer) 180 | layer?.addSublayer(containerLayer) 181 | setupTitle() 182 | setupImage() 183 | } 184 | 185 | internal func setupTitle() { 186 | guard let font = font else { 187 | return 188 | } 189 | titleLayer.string = title 190 | titleLayer.font = font 191 | titleLayer.fontSize = font.pointSize 192 | positionTitleAndImage() 193 | } 194 | 195 | func positionTitleAndImage() { 196 | let attributes = [NSAttributedString.Key.font: font as Any] 197 | let titleSize = title.size(withAttributes: attributes) 198 | var titleRect = NSMakeRect(0, 0, titleSize.width, titleSize.height) 199 | var imageRect = iconLayer.frame 200 | let hSpacing = round((bounds.width-(imageRect.width+titleSize.width))/3) 201 | let vSpacing = round((bounds.height-(imageRect.height+titleSize.height))/3) 202 | 203 | switch imagePosition { 204 | case .imageAbove: 205 | titleRect.origin.y = bounds.height-titleRect.height - 2 206 | titleRect.origin.x = round((bounds.width - titleSize.width)/2) 207 | imageRect.origin.y = vSpacing 208 | imageRect.origin.x = round((bounds.width - imageRect.width)/2) 209 | break 210 | case .imageBelow: 211 | titleRect.origin.y = 2 212 | titleRect.origin.x = round((bounds.width - titleSize.width)/2) 213 | imageRect.origin.y = bounds.height-vSpacing-imageRect.height 214 | imageRect.origin.x = round((bounds.width - imageRect.width)/2) 215 | break 216 | case .imageLeft: 217 | titleRect.origin.y = round((bounds.height - titleSize.height)/2) 218 | titleRect.origin.x = bounds.width - titleSize.width - 6 219 | imageRect.origin.y = round((bounds.height - imageRect.height)/2) 220 | imageRect.origin.x = hSpacing 221 | break 222 | case .imageRight: 223 | titleRect.origin.y = round((bounds.height - titleSize.height)/2) 224 | titleRect.origin.x = 2 225 | imageRect.origin.y = round((bounds.height - imageRect.height)/2) 226 | imageRect.origin.x = bounds.width - imageRect.width - hSpacing 227 | break 228 | default: 229 | titleRect.origin.y = round((bounds.height - titleSize.height)/2) 230 | titleRect.origin.x = round((bounds.width - titleSize.width)/2) 231 | } 232 | iconLayer.frame = imageRect 233 | alternateIconLayer.frame = imageRect 234 | titleLayer.frame = titleRect 235 | } 236 | 237 | internal func setupImage() { 238 | guard let image = image else { 239 | return 240 | } 241 | let maskLayer = CALayer() 242 | let imageSize = image.size 243 | var imageRect:CGRect = NSMakeRect(0, 0, imageSize.width, imageSize.height) 244 | let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) 245 | maskLayer.contents = imageRef 246 | iconLayer.frame = imageRect 247 | maskLayer.frame = imageRect 248 | iconLayer.mask = maskLayer 249 | 250 | if let alternateImage = alternateImage { 251 | let altMaskLayer = CALayer() 252 | let altImageSize = alternateImage.size 253 | var altImageRect:CGRect = NSMakeRect(0, 0, altImageSize.width, altImageSize.height) 254 | let altImageRef = alternateImage.cgImage(forProposedRect: &altImageRect, context: nil, hints: nil) 255 | altMaskLayer.contents = altImageRef 256 | alternateIconLayer.frame = altImageRect 257 | altMaskLayer.frame = altImageRect 258 | alternateIconLayer.mask = altMaskLayer 259 | alternateIconLayer.frame = altImageRect 260 | } 261 | positionTitleAndImage() 262 | } 263 | 264 | override open func awakeFromNib() { 265 | super.awakeFromNib() 266 | let trackingArea = NSTrackingArea(rect: bounds, options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited], owner: self, userInfo: nil) 267 | addTrackingArea(trackingArea) 268 | } 269 | 270 | // MARK: Animations 271 | 272 | internal func removeAnimations() { 273 | layer?.removeAllAnimations() 274 | if layer?.sublayers != nil { 275 | for subLayer in (layer?.sublayers)! { 276 | subLayer.removeAllAnimations() 277 | } 278 | } 279 | } 280 | 281 | public func animateColor(_ isOn: Bool) { 282 | removeAnimations() 283 | let duration = isOn ? onAnimationDuration : offAnimationDuration 284 | let bgColor = isOn ? activeButtonColor : buttonColor 285 | let titleColor = isOn ? activeTextColor : textColor 286 | let imageColor = isOn ? activeIconColor : iconColor 287 | let borderColor = isOn ? activeBorderColor : self.borderColor 288 | layer?.animate(color: bgColor.cgColor, keyPath: "backgroundColor", duration: duration) 289 | layer?.animate(color: borderColor.cgColor, keyPath: "borderColor", duration: duration) 290 | 291 | /* I started seeing high (~5%) background CPU usage in apps using 292 | FlatButton, and was able to track it down to background CATextLayer animation calls 293 | happening constantly, originating from the call below. It could be a CATextLayer bug. 294 | For now I'm going with setting the color instantly as it fixes this issue. */ 295 | //titleLayer.animate(color: titleColor.cgColor, keyPath: "foregroundColor", duration: duration) 296 | titleLayer.foregroundColor = titleColor.cgColor 297 | 298 | if alternateImage == nil { 299 | iconLayer.animate(color: imageColor.cgColor, keyPath: "backgroundColor", duration: duration) 300 | } else { 301 | iconLayer.animate(color: isOn ? NSColor.clear.cgColor : iconColor.cgColor, keyPath: "backgroundColor", duration: duration) 302 | alternateIconLayer.animate(color: isOn ? activeIconColor.cgColor : NSColor.clear.cgColor, keyPath: "backgroundColor", duration: duration) 303 | } 304 | 305 | // Shadows 306 | 307 | if glowRadius > 0, glowOpacity > 0 { 308 | containerLayer.animate(color: isOn ? activeIconColor.cgColor : NSColor.clear.cgColor, keyPath: "shadowColor", duration: duration) 309 | } 310 | } 311 | 312 | // MARK: Interaction 313 | 314 | public func setOn(_ isOn: Bool) { 315 | // let nextState = isOn ? .on : .off 316 | let nextState = isOn ? NSControl.StateValue.on : NSControl.StateValue.off 317 | if nextState != state { 318 | state = nextState 319 | animateColor(state == .on) 320 | } 321 | } 322 | 323 | override open func hitTest(_ point: NSPoint) -> NSView? { 324 | return isEnabled ? super.hitTest(point) : nil 325 | } 326 | 327 | override open func mouseDown(with event: NSEvent) { 328 | if isEnabled { 329 | mouseDown = true 330 | setOn(state == .on ? false : true) 331 | } 332 | } 333 | 334 | override open func mouseEntered(with event: NSEvent) { 335 | if mouseDown { 336 | setOn(state == .on ? false : true) 337 | } 338 | } 339 | 340 | override open func mouseExited(with event: NSEvent) { 341 | if mouseDown { 342 | setOn(state == .on ? false : true) 343 | mouseDown = false 344 | } 345 | } 346 | 347 | override open func mouseUp(with event: NSEvent) { 348 | if mouseDown { 349 | mouseDown = false 350 | if momentary { 351 | setOn(state == .on ? false : true) 352 | } 353 | _ = target?.perform(action, with: self) 354 | } 355 | } 356 | 357 | // MARK: Drawing 358 | 359 | override open func viewDidChangeBackingProperties() { 360 | super.viewDidChangeBackingProperties() 361 | if let scale = window?.backingScaleFactor { 362 | titleLayer.contentsScale = scale 363 | layer?.contentsScale = scale 364 | iconLayer.contentsScale = scale 365 | } 366 | } 367 | 368 | open func layer(_ layer: CALayer, shouldInheritContentsScale newScale: CGFloat, from window: NSWindow) -> Bool { 369 | return true 370 | } 371 | 372 | override open func draw(_ dirtyRect: NSRect) { 373 | 374 | } 375 | 376 | override open func layout() { 377 | super.layout() 378 | positionTitleAndImage() 379 | } 380 | 381 | override open func updateLayer() { 382 | super.updateLayer() 383 | } 384 | 385 | 386 | } 387 | -------------------------------------------------------------------------------- /MinaOTP-MAC/en.lproj/MainMenu.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ 3 | "1UK-8n-QPP.title" = "Customize Toolbar…"; 4 | 5 | /* Class = "NSMenuItem"; title = "MinaOTP-MAC"; ObjectID = "1Xt-HY-uBw"; */ 6 | "1Xt-HY-uBw.title" = "MinaOTP-MAC"; 7 | 8 | /* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ 9 | "1b7-l0-nxx.title" = "Find"; 10 | 11 | /* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */ 12 | "1tx-W0-xDw.title" = "Lower"; 13 | 14 | /* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */ 15 | "2h7-ER-AoG.title" = "Raise"; 16 | 17 | /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ 18 | "2oI-Rn-ZJC.title" = "Transformations"; 19 | 20 | /* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ 21 | "3IN-sU-3Bg.title" = "Spelling"; 22 | 23 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */ 24 | "3Om-Ey-2VK.title" = "Use Default"; 25 | 26 | /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ 27 | "3rS-ZA-NoH.title" = "Speech"; 28 | 29 | /* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */ 30 | "46P-cB-AYj.title" = "Tighten"; 31 | 32 | /* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ 33 | "4EN-yA-p0u.title" = "Find"; 34 | 35 | /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ 36 | "4J7-dP-txa.title" = "Enter Full Screen"; 37 | 38 | /* Class = "NSMenuItem"; title = "Quit MinaOTP-MAC"; ObjectID = "4sb-4s-VLi"; */ 39 | "4sb-4s-VLi.title" = "Quit MinaOTP-MAC"; 40 | 41 | /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ 42 | "5QF-Oa-p0T.title" = "Edit"; 43 | 44 | /* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */ 45 | "5Vv-lz-BsD.title" = "Copy Style"; 46 | 47 | /* Class = "NSMenuItem"; title = "About MinaOTP-MAC"; ObjectID = "5kV-Vb-QxS"; */ 48 | "5kV-Vb-QxS.title" = "About MinaOTP-MAC"; 49 | 50 | /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ 51 | "6dh-zS-Vam.title" = "Redo"; 52 | 53 | /* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ 54 | "78Y-hA-62v.title" = "Correct Spelling Automatically"; 55 | 56 | /* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */ 57 | "8mr-sm-Yjd.title" = "Writing Direction"; 58 | 59 | /* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ 60 | "9ic-FL-obx.title" = "Substitutions"; 61 | 62 | /* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ 63 | "9yt-4B-nSM.title" = "Smart Copy/Paste"; 64 | 65 | /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ 66 | "AYu-sK-qS6.title" = "Main Menu"; 67 | 68 | /* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ 69 | "BOF-NM-1cW.title" = "Preferences…"; 70 | 71 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */ 72 | "BgM-ve-c93.title" = "\tLeft to Right"; 73 | 74 | /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ 75 | "Bw7-FT-i3A.title" = "Save As…"; 76 | 77 | /* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ 78 | "DVo-aG-piG.title" = "Close"; 79 | 80 | /* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ 81 | "Dv1-io-Yv7.title" = "Spelling and Grammar"; 82 | 83 | /* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ 84 | "F2S-fz-NVQ.title" = "Help"; 85 | 86 | /* Class = "NSMenuItem"; title = "MinaOTP-MAC Help"; ObjectID = "FKE-Sm-Kum"; */ 87 | "FKE-Sm-Kum.title" = "MinaOTP-MAC Help"; 88 | 89 | /* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */ 90 | "Fal-I4-PZk.title" = "Text"; 91 | 92 | /* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ 93 | "FeM-D8-WVr.title" = "Substitutions"; 94 | 95 | /* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */ 96 | "GB9-OM-e27.title" = "Bold"; 97 | 98 | /* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */ 99 | "GEO-Iw-cKr.title" = "Format"; 100 | 101 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */ 102 | "GUa-eO-cwY.title" = "Use Default"; 103 | 104 | /* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */ 105 | "Gi5-1S-RQB.title" = "Font"; 106 | 107 | /* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */ 108 | "H1b-Si-o9J.title" = "Writing Direction"; 109 | 110 | /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ 111 | "H8h-7b-M4v.title" = "View"; 112 | 113 | /* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ 114 | "HFQ-gK-NFA.title" = "Text Replacement"; 115 | 116 | /* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ 117 | "HFo-cy-zxI.title" = "Show Spelling and Grammar"; 118 | 119 | /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ 120 | "HyV-fh-RgO.title" = "View"; 121 | 122 | /* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */ 123 | "I0S-gh-46l.title" = "Subscript"; 124 | 125 | /* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ 126 | "IAo-SY-fd9.title" = "Open…"; 127 | 128 | /* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */ 129 | "J5U-5w-g23.title" = "Justify"; 130 | 131 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */ 132 | "J7y-lM-qPV.title" = "Use None"; 133 | 134 | /* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ 135 | "KaW-ft-85H.title" = "Revert to Saved"; 136 | 137 | /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ 138 | "Kd2-mp-pUS.title" = "Show All"; 139 | 140 | /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ 141 | "LE2-aR-0XJ.title" = "Bring All to Front"; 142 | 143 | /* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */ 144 | "LVM-kO-fVI.title" = "Paste Ruler"; 145 | 146 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */ 147 | "Lbh-J2-qVU.title" = "\tLeft to Right"; 148 | 149 | /* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */ 150 | "MkV-Pr-PK5.title" = "Copy Ruler"; 151 | 152 | /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ 153 | "NMo-om-nkz.title" = "Services"; 154 | 155 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */ 156 | "Nop-cj-93Q.title" = "\tDefault"; 157 | 158 | /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ 159 | "OY7-WF-poV.title" = "Minimize"; 160 | 161 | /* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */ 162 | "OaQ-X3-Vso.title" = "Baseline"; 163 | 164 | /* Class = "NSMenuItem"; title = "Hide MinaOTP-MAC"; ObjectID = "Olw-nP-bQN"; */ 165 | "Olw-nP-bQN.title" = "Hide MinaOTP-MAC"; 166 | 167 | /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ 168 | "OwM-mh-QMV.title" = "Find Previous"; 169 | 170 | /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ 171 | "Oyz-dy-DGm.title" = "Stop Speaking"; 172 | 173 | /* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */ 174 | "Ptp-SP-VEL.title" = "Bigger"; 175 | 176 | /* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */ 177 | "Q5e-8K-NDq.title" = "Show Fonts"; 178 | 179 | /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ 180 | "R4o-n2-Eq4.title" = "Zoom"; 181 | 182 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */ 183 | "RB4-Sm-HuC.title" = "\tRight to Left"; 184 | 185 | /* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */ 186 | "Rqc-34-cIF.title" = "Superscript"; 187 | 188 | /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ 189 | "Ruw-6m-B2m.title" = "Select All"; 190 | 191 | /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ 192 | "S0p-oC-mLd.title" = "Jump to Selection"; 193 | 194 | /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ 195 | "Td7-aD-5lo.title" = "Window"; 196 | 197 | /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ 198 | "UEZ-Bs-lqG.title" = "Capitalize"; 199 | 200 | /* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */ 201 | "VIY-Ag-zcb.title" = "Center"; 202 | 203 | /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ 204 | "Vdr-fp-XzO.title" = "Hide Others"; 205 | 206 | /* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */ 207 | "Vjx-xi-njq.title" = "Italic"; 208 | 209 | /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ 210 | "W48-6f-4Dl.title" = "Edit"; 211 | 212 | /* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */ 213 | "WRG-CD-K1S.title" = "Underline"; 214 | 215 | /* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ 216 | "Was-JA-tGl.title" = "New"; 217 | 218 | /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ 219 | "WeT-3V-zwk.title" = "Paste and Match Style"; 220 | 221 | /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ 222 | "Xz5-n4-O0W.title" = "Find…"; 223 | 224 | /* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ 225 | "YEy-JH-Tfz.title" = "Find and Replace…"; 226 | 227 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */ 228 | "YGs-j5-SAR.title" = "\tDefault"; 229 | 230 | /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ 231 | "Ynk-f8-cLZ.title" = "Start Speaking"; 232 | 233 | /* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */ 234 | "ZM1-6Q-yy1.title" = "Align Left"; 235 | 236 | /* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */ 237 | "ZvO-Gk-QUH.title" = "Paragraph"; 238 | 239 | /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ 240 | "aTl-1u-JFS.title" = "Print…"; 241 | 242 | /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ 243 | "aUF-d1-5bR.title" = "Window"; 244 | 245 | /* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */ 246 | "aXa-aM-Jaq.title" = "Font"; 247 | 248 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */ 249 | "agt-UL-0e3.title" = "Use Default"; 250 | 251 | /* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */ 252 | "bgn-CT-cEk.title" = "Show Colors"; 253 | 254 | /* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ 255 | "bib-Uj-vzu.title" = "File"; 256 | 257 | /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ 258 | "buJ-ug-pKt.title" = "Use Selection for Find"; 259 | 260 | /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ 261 | "c8a-y6-VQd.title" = "Transformations"; 262 | 263 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */ 264 | "cDB-IK-hbR.title" = "Use None"; 265 | 266 | /* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */ 267 | "cqv-fj-IhA.title" = "Selection"; 268 | 269 | /* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ 270 | "cwL-P1-jid.title" = "Smart Links"; 271 | 272 | /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ 273 | "d9M-CD-aMd.title" = "Make Lower Case"; 274 | 275 | /* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */ 276 | "d9c-me-L2H.title" = "Text"; 277 | 278 | /* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ 279 | "dMs-cI-mzQ.title" = "File"; 280 | 281 | /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ 282 | "dRJ-4n-Yzg.title" = "Undo"; 283 | 284 | /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ 285 | "gVA-U4-sdL.title" = "Paste"; 286 | 287 | /* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ 288 | "hQb-2v-fYv.title" = "Smart Quotes"; 289 | 290 | /* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ 291 | "hz2-CU-CR7.title" = "Check Document Now"; 292 | 293 | /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ 294 | "hz9-B4-Xy5.title" = "Services"; 295 | 296 | /* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */ 297 | "i1d-Er-qST.title" = "Smaller"; 298 | 299 | /* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */ 300 | "ijk-EB-dga.title" = "Baseline"; 301 | 302 | /* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */ 303 | "jBQ-r6-VK2.title" = "Kern"; 304 | 305 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */ 306 | "jFq-tB-4Kx.title" = "\tRight to Left"; 307 | 308 | /* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */ 309 | "jxT-CU-nIS.title" = "Format"; 310 | 311 | /* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */ 312 | "kIP-vf-haE.title" = "Show Sidebar"; 313 | 314 | /* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ 315 | "mK6-2p-4JG.title" = "Check Grammar With Spelling"; 316 | 317 | /* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */ 318 | "o6e-r0-MWq.title" = "Ligatures"; 319 | 320 | /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ 321 | "oas-Oc-fiZ.title" = "Open Recent"; 322 | 323 | /* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */ 324 | "ogc-rX-tC1.title" = "Loosen"; 325 | 326 | /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ 327 | "pa3-QI-u2k.title" = "Delete"; 328 | 329 | /* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ 330 | "pxx-59-PXV.title" = "Save…"; 331 | 332 | /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ 333 | "q09-fT-Sye.title" = "Find Next"; 334 | 335 | /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ 336 | "qIS-W8-SiK.title" = "Page Setup…"; 337 | 338 | /* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ 339 | "rbD-Rh-wIN.title" = "Check Spelling While Typing"; 340 | 341 | /* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ 342 | "rgM-f4-ycn.title" = "Smart Dashes"; 343 | 344 | /* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ 345 | "snW-S8-Cw5.title" = "Show Toolbar"; 346 | 347 | /* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ 348 | "tRr-pd-1PS.title" = "Data Detectors"; 349 | 350 | /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ 351 | "tXI-mr-wws.title" = "Open Recent"; 352 | 353 | /* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */ 354 | "tlD-Oa-oAM.title" = "Kern"; 355 | 356 | /* Class = "NSMenu"; title = "MinaOTP-MAC"; ObjectID = "uQy-DD-JDr"; */ 357 | "uQy-DD-JDr.title" = "MinaOTP-MAC"; 358 | 359 | /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ 360 | "uRl-iY-unG.title" = "Cut"; 361 | 362 | /* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */ 363 | "vKC-jM-MkH.title" = "Paste Style"; 364 | 365 | /* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */ 366 | "vLm-3I-IUL.title" = "Show Ruler"; 367 | 368 | /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ 369 | "vNY-rz-j42.title" = "Clear Menu"; 370 | 371 | /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ 372 | "vmV-6d-7jI.title" = "Make Upper Case"; 373 | 374 | /* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */ 375 | "w0m-vy-SC9.title" = "Ligatures"; 376 | 377 | /* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */ 378 | "wb2-vD-lq4.title" = "Align Right"; 379 | 380 | /* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ 381 | "wpr-3q-Mcd.title" = "Help"; 382 | 383 | /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ 384 | "x3v-GG-iWU.title" = "Copy"; 385 | 386 | /* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */ 387 | "xQD-1f-W4t.title" = "Use All"; 388 | 389 | /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ 390 | "xrE-MZ-jX0.title" = "Speech"; 391 | 392 | /* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ 393 | "z6F-FW-3nz.title" = "Show Substitutions"; 394 | -------------------------------------------------------------------------------- /MinaOTP-MAC/zh-Hans.lproj/MainMenu.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ 3 | "1UK-8n-QPP.title" = "Customize Toolbar…"; 4 | 5 | /* Class = "NSMenuItem"; title = "MinaOTP-MAC"; ObjectID = "1Xt-HY-uBw"; */ 6 | "1Xt-HY-uBw.title" = "MinaOTP-MAC"; 7 | 8 | /* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ 9 | "1b7-l0-nxx.title" = "Find"; 10 | 11 | /* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */ 12 | "1tx-W0-xDw.title" = "Lower"; 13 | 14 | /* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */ 15 | "2h7-ER-AoG.title" = "Raise"; 16 | 17 | /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ 18 | "2oI-Rn-ZJC.title" = "Transformations"; 19 | 20 | /* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ 21 | "3IN-sU-3Bg.title" = "Spelling"; 22 | 23 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */ 24 | "3Om-Ey-2VK.title" = "Use Default"; 25 | 26 | /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ 27 | "3rS-ZA-NoH.title" = "Speech"; 28 | 29 | /* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */ 30 | "46P-cB-AYj.title" = "Tighten"; 31 | 32 | /* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ 33 | "4EN-yA-p0u.title" = "Find"; 34 | 35 | /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ 36 | "4J7-dP-txa.title" = "Enter Full Screen"; 37 | 38 | /* Class = "NSMenuItem"; title = "Quit MinaOTP-MAC"; ObjectID = "4sb-4s-VLi"; */ 39 | "4sb-4s-VLi.title" = "Quit MinaOTP-MAC"; 40 | 41 | /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ 42 | "5QF-Oa-p0T.title" = "Edit"; 43 | 44 | /* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */ 45 | "5Vv-lz-BsD.title" = "Copy Style"; 46 | 47 | /* Class = "NSMenuItem"; title = "About MinaOTP-MAC"; ObjectID = "5kV-Vb-QxS"; */ 48 | "5kV-Vb-QxS.title" = "About MinaOTP-MAC"; 49 | 50 | /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ 51 | "6dh-zS-Vam.title" = "Redo"; 52 | 53 | /* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ 54 | "78Y-hA-62v.title" = "Correct Spelling Automatically"; 55 | 56 | /* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */ 57 | "8mr-sm-Yjd.title" = "Writing Direction"; 58 | 59 | /* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ 60 | "9ic-FL-obx.title" = "Substitutions"; 61 | 62 | /* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ 63 | "9yt-4B-nSM.title" = "Smart Copy/Paste"; 64 | 65 | /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ 66 | "AYu-sK-qS6.title" = "Main Menu"; 67 | 68 | /* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ 69 | "BOF-NM-1cW.title" = "Preferences…"; 70 | 71 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */ 72 | "BgM-ve-c93.title" = "\tLeft to Right"; 73 | 74 | /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ 75 | "Bw7-FT-i3A.title" = "Save As…"; 76 | 77 | /* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ 78 | "DVo-aG-piG.title" = "Close"; 79 | 80 | /* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ 81 | "Dv1-io-Yv7.title" = "Spelling and Grammar"; 82 | 83 | /* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ 84 | "F2S-fz-NVQ.title" = "Help"; 85 | 86 | /* Class = "NSMenuItem"; title = "MinaOTP-MAC Help"; ObjectID = "FKE-Sm-Kum"; */ 87 | "FKE-Sm-Kum.title" = "MinaOTP-MAC Help"; 88 | 89 | /* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */ 90 | "Fal-I4-PZk.title" = "Text"; 91 | 92 | /* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ 93 | "FeM-D8-WVr.title" = "Substitutions"; 94 | 95 | /* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */ 96 | "GB9-OM-e27.title" = "Bold"; 97 | 98 | /* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */ 99 | "GEO-Iw-cKr.title" = "Format"; 100 | 101 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */ 102 | "GUa-eO-cwY.title" = "Use Default"; 103 | 104 | /* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */ 105 | "Gi5-1S-RQB.title" = "Font"; 106 | 107 | /* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */ 108 | "H1b-Si-o9J.title" = "Writing Direction"; 109 | 110 | /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ 111 | "H8h-7b-M4v.title" = "View"; 112 | 113 | /* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ 114 | "HFQ-gK-NFA.title" = "Text Replacement"; 115 | 116 | /* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ 117 | "HFo-cy-zxI.title" = "Show Spelling and Grammar"; 118 | 119 | /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ 120 | "HyV-fh-RgO.title" = "View"; 121 | 122 | /* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */ 123 | "I0S-gh-46l.title" = "Subscript"; 124 | 125 | /* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ 126 | "IAo-SY-fd9.title" = "Open…"; 127 | 128 | /* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */ 129 | "J5U-5w-g23.title" = "Justify"; 130 | 131 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */ 132 | "J7y-lM-qPV.title" = "Use None"; 133 | 134 | /* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ 135 | "KaW-ft-85H.title" = "Revert to Saved"; 136 | 137 | /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ 138 | "Kd2-mp-pUS.title" = "Show All"; 139 | 140 | /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ 141 | "LE2-aR-0XJ.title" = "Bring All to Front"; 142 | 143 | /* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */ 144 | "LVM-kO-fVI.title" = "Paste Ruler"; 145 | 146 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */ 147 | "Lbh-J2-qVU.title" = "\tLeft to Right"; 148 | 149 | /* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */ 150 | "MkV-Pr-PK5.title" = "Copy Ruler"; 151 | 152 | /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ 153 | "NMo-om-nkz.title" = "Services"; 154 | 155 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */ 156 | "Nop-cj-93Q.title" = "\tDefault"; 157 | 158 | /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ 159 | "OY7-WF-poV.title" = "Minimize"; 160 | 161 | /* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */ 162 | "OaQ-X3-Vso.title" = "Baseline"; 163 | 164 | /* Class = "NSMenuItem"; title = "Hide MinaOTP-MAC"; ObjectID = "Olw-nP-bQN"; */ 165 | "Olw-nP-bQN.title" = "Hide MinaOTP-MAC"; 166 | 167 | /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ 168 | "OwM-mh-QMV.title" = "Find Previous"; 169 | 170 | /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ 171 | "Oyz-dy-DGm.title" = "Stop Speaking"; 172 | 173 | /* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */ 174 | "Ptp-SP-VEL.title" = "Bigger"; 175 | 176 | /* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */ 177 | "Q5e-8K-NDq.title" = "Show Fonts"; 178 | 179 | /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ 180 | "R4o-n2-Eq4.title" = "Zoom"; 181 | 182 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */ 183 | "RB4-Sm-HuC.title" = "\tRight to Left"; 184 | 185 | /* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */ 186 | "Rqc-34-cIF.title" = "Superscript"; 187 | 188 | /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ 189 | "Ruw-6m-B2m.title" = "Select All"; 190 | 191 | /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ 192 | "S0p-oC-mLd.title" = "Jump to Selection"; 193 | 194 | /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ 195 | "Td7-aD-5lo.title" = "Window"; 196 | 197 | /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ 198 | "UEZ-Bs-lqG.title" = "Capitalize"; 199 | 200 | /* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */ 201 | "VIY-Ag-zcb.title" = "Center"; 202 | 203 | /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ 204 | "Vdr-fp-XzO.title" = "Hide Others"; 205 | 206 | /* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */ 207 | "Vjx-xi-njq.title" = "Italic"; 208 | 209 | /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ 210 | "W48-6f-4Dl.title" = "Edit"; 211 | 212 | /* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */ 213 | "WRG-CD-K1S.title" = "Underline"; 214 | 215 | /* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ 216 | "Was-JA-tGl.title" = "New"; 217 | 218 | /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ 219 | "WeT-3V-zwk.title" = "Paste and Match Style"; 220 | 221 | /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ 222 | "Xz5-n4-O0W.title" = "Find…"; 223 | 224 | /* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ 225 | "YEy-JH-Tfz.title" = "Find and Replace…"; 226 | 227 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */ 228 | "YGs-j5-SAR.title" = "\tDefault"; 229 | 230 | /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ 231 | "Ynk-f8-cLZ.title" = "Start Speaking"; 232 | 233 | /* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */ 234 | "ZM1-6Q-yy1.title" = "Align Left"; 235 | 236 | /* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */ 237 | "ZvO-Gk-QUH.title" = "Paragraph"; 238 | 239 | /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ 240 | "aTl-1u-JFS.title" = "Print…"; 241 | 242 | /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ 243 | "aUF-d1-5bR.title" = "Window"; 244 | 245 | /* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */ 246 | "aXa-aM-Jaq.title" = "Font"; 247 | 248 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */ 249 | "agt-UL-0e3.title" = "Use Default"; 250 | 251 | /* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */ 252 | "bgn-CT-cEk.title" = "Show Colors"; 253 | 254 | /* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ 255 | "bib-Uj-vzu.title" = "File"; 256 | 257 | /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ 258 | "buJ-ug-pKt.title" = "Use Selection for Find"; 259 | 260 | /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ 261 | "c8a-y6-VQd.title" = "Transformations"; 262 | 263 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */ 264 | "cDB-IK-hbR.title" = "Use None"; 265 | 266 | /* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */ 267 | "cqv-fj-IhA.title" = "Selection"; 268 | 269 | /* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ 270 | "cwL-P1-jid.title" = "Smart Links"; 271 | 272 | /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ 273 | "d9M-CD-aMd.title" = "Make Lower Case"; 274 | 275 | /* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */ 276 | "d9c-me-L2H.title" = "Text"; 277 | 278 | /* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ 279 | "dMs-cI-mzQ.title" = "File"; 280 | 281 | /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ 282 | "dRJ-4n-Yzg.title" = "Undo"; 283 | 284 | /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ 285 | "gVA-U4-sdL.title" = "Paste"; 286 | 287 | /* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ 288 | "hQb-2v-fYv.title" = "Smart Quotes"; 289 | 290 | /* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ 291 | "hz2-CU-CR7.title" = "Check Document Now"; 292 | 293 | /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ 294 | "hz9-B4-Xy5.title" = "Services"; 295 | 296 | /* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */ 297 | "i1d-Er-qST.title" = "Smaller"; 298 | 299 | /* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */ 300 | "ijk-EB-dga.title" = "Baseline"; 301 | 302 | /* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */ 303 | "jBQ-r6-VK2.title" = "Kern"; 304 | 305 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */ 306 | "jFq-tB-4Kx.title" = "\tRight to Left"; 307 | 308 | /* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */ 309 | "jxT-CU-nIS.title" = "Format"; 310 | 311 | /* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */ 312 | "kIP-vf-haE.title" = "Show Sidebar"; 313 | 314 | /* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ 315 | "mK6-2p-4JG.title" = "Check Grammar With Spelling"; 316 | 317 | /* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */ 318 | "o6e-r0-MWq.title" = "Ligatures"; 319 | 320 | /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ 321 | "oas-Oc-fiZ.title" = "Open Recent"; 322 | 323 | /* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */ 324 | "ogc-rX-tC1.title" = "Loosen"; 325 | 326 | /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ 327 | "pa3-QI-u2k.title" = "Delete"; 328 | 329 | /* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ 330 | "pxx-59-PXV.title" = "Save…"; 331 | 332 | /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ 333 | "q09-fT-Sye.title" = "Find Next"; 334 | 335 | /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ 336 | "qIS-W8-SiK.title" = "Page Setup…"; 337 | 338 | /* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ 339 | "rbD-Rh-wIN.title" = "Check Spelling While Typing"; 340 | 341 | /* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ 342 | "rgM-f4-ycn.title" = "Smart Dashes"; 343 | 344 | /* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ 345 | "snW-S8-Cw5.title" = "Show Toolbar"; 346 | 347 | /* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ 348 | "tRr-pd-1PS.title" = "Data Detectors"; 349 | 350 | /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ 351 | "tXI-mr-wws.title" = "Open Recent"; 352 | 353 | /* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */ 354 | "tlD-Oa-oAM.title" = "Kern"; 355 | 356 | /* Class = "NSMenu"; title = "MinaOTP-MAC"; ObjectID = "uQy-DD-JDr"; */ 357 | "uQy-DD-JDr.title" = "MinaOTP-MAC"; 358 | 359 | /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ 360 | "uRl-iY-unG.title" = "Cut"; 361 | 362 | /* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */ 363 | "vKC-jM-MkH.title" = "Paste Style"; 364 | 365 | /* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */ 366 | "vLm-3I-IUL.title" = "Show Ruler"; 367 | 368 | /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ 369 | "vNY-rz-j42.title" = "Clear Menu"; 370 | 371 | /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ 372 | "vmV-6d-7jI.title" = "Make Upper Case"; 373 | 374 | /* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */ 375 | "w0m-vy-SC9.title" = "Ligatures"; 376 | 377 | /* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */ 378 | "wb2-vD-lq4.title" = "Align Right"; 379 | 380 | /* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ 381 | "wpr-3q-Mcd.title" = "Help"; 382 | 383 | /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ 384 | "x3v-GG-iWU.title" = "Copy"; 385 | 386 | /* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */ 387 | "xQD-1f-W4t.title" = "Use All"; 388 | 389 | /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ 390 | "xrE-MZ-jX0.title" = "Speech"; 391 | 392 | /* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ 393 | "z6F-FW-3nz.title" = "Show Substitutions"; 394 | -------------------------------------------------------------------------------- /MinaOTP-MAC/PopoverViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopoverViewController.swift 3 | // MinaOTP-MAC 4 | // 5 | // Created by 武建明 on 2018/8/1. 6 | // Copyright © 2018年 Four_w. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class PopoverViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, NSMenuDelegate, NSPopoverDelegate{ 12 | 13 | 14 | let importButton = CustomFlatButton().customFlatButton(frame: CGRect(x: 12, y: 365, width: 48, height: 24), title: NSLocalizedString("import", comment: "")) 15 | let exportButton = CustomFlatButton().customFlatButton(frame: CGRect(x: 72, y: 365, width: 48, height: 24), title: NSLocalizedString("export", comment: "")) 16 | let addButton = CustomFlatButton().customFlatButton(frame: CGRect(x: 240, y: 365, width: 48, height: 24), title: NSLocalizedString("add", comment: "add")) 17 | var totpArray = [String]() 18 | var oldTimeStamp = 0 19 | let kCellHeight = CGFloat(60) 20 | var clickedRow: Int = -1 21 | 22 | override func loadView() { 23 | self.view = NSView(frame: CGRect(x: 0, y: 0, width: 300, height: 400)) 24 | self.view.wantsLayer = true 25 | self.view.layer?.backgroundColor = NSColor.clear.cgColor 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | self.config() 31 | DataManager.initial { [weak self] in 32 | self?.reloadData() 33 | } 34 | 35 | NotificationCenter.default.addObserver(self, selector: #selector(notificationAction), name: NSNotification.Name(rawValue:"reloadData"), object: nil) 36 | NotificationCenter.default.addObserver(self, selector: #selector(notificationAction), name: NSNotification.Name(rawValue:"addData"), object: nil) 37 | // NotificationCenter.default.addObserver(self, selector: #selector(didScroll), name: NSView.boundsDidChangeNotification, object: totpTableView.enclosingScrollView?.contentView) 38 | } 39 | override func awakeFromNib() { 40 | } 41 | //MARK: - Lazy 42 | struct OtpModel:Codable { 43 | var secret:String 44 | var issuer:String 45 | var remark:String 46 | } 47 | lazy var editPopoverView : NSPopover = { 48 | let pop = NSPopover() 49 | pop.behavior = .transient 50 | //pop.appearance = NSAppearance.init(named: .vibrantLight) 51 | pop.delegate = self 52 | return pop 53 | }() 54 | lazy var totpTableView: NSTableView = { 55 | let tab = NSTableView.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) 56 | tab.delegate = self; 57 | tab.dataSource = self; 58 | tab.enclosingScrollView?.drawsBackground = false 59 | tab.backgroundColor = .clear 60 | tab.headerView = NSTableHeaderView.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) 61 | let column1 = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "column1")) 62 | column1.width = 300 63 | tab.addTableColumn(column1) 64 | tab.target = self 65 | tab.doubleAction = #selector(self.tableViewDoubleClick) 66 | let menu = NSMenu() 67 | menu.delegate = self 68 | tab.menu = menu 69 | tab.draggingDestinationFeedbackStyle = .gap 70 | return tab 71 | }() 72 | lazy var bgScrollView: NSScrollView = { 73 | let scrollView = NSScrollView.init(frame: CGRect(x: 0, y: 0, width: 300, height: 350)) 74 | scrollView.contentView.backgroundColor = .clear 75 | scrollView.documentView = totpTableView 76 | totpTableView.enclosingScrollView?.drawsBackground = false 77 | return scrollView 78 | }() 79 | lazy var progressView:ProgressView = { 80 | let view = ProgressView.init(frame: CGRect(x: 0, y: 350, width: 300, height: 2)) 81 | view.layer?.backgroundColor = NSColor.clear.cgColor 82 | return view 83 | }() 84 | lazy var timer:Timer! = { 85 | let t = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true) 86 | RunLoop.main.add(t, forMode: RunLoop.Mode.common) 87 | return t 88 | }() 89 | private func config() { 90 | self.view.addSubview(importButton) 91 | self.view.addSubview(exportButton) 92 | self.view.addSubview(addButton) 93 | self.view.addSubview(self.bgScrollView) 94 | self.view.addSubview(self.progressView) 95 | importButton.target = self 96 | exportButton.target = self 97 | addButton.target = self 98 | importButton.action = #selector(self.importButtonAction) 99 | exportButton.action = #selector(self.exportButtonAction) 100 | addButton.action = #selector(self.addButtonAction(button:)) 101 | 102 | 103 | totpTableView.registerForDraggedTypes([NSPasteboard.PasteboardType.string]) 104 | } 105 | // MARK: - Action 106 | func menuNeedsUpdate(_ menu: NSMenu) { 107 | menu.removeAllItems() 108 | clickedRow = totpTableView.clickedRow 109 | menu.addItem(withTitle: NSLocalizedString("delete", comment: ""), action: #selector(deleteRowAction), keyEquivalent: "") 110 | menu.addItem(withTitle: NSLocalizedString("edit", comment: ""), action: #selector(editRowAction), keyEquivalent: "") 111 | } 112 | func menuWillOpen(_ menu: NSMenu) { 113 | print("menuWillOpen") 114 | stopTimer() 115 | } 116 | func menuDidClose(_ menu: NSMenu) { 117 | print("menuDidClose") 118 | startTimer() 119 | } 120 | func reloadData() { 121 | totpArray.removeAll() 122 | totpArray = DataManager.get() 123 | totpTableView.reloadData() 124 | } 125 | func startTimer() { 126 | print("计时器开始") 127 | self.reloadData() 128 | if totpArray.count > 0 { 129 | totpTableView.reloadData() 130 | oldTimeStamp = Int(Date().timeIntervalSince1970)/30 131 | self.timer.fireDate = Date.distantPast 132 | }else{ 133 | self.progressView.setProgress(value: 0.00) 134 | } 135 | } 136 | func stopTimer() { 137 | print("计时器停止") 138 | self.timer.fireDate = Date.distantFuture 139 | } 140 | @objc func notificationAction(nofi : Notification){ 141 | let type = nofi.userInfo!["type"] 142 | if (type as! String) == "edit_save" { 143 | ShowTips().showTip(message: "修改成功", view: self.view) 144 | editPopoverView.close() 145 | }else if (type as! String) == "reload" { 146 | startTimer() 147 | }else if (type as! String) == "close" { 148 | stopTimer() 149 | }else if (type as! String) == "edit_cancel" { 150 | editPopoverView.close() 151 | }else if (type as! String) == "add_success_from_textfield" { 152 | self.totpTableView.scrollRowToVisible(totpArray.count-1) 153 | } 154 | } 155 | func popoverWillClose(_ notification: Notification) { 156 | print("popoverWillClose") 157 | startTimer() 158 | } 159 | func popoverWillShow(_ notification: Notification) { 160 | print("popoverWillShow") 161 | stopTimer() 162 | } 163 | @objc func timerAction() { 164 | let nowTimeStamp = Int(Date().timeIntervalSince1970)/30 165 | let progress = Date().timeIntervalSince1970/30-Double(nowTimeStamp) 166 | self.progressView.setProgress(value: CGFloat(progress)) 167 | if nowTimeStamp != self.oldTimeStamp{ 168 | totpTableView.reloadData() 169 | oldTimeStamp = nowTimeStamp 170 | } 171 | } 172 | @objc func deleteRowAction() { 173 | print("deleteRowAction") 174 | print(clickedRow) 175 | totpArray.remove(at: clickedRow) 176 | totpTableView.removeRows(at: [clickedRow], withAnimation: .slideRight) 177 | DataManager.save(self.totpArray) 178 | } 179 | @objc func editRowAction() { 180 | stopTimer() 181 | let rect = totpTableView.rect(ofRow: clickedRow) 182 | let super_rect = totpTableView.convert(rect, to: self.view) 183 | let editVC = EditPopoverViewController() 184 | editVC.editRow = clickedRow 185 | self.editPopoverView.contentViewController = editVC 186 | self.editPopoverView.show(relativeTo: super_rect, of: self.view, preferredEdge: NSRectEdge.maxX) 187 | 188 | } 189 | @objc func tableViewDoubleClick() { 190 | print(totpTableView.selectedRow) 191 | let view = totpTableView.view(atColumn: 0, row: totpTableView.selectedRow, makeIfNecessary: false) 192 | let pasteboard = NSPasteboard.general 193 | pasteboard.declareTypes([.string], owner: nil) 194 | let res = pasteboard.setString((view as! CellView).codeTextField.stringValue, forType: .string) 195 | if res { 196 | ShowTips().showTip(message: NSLocalizedString("copy_success", comment: ""), view: self.view) 197 | } 198 | } 199 | @objc func importButtonAction() { 200 | let openPanel = NSOpenPanel.init() 201 | openPanel.prompt = NSLocalizedString("choose_title", comment: "") 202 | openPanel.allowedFileTypes = ["json"] 203 | openPanel.allowsMultipleSelection = false 204 | openPanel.canChooseDirectories = false 205 | openPanel.canCreateDirectories = false 206 | openPanel.canChooseFiles = true 207 | let code = openPanel.runModal() 208 | if code.rawValue == 1{ 209 | let jsonData = NSData.init(contentsOf: openPanel.url!) 210 | let decoder = JSONDecoder() 211 | do { 212 | let otpModelArray = try decoder.decode([OtpModel].self, from: jsonData! as Data) 213 | 214 | // 将数据保存到UserDefaults 215 | var allItems = DataManager.get() 216 | 217 | for item in otpModelArray{ 218 | let otp = Tools().totpStringFormat(remark: item.remark, issuer: item.issuer, secret: item.secret) 219 | allItems.append(otp) 220 | } 221 | DataManager.save(allItems) 222 | Tools().showAlert(message: NSLocalizedString("add_success_tip", comment: "")) 223 | 224 | } catch let error{ 225 | print(error) 226 | Tools().showAlert(message: NSLocalizedString("parse_failed", comment: "")) 227 | } 228 | } 229 | } 230 | 231 | @objc func exportButtonAction() { 232 | let savePanel = NSSavePanel() 233 | savePanel.title = NSLocalizedString("export_title", comment: "") 234 | savePanel.nameFieldStringValue = "minaOTP.json" 235 | savePanel.canCreateDirectories = true 236 | savePanel.isExtensionHidden = false 237 | let i = savePanel.runModal() 238 | if i == NSApplication.ModalResponse.OK { 239 | 240 | // 将数据保存到UserDefaults 241 | let allItems = DataManager.get() 242 | var temArray = [Any]() 243 | for item in allItems{ 244 | let otpDic = Tools().totpDictionaryFormat(code: item) 245 | temArray.append(otpDic) 246 | } 247 | let temJsonData = try! JSONSerialization.data(withJSONObject: temArray, options: .prettyPrinted) 248 | let temJsonStr = String.init(data: temJsonData, encoding: .utf8) 249 | 250 | do { 251 | try temJsonStr?.write(to: savePanel.url!, atomically: true, encoding: .utf8) 252 | Tools().showAlert(message: NSLocalizedString("export_sucess", comment: "")) 253 | } catch { 254 | Tools().showAlert(message: NSLocalizedString("export_failure", comment: "")) 255 | } 256 | } 257 | } 258 | @objc func addButtonAction(button: NSButton) { 259 | print("addButtonAction") 260 | let temWindow = AddWindow.init(contentRect: NSRect(x: 0, y: 0, width: 480, height: 320), styleMask: .closable, backing: .buffered, defer: true) 261 | let windowVC = NSWindowController.init(window: temWindow) 262 | windowVC.window?.makeKeyAndOrderFront(nil) 263 | NSApp.activate(ignoringOtherApps: true) 264 | } 265 | 266 | // MARK: - TableViewDelegate,TableViewDataSource 267 | func numberOfRows(in tableView: NSTableView) -> Int { 268 | return totpArray.count 269 | } 270 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { 271 | return kCellHeight 272 | } 273 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 274 | 275 | var view = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "cellIdentifier"), owner: self) 276 | if (view==nil) { 277 | view = CellView.init(frame: CGRect(x: 0, y: 0, width: 400, height: kCellHeight)) 278 | (view as! CellView).identifier = NSUserInterfaceItemIdentifier(rawValue: "cellIdentifier"); 279 | } 280 | let totpDic = Tools().totpDictionaryFormat(code: totpArray[row]) 281 | (view as! CellView).remarkTextField.stringValue = (totpDic["remark"] as? String)! 282 | (view as! CellView).issuerTextField.stringValue = (totpDic["issuer"] as? String)! 283 | (view as! CellView).codeTextField.stringValue = GeneratorTotp.generateOTP(forSecret: totpDic["secret"] as? String) 284 | // (view as! CellView).hotKeyTextField.stringValue = "⌘\(row)" 285 | 286 | return view; 287 | } 288 | 289 | func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { 290 | let tableRowView = CSTableRowView() 291 | tableRowView.isGroupRowStyle = false 292 | return tableRowView 293 | } 294 | 295 | func tableView(_ tableView: NSTableView, shouldEdit tableColumn: NSTableColumn?, row: Int) -> Bool { 296 | return true 297 | } 298 | // 299 | // func tableView(_ tableView: NSTableView, didDrag tableColumn: NSTableColumn) { 300 | // print("拖动") 301 | // } 302 | 303 | func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool { 304 | let data = NSKeyedArchiver.archivedData(withRootObject: rowIndexes) 305 | pboard.declareTypes([NSPasteboard.PasteboardType.string], owner: self) 306 | pboard.setData(data, forType: NSPasteboard.PasteboardType.string) 307 | return true 308 | } 309 | func tableView(tableView: NSTableView, writeRowsWithIndexes rowIndexes: NSIndexSet, toPasteboard pboard: NSPasteboard) -> Bool { 310 | pboard.declareTypes([NSPasteboard.PasteboardType.string], owner: self) 311 | pboard.setString("currencyCode", forType: NSPasteboard.PasteboardType.string) 312 | return true 313 | } 314 | 315 | func tableView(_ tableView: NSTableView, didDrag tableColumn: NSTableColumn) { 316 | print("asasasdad") 317 | } 318 | func tableView(_ tableView: NSTableView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forRowIndexes rowIndexes: IndexSet) { 319 | stopTimer() 320 | print("开始拖动") 321 | } 322 | func tableView(_ tableView: NSTableView, updateDraggingItemsForDrag draggingInfo: NSDraggingInfo) { 323 | print("拖动结束") 324 | } 325 | func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation { 326 | 327 | if dropOperation == .above { 328 | print("插入") 329 | return .move 330 | } 331 | return [] 332 | } 333 | func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool { 334 | 335 | if info.draggingSource as! NSTableView == totpTableView{ 336 | let data = info.draggingPasteboard.data(forType: NSPasteboard.PasteboardType.string) 337 | let rowIndexes:NSIndexSet = NSKeyedUnarchiver.unarchiveObject(with: data!) as! NSIndexSet 338 | 339 | if rowIndexes.firstIndex == row{ 340 | totpTableView.reloadData() 341 | startTimer() 342 | return true 343 | } 344 | let value:String = totpArray[rowIndexes.firstIndex] 345 | totpArray.remove(at: rowIndexes.firstIndex) 346 | if rowIndexes.firstIndex < row{ 347 | totpArray.insert(value, at: row-1) 348 | }else{ 349 | totpArray.insert(value, at: row) 350 | } 351 | DataManager.save(totpArray) 352 | totpTableView.reloadData() 353 | startTimer() 354 | return true 355 | } 356 | return false 357 | 358 | } 359 | } 360 | --------------------------------------------------------------------------------