From 02d9ac3031ca87cb7ae0b2d2ae63ca7eca76e65d Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Sun, 16 Sep 2018 16:46:48 +0800 Subject: [PATCH] Improve sharing server profiles. * New share server profiles window. * Import server profile urls from pasteboard. --- README.md | 19 +- ShadowsocksX-NG.xcodeproj/project.pbxproj | 22 +- ShadowsocksX-NG/AppDelegate.swift | 69 +++++- .../Base.lproj/Localizable.strings | 4 +- ShadowsocksX-NG/Base.lproj/MainMenu.xib | 31 ++- .../PreferencesWindowController.xib | 233 ++++++------------ .../ShareServerProfilesWindowController.xib | 163 ++++++++++++ .../PreferencesWindowController.swift | 7 +- .../ShareServerProfilesWindowController.swift | 189 ++++++++++++++ ShadowsocksX-NG/Utils.h | 2 + ShadowsocksX-NG/Utils.m | 40 +++ .../zh-Hans.lproj/Localizable.strings | 4 +- .../zh-Hans.lproj/MainMenu.strings | 8 +- ...hareServerProfilesWindowController.strings | 19 ++ 14 files changed, 605 insertions(+), 205 deletions(-) create mode 100644 ShadowsocksX-NG/Base.lproj/ShareServerProfilesWindowController.xib create mode 100644 ShadowsocksX-NG/ShareServerProfilesWindowController.swift create mode 100644 ShadowsocksX-NG/zh-Hans.lproj/ShareServerProfilesWindowController.strings diff --git a/README.md b/README.md index feeb44c..c5f1c9f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Current version is 1.8.0 Next Generation of [ShadowsocksX](https://github.com/shadowsocks/shadowsocks-iOS) -## Why? +## Why a new implementation? It's hard to maintain the original implementation as there is too much unused code in it. It also embeds the `ss-local` source. It's crazy to maintain dependencies of `ss-local`. @@ -36,21 +36,12 @@ From [here](https://github.com/shadowsocks/ShadowsocksX-NG/releases/) - `ss-local` from shadowsocks-libev 3.2.0 - Support SIP003 plugins. Embed `kcptun` and `simple-obfs`. - Could update PAC by download GFW List from GitHub. -- Shows QRCode for current server profile in legacy and SIP002 format. -- Scans QRCode from screen. -- Auto launch at login. -- User rules for PAC. +- Share your server profiles by qrcode or url. +- Import server profile urls from pasteboard. +- Import server profile by scan QRCode on screen. +- Custom rules for PAC. - Support for [AEAD Ciphers](https://shadowsocks.org/en/spec/AEAD-Ciphers.html) - HTTP Proxy by [privoxy](http://www.privoxy.org/) -- An advanced preferences panel for configuring: - - Local SOCKS5 listen address. - - Local SOCKS5 listen port. - - Local SOCKS5 timeout. - - If enable UDP relay. - - GFW List URL. -- Manually specify network service profiles which would be used to configure the proxy. -- Could reorder shadowsocks profiles by drag-&-dropping in servers preferences panel. -- Configurable global shortcuts for toggle running and switch proxy mode. ## Difference from original ShadowsocksX diff --git a/ShadowsocksX-NG.xcodeproj/project.pbxproj b/ShadowsocksX-NG.xcodeproj/project.pbxproj index 73e0f11..d3f372c 100755 --- a/ShadowsocksX-NG.xcodeproj/project.pbxproj +++ b/ShadowsocksX-NG.xcodeproj/project.pbxproj @@ -46,10 +46,11 @@ 9B5832031E741F8D009D5B7D /* command-512.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B5832021E741F8D009D5B7D /* command-512.png */; }; 9B5832071E7421B2009D5B7D /* virtual-server-icon-3.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B5832061E7421B2009D5B7D /* virtual-server-icon-3.png */; }; 9B58320B1E7422DB009D5B7D /* http.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B58320A1E7422DB009D5B7D /* http.png */; }; - 9B5832111E742632009D5B7D /* kcptun_1.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B5832101E742632009D5B7D /* kcptun_1.png */; }; 9B5AA0AC209C43C200E8B659 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 9B5AA0AB209C43C200E8B659 /* Credits.rtf */; }; 9B6BF9541E27B2570061B9A7 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B6BF9531E27B2570061B9A7 /* ServiceManagement.framework */; }; 9B7297E7214D69C300FD24AA /* libmbedcrypto.2.12.0.dylib in Resources */ = {isa = PBXBuildFile; fileRef = 9B7297E5214D68F800FD24AA /* libmbedcrypto.2.12.0.dylib */; }; + 9B7297EA214D7C6B00FD24AA /* ShareServerProfilesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7297E8214D7C6B00FD24AA /* ShareServerProfilesWindowController.swift */; }; + 9B7297EC214DA88A00FD24AA /* ShareServerProfilesWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9B7297EE214DA88A00FD24AA /* ShareServerProfilesWindowController.xib */; }; 9B86459D1E7C2CAD00A84029 /* ProxyInterfacesViewCtrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B86459C1E7C2CAD00A84029 /* ProxyInterfacesViewCtrl.swift */; }; 9B938D991E864B38005F5636 /* menu_g_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B938D931E864B38005F5636 /* menu_g_icon.png */; }; 9B938D9A1E864B38005F5636 /* menu_g_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B938D941E864B38005F5636 /* menu_g_icon@2x.png */; }; @@ -195,12 +196,14 @@ 9B5832021E741F8D009D5B7D /* command-512.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "command-512.png"; sourceTree = ""; }; 9B5832061E7421B2009D5B7D /* virtual-server-icon-3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "virtual-server-icon-3.png"; sourceTree = ""; }; 9B58320A1E7422DB009D5B7D /* http.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = http.png; sourceTree = ""; }; - 9B5832101E742632009D5B7D /* kcptun_1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = kcptun_1.png; sourceTree = ""; }; 9B5AA09F209C100C00E8B659 /* libsodium.23.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libsodium.23.dylib; sourceTree = ""; }; 9B5AA0A2209C103900E8B659 /* libcares.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libcares.2.dylib; sourceTree = ""; }; 9B5AA0AB209C43C200E8B659 /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 9B6BF9531E27B2570061B9A7 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; 9B7297E5214D68F800FD24AA /* libmbedcrypto.2.12.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libmbedcrypto.2.12.0.dylib; sourceTree = ""; }; + 9B7297E8214D7C6B00FD24AA /* ShareServerProfilesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareServerProfilesWindowController.swift; sourceTree = ""; }; + 9B7297ED214DA88A00FD24AA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ShareServerProfilesWindowController.xib; sourceTree = ""; }; + 9B7297F0214DA89000FD24AA /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ShareServerProfilesWindowController.strings"; sourceTree = ""; }; 9B86459C1E7C2CAD00A84029 /* ProxyInterfacesViewCtrl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyInterfacesViewCtrl.swift; sourceTree = ""; }; 9B938D931E864B38005F5636 /* menu_g_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_g_icon.png; sourceTree = ""; }; 9B938D941E864B38005F5636 /* menu_g_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_g_icon@2x.png"; sourceTree = ""; }; @@ -308,7 +311,6 @@ 9B938D961E864B38005F5636 /* menu_m_icon@2x.png */, 9B938D971E864B38005F5636 /* menu_p_icon.png */, 9B938D981E864B38005F5636 /* menu_p_icon@2x.png */, - 9B5832101E742632009D5B7D /* kcptun_1.png */, 9B58320A1E7422DB009D5B7D /* http.png */, 9B5832061E7421B2009D5B7D /* virtual-server-icon-3.png */, 9B5832021E741F8D009D5B7D /* command-512.png */, @@ -411,6 +413,8 @@ 9B86459C1E7C2CAD00A84029 /* ProxyInterfacesViewCtrl.swift */, 9B3546701E802B1200B510B4 /* ToastWindowController.swift */, 9B3546711E802B1200B510B4 /* ToastWindowController.xib */, + 9B7297E8214D7C6B00FD24AA /* ShareServerProfilesWindowController.swift */, + 9B7297EE214DA88A00FD24AA /* ShareServerProfilesWindowController.xib */, ); name = UI; sourceTree = ""; @@ -622,7 +626,6 @@ C6D429971DA75988002A5711 /* stop_privoxy.sh in Resources */, C8E42A6E1D4F2CAF0074C7EA /* UserRulesController.xib in Resources */, 9BEEF06A1D04D4D500FC52B3 /* start_ss_local.sh in Resources */, - 9B5832111E742632009D5B7D /* kcptun_1.png in Resources */, 9B16E59A1F99FD0700E54DC5 /* icons8-Eye Filled-50.png in Resources */, 9B938D9C1E864B38005F5636 /* menu_m_icon@2x.png in Resources */, 9B3546731E802B1200B510B4 /* ToastWindowController.xib in Resources */, @@ -638,6 +641,7 @@ 1C82DBA81FA96C7500B32551 /* obfs-local in Resources */, 9B938D9D1E864B38005F5636 /* menu_p_icon.png in Resources */, 9B938D9B1E864B38005F5636 /* menu_m_icon.png in Resources */, + 9B7297EC214DA88A00FD24AA /* ShareServerProfilesWindowController.xib in Resources */, 9B3FFF271D0898EB0019A709 /* gfwlist.txt in Resources */, C6D429931DA75988002A5711 /* install_privoxy.sh in Resources */, 9B58320B1E7422DB009D5B7D /* http.png in Resources */, @@ -818,6 +822,7 @@ 9B5831F61E7302F8009D5B7D /* ShortcutsController.m in Sources */, 9BB706A71D1B982300551F0E /* SWBApplication.m in Sources */, 9B3FFF1E1D0732660019A709 /* Utils.m in Sources */, + 9B7297EA214D7C6B00FD24AA /* ShareServerProfilesWindowController.swift in Sources */, 9B3FFF321D08CEE40019A709 /* SWBQRCodeWindowController.m in Sources */, 9B3FFF211D08826E0019A709 /* PACUtils.swift in Sources */, 9B3FFF141D0705810019A709 /* Notifications.swift in Sources */, @@ -894,6 +899,15 @@ name = PreferencesWindowController.xib; sourceTree = ""; }; + 9B7297EE214DA88A00FD24AA /* ShareServerProfilesWindowController.xib */ = { + isa = PBXVariantGroup; + children = ( + 9B7297ED214DA88A00FD24AA /* Base */, + 9B7297F0214DA89000FD24AA /* zh-Hans */, + ); + name = ShareServerProfilesWindowController.xib; + sourceTree = ""; + }; 9BAFE2E41E83ED7F00F71CCE /* PreferencesWinController.xib */ = { isa = PBXVariantGroup; children = ( diff --git a/ShadowsocksX-NG/AppDelegate.swift b/ShadowsocksX-NG/AppDelegate.swift index 8286d15..5709662 100755 --- a/ShadowsocksX-NG/AppDelegate.swift +++ b/ShadowsocksX-NG/AppDelegate.swift @@ -14,6 +14,7 @@ import RxSwift @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate { + var shareWinCtrl: ShareServerProfilesWindowController! var qrcodeWinCtrl: SWBQRCodeWindowController! var preferencesWinCtrl: PreferencesWindowController! var editUserRulesWinCtrl: UserRulesController! @@ -279,9 +280,50 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele } } + @IBAction func showShareServerProfiles(_ sender: NSMenuItem) { + if shareWinCtrl != nil { + shareWinCtrl.close() + } + shareWinCtrl = ShareServerProfilesWindowController(windowNibName: NSNib.Name(rawValue: "ShareServerProfilesWindowController")) + shareWinCtrl.showWindow(self) + NSApp.activate(ignoringOtherApps: true) + shareWinCtrl.window?.makeKeyAndOrderFront(nil) + } + @IBAction func scanQRCodeFromScreen(_ sender: NSMenuItem) { ScanQRCodeOnScreen() } + + @IBAction func importProfileURLFromPasteboard(_ sender: NSMenuItem) { + let pb = NSPasteboard.general + if #available(OSX 10.13, *) { + if let text = pb.string(forType: NSPasteboard.PasteboardType.URL) { + if let url = URL(string: text) { + NotificationCenter.default.post( + name: Notification.Name(rawValue: "NOTIFY_FOUND_SS_URL"), object: nil + , userInfo: [ + "urls": [url], + "source": "pasteboard", + ]) + } + } + } + if let text = pb.string(forType: NSPasteboard.PasteboardType.string) { + var urls = text.split(separator: "\n") + .map { String($0).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } + .map { URL(string: $0) } + .filter { $0 != nil } + .map { $0! } + urls = urls.filter { $0.scheme == "ss" } + + NotificationCenter.default.post( + name: Notification.Name(rawValue: "NOTIFY_FOUND_SS_URL"), object: nil + , userInfo: [ + "urls": urls, + "source": "pasteboard", + ]) + } + } @IBAction func selectPACMode(_ sender: NSMenuItem) { let defaults = UserDefaults.standard @@ -528,29 +570,30 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele let urls: [URL] = userInfo["urls"] as! [URL] let mgr = ServerProfileManager.instance - var isChanged = false + var addCount = 0 + + var subtitle: String = "" + if userInfo["source"] as! String == "qrcode" { + subtitle = "By scan QR Code".localized + } else if userInfo["source"] as! String == "url" { + subtitle = "By handle SS URL".localized + } else if userInfo["source"] as! String == "pasteboard" { + subtitle = "By import from pasteboard".localized + } for url in urls { if let profile = ServerProfile(url: url) { mgr.profiles.append(profile) - isChanged = true - - var subtitle: String = "" - if userInfo["source"] as! String == "qrcode" { - subtitle = "By scan QR Code".localized - } else if userInfo["source"] as! String == "url" { - subtitle = "By Handle SS URL".localized - } - - sendNotify("Add Shadowsocks Server Profile".localized, subtitle, "Host: \(profile.serverHost)") + addCount = addCount + 1 } } - if isChanged { + if addCount > 0 { + sendNotify("Add \(addCount) Shadowsocks Server Profile".localized, subtitle, "") mgr.save() self.updateServersMenu() } else { - sendNotify("Not found valid qrcode of shadowsocks profile.", "", "") + sendNotify("", "", "Not found valid qrcode or url of shadowsocks profile".localized) } } } diff --git a/ShadowsocksX-NG/Base.lproj/Localizable.strings b/ShadowsocksX-NG/Base.lproj/Localizable.strings index c3ce111..fd28729 100755 --- a/ShadowsocksX-NG/Base.lproj/Localizable.strings +++ b/ShadowsocksX-NG/Base.lproj/Localizable.strings @@ -20,7 +20,9 @@ * ./AppDelegate.swift */ -"Add Shadowsocks Server Profile" = "Add Shadowsocks Server Profile"; +"Add \(addCount) Shadowsocks Server Profile" = "Add \(addCount) Shadowsocks Server Profile"; + +"Not found valid qrcode or url of shadowsocks profile" = "Not found valid qrcode or url of shadowsocks profile"; "By scan QR Code" = "By scan QR Code"; diff --git a/ShadowsocksX-NG/Base.lproj/MainMenu.xib b/ShadowsocksX-NG/Base.lproj/MainMenu.xib index e1cfe86..3bde40b 100755 --- a/ShadowsocksX-NG/Base.lproj/MainMenu.xib +++ b/ShadowsocksX-NG/Base.lproj/MainMenu.xib @@ -23,7 +23,6 @@ - @@ -72,21 +71,27 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/ShadowsocksX-NG/Base.lproj/PreferencesWindowController.xib b/ShadowsocksX-NG/Base.lproj/PreferencesWindowController.xib index 2ed12ad..432778a 100644 --- a/ShadowsocksX-NG/Base.lproj/PreferencesWindowController.xib +++ b/ShadowsocksX-NG/Base.lproj/PreferencesWindowController.xib @@ -29,20 +29,21 @@ - + - + - - - - + + + + + - + @@ -88,97 +89,106 @@ - - - - + + + + + - - + + + - - - - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + @@ -191,17 +201,19 @@ - - + + + - + - + + @@ -212,11 +224,6 @@ - - - - - @@ -224,8 +231,9 @@ - + + @@ -233,20 +241,13 @@ - - - - - - - - + + + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/ShadowsocksX-NG/Base.lproj/ShareServerProfilesWindowController.xib b/ShadowsocksX-NG/Base.lproj/ShareServerProfilesWindowController.xib new file mode 100644 index 0000000..04117b1 --- /dev/null +++ b/ShadowsocksX-NG/Base.lproj/ShareServerProfilesWindowController.xib @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ShadowsocksX-NG/PreferencesWindowController.swift b/ShadowsocksX-NG/PreferencesWindowController.swift index 20e4cc4..f426446 100644 --- a/ShadowsocksX-NG/PreferencesWindowController.swift +++ b/ShadowsocksX-NG/PreferencesWindowController.swift @@ -36,8 +36,6 @@ class PreferencesWindowController: NSWindowController var profileMgr: ServerProfileManager! var editingProfile: ServerProfile! - - var enabledKcptunSubDisosable: Disposable? override func windowDidLoad() { @@ -207,10 +205,7 @@ class PreferencesWindowController: NSWindowController func bindProfile(_ index:Int) { NSLog("bind profile \(index)") - if let dis = enabledKcptunSubDisosable { - dis.dispose() - enabledKcptunSubDisosable = Optional.none - } + if index >= 0 && index < profileMgr.profiles.count { editingProfile = profileMgr.profiles[index] diff --git a/ShadowsocksX-NG/ShareServerProfilesWindowController.swift b/ShadowsocksX-NG/ShareServerProfilesWindowController.swift new file mode 100644 index 0000000..741ada1 --- /dev/null +++ b/ShadowsocksX-NG/ShareServerProfilesWindowController.swift @@ -0,0 +1,189 @@ +// +// ShareServerProfilesWindowController.swift +// ShadowsocksX-NG +// +// Created by 邱宇舟 on 2018/9/16. +// Copyright © 2018年 qiuyuzhou. All rights reserved. +// + +import Cocoa + +class ShareServerProfilesWindowController: NSWindowController + , NSTableViewDataSource, NSTableViewDelegate { + + @IBOutlet weak var profilesTableView: NSTableView! + + @IBOutlet weak var qrCodeImageView: NSImageView! + + @IBOutlet weak var copyAllServerURLsButton: NSButton! + @IBOutlet weak var saveAllServerURLsAsFileButton: NSButton! + + @IBOutlet weak var copyURLButton: NSButton! + @IBOutlet weak var copyQRCodeButton: NSButton! + @IBOutlet weak var saveQRCodeAsFileButton: NSButton! + + var defaults: UserDefaults! + var profileMgr: ServerProfileManager! + + override func windowDidLoad() { + super.windowDidLoad() + + defaults = UserDefaults.standard + profileMgr = ServerProfileManager.instance + + if !profileMgr.profiles.isEmpty { + let index = IndexSet(integer: 0) + profilesTableView.selectRowIndexes(index, byExtendingSelection: false) + } else { + copyAllServerURLsButton.isEnabled = false + saveAllServerURLsAsFileButton.isEnabled = false + copyURLButton.isEnabled = false + copyQRCodeButton.isEnabled = false + saveQRCodeAsFileButton.isEnabled = false + } + } + + @IBAction func copyURL(_ sender: NSButton) { + let profile = getSelectedProfile() + if profile.isValid(), let url = profile.URL() { + let pb = NSPasteboard.general + pb.clearContents() + if pb.writeObjects([url as NSPasteboardWriting]) { + NSLog("Copy URL to clipboard") + } else { + NSLog("Failed to copy URL to clipboard") + } + } + } + + @IBAction func copyQRCode(_ sender: NSButton) { + if let img = qrCodeImageView.image { + let pb = NSPasteboard.general + pb.clearContents() + if pb.writeObjects([img as NSPasteboardWriting]) { + NSLog("Copy QRCode to clipboard") + } else { + NSLog("Failed to copy QRCode to clipboard") + } + } + } + + @IBAction func saveQRCodeAsFile(_ sender: NSButton) { + if let img = qrCodeImageView.image { + let savePanel = NSSavePanel() + savePanel.title = "Save All Server URLs To File".localized + savePanel.canCreateDirectories = true + savePanel.allowedFileTypes = ["gif"] + savePanel.isExtensionHidden = false + + let profile = getSelectedProfile() + if profile.remark.isEmpty { + savePanel.nameFieldStringValue = "shadowsocks_qrcode.gif" + } else { + savePanel.nameFieldStringValue = "shadowsocks_qrcode_\(profile.remark).gif" + } + + savePanel.becomeKey() + let result = savePanel.runModal() + if (result.rawValue == NSFileHandlingPanelOKButton && (savePanel.url) != nil) { + let imgRep = NSBitmapImageRep(data: img.tiffRepresentation!) + let data = imgRep?.representation(using: NSBitmapImageRep.FileType.gif, properties: [:]) + try! data?.write(to: savePanel.url!) + } + } + } + + @IBAction func copyAllServerURLs(_ sender: NSButton) { + let pb = NSPasteboard.general + pb.clearContents() + if pb.writeObjects([getAllServerURLs() as NSPasteboardWriting]) { + NSLog("Copy all server URLs to clipboard") + } else { + NSLog("Failed to all server URLs to clipboard") + } + } + + @IBAction func saveAllServerURLsAsFile(_ sender: NSButton) { + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd" + let date_string = formatter.string(from: Date()) + + let savePanel = NSSavePanel() + savePanel.title = "Save All Server URLs To File".localized + savePanel.canCreateDirectories = true + savePanel.allowedFileTypes = ["txt"] + savePanel.isExtensionHidden = false + savePanel.nameFieldStringValue = "shadowsocks_profiles_\(date_string).txt" + savePanel.becomeKey() + let result = savePanel.runModal() + if (result.rawValue == NSFileHandlingPanelOKButton && (savePanel.url) != nil) { + let urls = getAllServerURLs() + try! urls.write(to: (savePanel.url)!, atomically: true, encoding: String.Encoding.utf8) + } + } + + func getAllServerURLs() -> String { + let urls = profileMgr.profiles.filter({ (profile) -> Bool in + return profile.isValid() + }).map { (profile) -> String in + return profile.URL()!.absoluteString + } + return urls.joined(separator: "\n") + } + + func getSelectedProfile() -> ServerProfile { + let i = profilesTableView.selectedRow + return profileMgr.profiles[i] + } + + func getDataAtRow(_ index:Int) -> String { + let profile = profileMgr.profiles[index] + if !profile.remark.isEmpty { + return profile.remark + } else { + return profile.serverHost + } + } + + //-------------------------------------------------- + // For NSTableViewDataSource + + func numberOfRows(in tableView: NSTableView) -> Int { + if let mgr = profileMgr { + return mgr.profiles.count + } + return 0 + } + + //-------------------------------------------------- + // For NSTableViewDelegate + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let colId = NSUserInterfaceItemIdentifier(rawValue: "cellTitle") + if let cell = tableView.makeView(withIdentifier: colId, owner: self) as? NSTableCellView { + cell.textField?.stringValue = getDataAtRow(row) + return cell + } + return nil + } + + func tableViewSelectionDidChange(_ notification: Notification) { + if profilesTableView.selectedRow >= 0 { + let profile = getSelectedProfile() + if profile.isValid(), let url = profile.URL() { + let img = createQRImage(url.absoluteString, NSMakeSize(250, 250)) + qrCodeImageView.image = img + + copyURLButton.isEnabled = true + copyQRCodeButton.isEnabled = true + saveQRCodeAsFileButton.isEnabled = true + return + } + } + qrCodeImageView.image = nil + + copyURLButton.isEnabled = false + copyQRCodeButton.isEnabled = false + saveQRCodeAsFileButton.isEnabled = false + } +} diff --git a/ShadowsocksX-NG/Utils.h b/ShadowsocksX-NG/Utils.h index 9e9110f..9c30936 100644 --- a/ShadowsocksX-NG/Utils.h +++ b/ShadowsocksX-NG/Utils.h @@ -11,4 +11,6 @@ void ScanQRCodeOnScreen(); +NSImage* createQRImage(NSString *string, NSSize size); + #endif /* QRCodeUtils_h */ diff --git a/ShadowsocksX-NG/Utils.m b/ShadowsocksX-NG/Utils.m index 92912c9..c5a6292 100644 --- a/ShadowsocksX-NG/Utils.m +++ b/ShadowsocksX-NG/Utils.m @@ -8,6 +8,7 @@ #import #import +#import void ScanQRCodeOnScreen() { /* displays[] Quartz display ID's */ @@ -75,3 +76,42 @@ void ScanQRCodeOnScreen() { } ]; } + +NSImage* createQRImage(NSString *string, NSSize size) { + NSImage *outputImage = [[NSImage alloc]initWithSize:size]; + [outputImage lockFocus]; + + // Setup the QR filter with our string + CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"]; + [filter setDefaults]; + + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + [filter setValue:data forKey:@"inputMessage"]; + /* + L: 7% + M: 15% + Q: 25% + H: 30% + */ + [filter setValue:@"Q" forKey:@"inputCorrectionLevel"]; + + CIImage *image = [filter valueForKey:@"outputImage"]; + + // Calculate the size of the generated image and the scale for the desired image size + CGRect extent = CGRectIntegral(image.extent); + CGFloat scale = MIN(size.width / CGRectGetWidth(extent), size.height / CGRectGetHeight(extent)); + + CGImageRef bitmapImage = [NSGraphicsContext.currentContext.CIContext createCGImage:image fromRect:extent]; + + CGContextRef graphicsContext = NSGraphicsContext.currentContext.CGContext; + + CGContextSetInterpolationQuality(graphicsContext, kCGInterpolationNone); + CGContextScaleCTM(graphicsContext, scale, scale); + CGContextDrawImage(graphicsContext, extent, bitmapImage); + + // Cleanup + CGImageRelease(bitmapImage); + + [outputImage unlockFocus]; + return outputImage; +} diff --git a/ShadowsocksX-NG/zh-Hans.lproj/Localizable.strings b/ShadowsocksX-NG/zh-Hans.lproj/Localizable.strings index 3dc0bbe..4a8f784 100755 --- a/ShadowsocksX-NG/zh-Hans.lproj/Localizable.strings +++ b/ShadowsocksX-NG/zh-Hans.lproj/Localizable.strings @@ -26,7 +26,9 @@ * ./AppDelegate.swift */ -"Add Shadowsocks Server Profile" = "已添加新Shadowsocks服务器配置"; +"Add \(addCount) Shadowsocks Server Profile" = "已添加\(addCount)个Shadowsocks服务器配置"; + +"Not found valid qrcode or url of shadowsocks profile" = "没有找到有效的shadowsocks配置二维码或链接"; "By scan QR Code" = "通过扫描二维码"; diff --git a/ShadowsocksX-NG/zh-Hans.lproj/MainMenu.strings b/ShadowsocksX-NG/zh-Hans.lproj/MainMenu.strings index 8662d88..3d88fbf 100644 --- a/ShadowsocksX-NG/zh-Hans.lproj/MainMenu.strings +++ b/ShadowsocksX-NG/zh-Hans.lproj/MainMenu.strings @@ -2,6 +2,9 @@ /* Class = "NSMenuItem"; title = "Preferences..."; ObjectID = "4CS-qD-zW5"; */ "4CS-qD-zW5.title" = "偏好设置..."; +/* Class = "NSMenuItem"; title = "Import Server URLs From Pasteboard"; ObjectID = "7Eq-XD-K5c"; */ +"7Eq-XD-K5c.title" = "从剪贴板导入服务器配置链接"; + /* Class = "NSMenuItem"; title = "应用用户自定规则到 PAC"; ObjectID = "6qf-cg-HXc"; */ "6qf-cg-HXc.title" = "应用用户自定规则到 PAC"; @@ -30,7 +33,7 @@ "Mw3-Jm-eXA.title" = "全局模式"; /* Class = "NSMenuItem"; title = "扫描屏幕上的二维码..."; ObjectID = "Qe6-bF-paT"; */ -"Qe6-bF-paT.title" = "扫描屏幕上的二维码..."; +"Qe6-bF-paT.title" = "扫描屏幕上的二维码"; /* Class = "NSMenuItem"; title = "显示当前服务器的二维码..."; ObjectID = "R6A-96-Zcb"; */ "R6A-96-Zcb.title" = "显示当前服务器的二维码..."; @@ -98,4 +101,5 @@ /* Class = "NSMenuItem"; title = "Preferences"; ObjectID = "iVn-LD-Ynd"; */ "iVn-LD-Ynd.title" = "偏好设置"; - +/* Class = "NSMenuItem"; title = "Share Server Profiles..."; ObjectID = "r5z-RB-LIZ"; */ +"r5z-RB-LIZ.title" = "分享服务器配置..."; diff --git a/ShadowsocksX-NG/zh-Hans.lproj/ShareServerProfilesWindowController.strings b/ShadowsocksX-NG/zh-Hans.lproj/ShareServerProfilesWindowController.strings new file mode 100644 index 0000000..8def4b0 --- /dev/null +++ b/ShadowsocksX-NG/zh-Hans.lproj/ShareServerProfilesWindowController.strings @@ -0,0 +1,19 @@ + +/* Class = "NSButtonCell"; title = "Save All Server URLs As File"; ObjectID = "9OS-xy-GB1"; */ +"9OS-xy-GB1.title" = "保存所有服务器链接到文件"; + +/* Class = "NSWindow"; title = "Share Server Profiles"; ObjectID = "F0z-JX-Cv5"; */ +"F0z-JX-Cv5.title" = "分享服务器配置"; + +/* Class = "NSButtonCell"; title = "Save QRCode As File"; ObjectID = "IaI-Rj-Kss"; */ +"IaI-Rj-Kss.title" = "保存二维码到文件"; + +/* Class = "NSButtonCell"; title = "Copy URL"; ObjectID = "PHX-gY-lZe"; */ +"PHX-gY-lZe.title" = "复制链接"; + +/* Class = "NSButtonCell"; title = "Copy QRCode"; ObjectID = "PrW-s6-Uab"; */ +"PrW-s6-Uab.title" = "复制二维码"; + +/* Class = "NSButtonCell"; title = "Copy All Server URLs"; ObjectID = "Yt2-p1-4w0"; */ +"Yt2-p1-4w0.title" = "复制所有服务器链接"; +