671 lines
24 KiB
Swift
Executable File
671 lines
24 KiB
Swift
Executable File
//
|
|
// AppDelegate.swift
|
|
// ShadowsocksX-NG
|
|
//
|
|
// Created by 邱宇舟 on 16/6/5.
|
|
// Copyright © 2016年 qiuyuzhou. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import Carbon
|
|
import RxCocoa
|
|
import RxSwift
|
|
|
|
@NSApplicationMain
|
|
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
|
|
|
|
var qrcodeWinCtrl: SWBQRCodeWindowController!
|
|
var preferencesWinCtrl: PreferencesWindowController!
|
|
var editUserRulesWinCtrl: UserRulesController!
|
|
var allInOnePreferencesWinCtrl: PreferencesWinController!
|
|
|
|
var launchAtLoginController: LaunchAtLoginController = LaunchAtLoginController()
|
|
|
|
@IBOutlet weak var window: NSWindow!
|
|
@IBOutlet weak var statusMenu: NSMenu!
|
|
|
|
@IBOutlet weak var runningStatusMenuItem: NSMenuItem!
|
|
@IBOutlet weak var toggleRunningMenuItem: NSMenuItem!
|
|
@IBOutlet weak var autoModeMenuItem: NSMenuItem!
|
|
@IBOutlet weak var globalModeMenuItem: NSMenuItem!
|
|
@IBOutlet weak var manualModeMenuItem: NSMenuItem!
|
|
|
|
@IBOutlet weak var serversMenuItem: NSMenuItem!
|
|
@IBOutlet var showQRCodeMenuItem: NSMenuItem!
|
|
@IBOutlet var scanQRCodeMenuItem: NSMenuItem!
|
|
@IBOutlet var showBunchJsonExampleFileItem: NSMenuItem!
|
|
@IBOutlet var importBunchJsonFileItem: NSMenuItem!
|
|
@IBOutlet var exportAllServerProfileItem: NSMenuItem!
|
|
@IBOutlet var serversPreferencesMenuItem: NSMenuItem!
|
|
|
|
@IBOutlet weak var copyHttpProxyExportCmdLineMenuItem: NSMenuItem!
|
|
|
|
@IBOutlet weak var lanchAtLoginMenuItem: NSMenuItem!
|
|
|
|
@IBOutlet weak var hudWindow: NSPanel!
|
|
@IBOutlet weak var panelView: NSView!
|
|
@IBOutlet weak var isNameTextField: NSTextField!
|
|
|
|
let kHudFadeInDuration: Double = 0.25
|
|
let kHudFadeOutDuration: Double = 0.5
|
|
let kHudDisplayDuration: Double = 2.0
|
|
|
|
let kHudAlphaValue: CGFloat = 0.75
|
|
let kHudCornerRadius: CGFloat = 18.0
|
|
let kHudHorizontalMargin: CGFloat = 30
|
|
let kHudHeight: CGFloat = 90.0
|
|
|
|
let kProfileMenuItemIndexBase = 100
|
|
|
|
var timerToFadeOut: Timer? = nil
|
|
var fadingOut: Bool = false
|
|
|
|
var statusItem: NSStatusItem!
|
|
|
|
static let StatusItemIconWidth:CGFloat = 20
|
|
|
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
// Insert code here to initialize your application
|
|
|
|
NSUserNotificationCenter.default.delegate = self
|
|
|
|
// Prepare ss-local
|
|
InstallSSLocal()
|
|
InstallKcptunClient()
|
|
InstallPrivoxy()
|
|
// Prepare defaults
|
|
let defaults = UserDefaults.standard
|
|
defaults.register(defaults: [
|
|
"ShadowsocksOn": true,
|
|
"ShadowsocksRunningMode": "auto",
|
|
"LocalSocks5.ListenPort": NSNumber(value: 1086 as UInt16),
|
|
"LocalSocks5.ListenAddress": "127.0.0.1",
|
|
"PacServer.ListenPort":NSNumber(value: 1089 as UInt16),
|
|
"LocalSocks5.Timeout": NSNumber(value: 60 as UInt),
|
|
"LocalSocks5.EnableUDPRelay": NSNumber(value: false as Bool),
|
|
"LocalSocks5.EnableVerboseMode": NSNumber(value: false as Bool),
|
|
"GFWListURL": "https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt",
|
|
"AutoConfigureNetworkServices": NSNumber(value: true as Bool),
|
|
"LocalHTTP.ListenAddress": "127.0.0.1",
|
|
"LocalHTTP.ListenPort": NSNumber(value: 1087 as UInt16),
|
|
"LocalHTTPOn": true,
|
|
"LocalHTTP.FollowGlobal": true,
|
|
"Kcptun.LocalHost": "127.0.0.1",
|
|
"Kcptun.LocalPort": NSNumber(value: 8388),
|
|
"Kcptun.Conn": NSNumber(value: 1),
|
|
"ShowRunningModeOnStatusBar": true,
|
|
])
|
|
|
|
statusItem = NSStatusBar.system().statusItem(withLength: AppDelegate.StatusItemIconWidth)
|
|
let image = NSImage(named: "menu_icon")
|
|
image?.isTemplate = true
|
|
statusItem.image = image
|
|
statusItem.menu = statusMenu
|
|
|
|
|
|
_ = defaults.rx.observe(Bool.self, "ShowRunningModeOnStatusBar")
|
|
// .distinctUntilChanged()
|
|
.subscribe(onNext: { value in
|
|
if let enabled = value {
|
|
self.updateStatusItemUI(isShownnRunningMode: enabled)
|
|
}
|
|
})
|
|
|
|
let notifyCenter = NotificationCenter.default
|
|
|
|
_ = notifyCenter.rx.notification(NOTIFY_CONF_CHANGED)
|
|
.subscribe(onNext: { noti in
|
|
SyncSSLocal()
|
|
self.applyConfig()
|
|
self.updateCopyHttpProxyExportMenu()
|
|
})
|
|
|
|
notifyCenter.addObserver(forName: NSNotification.Name(rawValue: NOTIFY_SERVER_PROFILES_CHANGED), object: nil, queue: nil
|
|
, using: {
|
|
(note) in
|
|
let profileMgr = ServerProfileManager.instance
|
|
if profileMgr.activeProfileId == nil &&
|
|
profileMgr.profiles.count > 0{
|
|
if profileMgr.profiles[0].isValid(){
|
|
profileMgr.setActiveProfiledId(profileMgr.profiles[0].uuid)
|
|
}
|
|
}
|
|
self.updateServersMenu()
|
|
self.updateRunningModeMenu()
|
|
SyncSSLocal()
|
|
}
|
|
)
|
|
_ = notifyCenter.rx.notification(NOTIFY_TOGGLE_RUNNING_SHORTCUT)
|
|
.subscribe(onNext: { noti in
|
|
self.doToggleRunning(showToast: true)
|
|
})
|
|
_ = notifyCenter.rx.notification(NOTIFY_SWITCH_PROXY_MODE_SHORTCUT)
|
|
.subscribe(onNext: { noti in
|
|
let mode = defaults.string(forKey: "ShadowsocksRunningMode")!
|
|
switch mode {
|
|
case "auto":
|
|
defaults.setValue("global", forKey: "ShadowsocksRunningMode")
|
|
self.isNameTextField.stringValue = "Global Mode".localized
|
|
case "global":
|
|
defaults.setValue("auto", forKey: "ShadowsocksRunningMode")
|
|
self.isNameTextField.stringValue = "Auto Mode By PAC".localized
|
|
default:
|
|
defaults.setValue("auto", forKey: "ShadowsocksRunningMode")
|
|
self.isNameTextField.stringValue = "Auto Mode By PAC".localized
|
|
}
|
|
|
|
self.updateRunningModeMenu()
|
|
self.applyConfig()
|
|
self.fadeInHud()
|
|
})
|
|
|
|
_ = notifyCenter.rx.notification(NOTIFY_FOUND_SS_URL)
|
|
.subscribe(onNext: { noti in
|
|
self.handleFoundSSURL(noti)
|
|
})
|
|
|
|
// Handle ss url scheme
|
|
NSAppleEventManager.shared().setEventHandler(self
|
|
, andSelector: #selector(self.handleURLEvent)
|
|
, forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
|
|
|
|
updateMainMenu()
|
|
updateCopyHttpProxyExportMenu()
|
|
updateServersMenu()
|
|
updateRunningModeMenu()
|
|
|
|
ProxyConfHelper.install()
|
|
ProxyConfHelper.startMonitorPAC()
|
|
applyConfig()
|
|
|
|
// Register global hotkey
|
|
ShortcutsController.bindShortcuts()
|
|
}
|
|
|
|
func applicationWillTerminate(_ aNotification: Notification) {
|
|
// Insert code here to tear down your application
|
|
StopSSLocal()
|
|
StopKcptun()
|
|
StopPrivoxy()
|
|
ProxyConfHelper.disableProxy()
|
|
}
|
|
|
|
func applyConfig() {
|
|
SyncSSLocal()
|
|
|
|
let defaults = UserDefaults.standard
|
|
let isOn = defaults.bool(forKey: "ShadowsocksOn")
|
|
let mode = defaults.string(forKey: "ShadowsocksRunningMode")
|
|
|
|
if isOn {
|
|
if mode == "auto" {
|
|
ProxyConfHelper.enablePACProxy()
|
|
} else if mode == "global" {
|
|
ProxyConfHelper.enableGlobalProxy()
|
|
} else if mode == "manual" {
|
|
ProxyConfHelper.disableProxy()
|
|
}
|
|
} else {
|
|
ProxyConfHelper.disableProxy()
|
|
}
|
|
}
|
|
|
|
// MARK: - UI Methods
|
|
@IBAction func toggleRunning(_ sender: NSMenuItem) {
|
|
self.doToggleRunning(showToast: false)
|
|
}
|
|
|
|
func doToggleRunning(showToast: Bool) {
|
|
let defaults = UserDefaults.standard
|
|
var isOn = UserDefaults.standard.bool(forKey: "ShadowsocksOn")
|
|
isOn = !isOn
|
|
defaults.set(isOn, forKey: "ShadowsocksOn")
|
|
|
|
self.updateMainMenu()
|
|
self.applyConfig()
|
|
|
|
if showToast {
|
|
if isOn {
|
|
self.isNameTextField.stringValue = "Shadowsocks: On".localized
|
|
}
|
|
else {
|
|
self.isNameTextField.stringValue = "Shadowsocks: Off".localized
|
|
}
|
|
self.fadeInHud()
|
|
}
|
|
}
|
|
|
|
@IBAction func updateGFWList(_ sender: NSMenuItem) {
|
|
UpdatePACFromGFWList()
|
|
}
|
|
|
|
@IBAction func editUserRulesForPAC(_ sender: NSMenuItem) {
|
|
if editUserRulesWinCtrl != nil {
|
|
editUserRulesWinCtrl.close()
|
|
}
|
|
let ctrl = UserRulesController(windowNibName: "UserRulesController")
|
|
editUserRulesWinCtrl = ctrl
|
|
|
|
ctrl.showWindow(self)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
ctrl.window?.makeKeyAndOrderFront(self)
|
|
}
|
|
|
|
@IBAction func showQRCodeForCurrentServer(_ sender: NSMenuItem) {
|
|
var errMsg: String?
|
|
if let profile = ServerProfileManager.instance.getActiveProfile() {
|
|
if profile.isValid() {
|
|
// Show window
|
|
if qrcodeWinCtrl != nil{
|
|
qrcodeWinCtrl.close()
|
|
}
|
|
qrcodeWinCtrl = SWBQRCodeWindowController(windowNibName: "SWBQRCodeWindowController")
|
|
qrcodeWinCtrl.qrCode = profile.URL()!.absoluteString
|
|
qrcodeWinCtrl.showWindow(self)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
qrcodeWinCtrl.window?.makeKeyAndOrderFront(nil)
|
|
|
|
return
|
|
} else {
|
|
errMsg = "Current server profile is not valid.".localized
|
|
}
|
|
} else {
|
|
errMsg = "No current server profile.".localized
|
|
}
|
|
let userNote = NSUserNotification()
|
|
userNote.title = errMsg
|
|
userNote.soundName = NSUserNotificationDefaultSoundName
|
|
|
|
NSUserNotificationCenter.default
|
|
.deliver(userNote);
|
|
}
|
|
|
|
@IBAction func scanQRCodeFromScreen(_ sender: NSMenuItem) {
|
|
ScanQRCodeOnScreen()
|
|
}
|
|
|
|
@IBAction func showBunchJsonExampleFile(sender: NSMenuItem) {
|
|
ServerProfileManager.showExampleConfigFile()
|
|
}
|
|
|
|
@IBAction func importBunchJsonFile(sender: NSMenuItem) {
|
|
ServerProfileManager.instance.importConfigFile()
|
|
//updateServersMenu()//not working
|
|
}
|
|
|
|
@IBAction func exportAllServerProfile(sender: NSMenuItem) {
|
|
ServerProfileManager.instance.exportConfigFile()
|
|
}
|
|
|
|
@IBAction func selectPACMode(_ sender: NSMenuItem) {
|
|
let defaults = UserDefaults.standard
|
|
defaults.setValue("auto", forKey: "ShadowsocksRunningMode")
|
|
updateRunningModeMenu()
|
|
applyConfig()
|
|
}
|
|
|
|
@IBAction func selectGlobalMode(_ sender: NSMenuItem) {
|
|
let defaults = UserDefaults.standard
|
|
defaults.setValue("global", forKey: "ShadowsocksRunningMode")
|
|
updateRunningModeMenu()
|
|
applyConfig()
|
|
}
|
|
|
|
@IBAction func selectManualMode(_ sender: NSMenuItem) {
|
|
let defaults = UserDefaults.standard
|
|
defaults.setValue("manual", forKey: "ShadowsocksRunningMode")
|
|
updateRunningModeMenu()
|
|
applyConfig()
|
|
}
|
|
|
|
@IBAction func editServerPreferences(_ sender: NSMenuItem) {
|
|
if preferencesWinCtrl != nil {
|
|
preferencesWinCtrl.close()
|
|
}
|
|
let ctrl = PreferencesWindowController(windowNibName: "PreferencesWindowController")
|
|
preferencesWinCtrl = ctrl
|
|
|
|
ctrl.showWindow(self)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
ctrl.window?.makeKeyAndOrderFront(self)
|
|
}
|
|
|
|
@IBAction func showAllInOnePreferences(_ sender: NSMenuItem) {
|
|
if allInOnePreferencesWinCtrl != nil {
|
|
allInOnePreferencesWinCtrl.close()
|
|
}
|
|
|
|
allInOnePreferencesWinCtrl = PreferencesWinController(windowNibName: "PreferencesWinController")
|
|
|
|
allInOnePreferencesWinCtrl.showWindow(self)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
allInOnePreferencesWinCtrl.window?.makeKeyAndOrderFront(self)
|
|
}
|
|
|
|
@IBAction func selectServer(_ sender: NSMenuItem) {
|
|
let index = sender.tag - kProfileMenuItemIndexBase
|
|
let spMgr = ServerProfileManager.instance
|
|
let newProfile = spMgr.profiles[index]
|
|
if newProfile.uuid != spMgr.activeProfileId {
|
|
spMgr.setActiveProfiledId(newProfile.uuid)
|
|
updateServersMenu()
|
|
SyncSSLocal()
|
|
applyConfig()
|
|
}
|
|
updateRunningModeMenu()
|
|
}
|
|
|
|
@IBAction func copyExportCommand(_ sender: NSMenuItem) {
|
|
// Get the Http proxy config.
|
|
let defaults = UserDefaults.standard
|
|
let address = defaults.string(forKey: "LocalHTTP.ListenAddress")!
|
|
let port = defaults.integer(forKey: "LocalHTTP.ListenPort")
|
|
|
|
// Format an export string.
|
|
let command = "export http_proxy=http://\(address):\(port);export https_proxy=http://\(address):\(port);"
|
|
|
|
// Copy to paste board.
|
|
NSPasteboard.general().clearContents()
|
|
NSPasteboard.general().setString(command, forType: NSStringPboardType)
|
|
|
|
// Give a system notification.
|
|
let notification = NSUserNotification()
|
|
notification.title = "Export Command Copied.".localized
|
|
NSUserNotificationCenter.default
|
|
.deliver(notification)
|
|
}
|
|
|
|
@IBAction func showLogs(_ sender: NSMenuItem) {
|
|
let ws = NSWorkspace.shared()
|
|
if let appUrl = ws.urlForApplication(withBundleIdentifier: "com.apple.Console") {
|
|
try! ws.launchApplication(at: appUrl
|
|
,options: .default
|
|
,configuration: [NSWorkspaceLaunchConfigurationArguments: "~/Library/Logs/ss-local.log"])
|
|
}
|
|
}
|
|
|
|
@IBAction func feedback(_ sender: NSMenuItem) {
|
|
NSWorkspace.shared().open(URL(string: "https://github.com/qiuyuzhou/ShadowsocksX-NG/issues")!)
|
|
}
|
|
|
|
@IBAction func showAbout(_ sender: NSMenuItem) {
|
|
NSApp.orderFrontStandardAboutPanel(sender);
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
}
|
|
|
|
func updateRunningModeMenu() {
|
|
let defaults = UserDefaults.standard
|
|
let mode = defaults.string(forKey: "ShadowsocksRunningMode")
|
|
|
|
var serverMenuText = "Servers".localized
|
|
|
|
let mgr = ServerProfileManager.instance
|
|
for p in mgr.profiles {
|
|
if mgr.activeProfileId == p.uuid {
|
|
var profileName :String
|
|
if !p.remark.isEmpty {
|
|
profileName = p.remark
|
|
} else {
|
|
profileName = p.serverHost
|
|
}
|
|
serverMenuText = "\(serverMenuText) - \(profileName)"
|
|
}
|
|
}
|
|
serversMenuItem.title = serverMenuText
|
|
|
|
if mode == "auto" {
|
|
autoModeMenuItem.state = 1
|
|
globalModeMenuItem.state = 0
|
|
manualModeMenuItem.state = 0
|
|
} else if mode == "global" {
|
|
autoModeMenuItem.state = 0
|
|
globalModeMenuItem.state = 1
|
|
manualModeMenuItem.state = 0
|
|
} else if mode == "manual" {
|
|
autoModeMenuItem.state = 0
|
|
globalModeMenuItem.state = 0
|
|
manualModeMenuItem.state = 1
|
|
}
|
|
let isShown = defaults.bool(forKey: "ShowRunningModeOnStatusBar")
|
|
updateStatusItemUI(isShownnRunningMode: isShown)
|
|
}
|
|
|
|
func updateStatusItemUI(isShownnRunningMode: Bool) {
|
|
if isShownnRunningMode {
|
|
let defaults = UserDefaults.standard
|
|
let mode = defaults.string(forKey: "ShadowsocksRunningMode")
|
|
if mode == "auto" {
|
|
statusItem.title = "A"
|
|
} else if mode == "global" {
|
|
statusItem.title = "G"
|
|
} else if mode == "manual" {
|
|
statusItem.title = "M"
|
|
}
|
|
let titleWidth = statusItem.title!.size(withAttributes: [NSFontAttributeName: statusItem.button!.font!]).width
|
|
let imageWidth:CGFloat = AppDelegate.StatusItemIconWidth
|
|
statusItem.length = titleWidth + imageWidth + 2
|
|
} else {
|
|
statusItem.length = AppDelegate.StatusItemIconWidth
|
|
}
|
|
}
|
|
|
|
func updateMainMenu() {
|
|
let defaults = UserDefaults.standard
|
|
let isOn = defaults.bool(forKey: "ShadowsocksOn")
|
|
if isOn {
|
|
runningStatusMenuItem.title = "Shadowsocks: On".localized
|
|
toggleRunningMenuItem.title = "Turn Shadowsocks Off".localized
|
|
let image = NSImage(named: "menu_icon")
|
|
statusItem.image = image
|
|
} else {
|
|
runningStatusMenuItem.title = "Shadowsocks: Off".localized
|
|
toggleRunningMenuItem.title = "Turn Shadowsocks On".localized
|
|
let image = NSImage(named: "menu_icon_disabled")
|
|
statusItem.image = image
|
|
}
|
|
}
|
|
|
|
func updateCopyHttpProxyExportMenu() {
|
|
let defaults = UserDefaults.standard
|
|
let isOn = defaults.bool(forKey: "LocalHTTPOn")
|
|
copyHttpProxyExportCmdLineMenuItem.isHidden = !isOn
|
|
}
|
|
|
|
func updateServersMenu() {
|
|
let mgr = ServerProfileManager.instance
|
|
serversMenuItem.submenu?.removeAllItems()
|
|
let preferencesItem = serversPreferencesMenuItem
|
|
let showBunch = showBunchJsonExampleFileItem
|
|
let importBuntch = importBunchJsonFileItem
|
|
let exportAllServer = exportAllServerProfileItem
|
|
|
|
serversMenuItem.submenu?.addItem(preferencesItem!)
|
|
serversMenuItem.submenu?.addItem(NSMenuItem.separator())
|
|
|
|
var i = 0
|
|
for p in mgr.profiles {
|
|
let item = NSMenuItem()
|
|
item.tag = i + kProfileMenuItemIndexBase
|
|
if p.remark.isEmpty {
|
|
item.title = "\(p.serverHost):\(p.serverPort)"
|
|
} else {
|
|
item.title = "\(p.remark) (\(p.serverHost):\(p.serverPort))"
|
|
}
|
|
if mgr.activeProfileId == p.uuid {
|
|
item.state = 1
|
|
}
|
|
if !p.isValid() {
|
|
item.isEnabled = false
|
|
}
|
|
item.action = #selector(AppDelegate.selectServer)
|
|
|
|
serversMenuItem.submenu?.addItem(item)
|
|
i += 1
|
|
}
|
|
if !mgr.profiles.isEmpty {
|
|
serversMenuItem.submenu?.addItem(NSMenuItem.separator())
|
|
}
|
|
|
|
serversMenuItem.submenu?.addItem(showBunch!)
|
|
serversMenuItem.submenu?.addItem(importBuntch!)
|
|
serversMenuItem.submenu?.addItem(exportAllServer!)
|
|
}
|
|
|
|
func handleURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) {
|
|
if let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue {
|
|
if let url = URL(string: urlString) {
|
|
NotificationCenter.default.post(
|
|
name: Notification.Name(rawValue: "NOTIFY_FOUND_SS_URL"), object: nil
|
|
, userInfo: [
|
|
"urls": [url],
|
|
"source": "url",
|
|
])
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleFoundSSURL(_ note: Notification) {
|
|
let sendNotify = {
|
|
(title: String, subtitle: String, infoText: String) in
|
|
|
|
let userNote = NSUserNotification()
|
|
userNote.title = title
|
|
userNote.subtitle = subtitle
|
|
userNote.informativeText = infoText
|
|
userNote.soundName = NSUserNotificationDefaultSoundName
|
|
|
|
NSUserNotificationCenter.default
|
|
.deliver(userNote);
|
|
}
|
|
|
|
if let userInfo = (note as NSNotification).userInfo {
|
|
let urls: [URL] = userInfo["urls"] as! [URL]
|
|
|
|
let mgr = ServerProfileManager.instance
|
|
var isChanged = false
|
|
|
|
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)")
|
|
}
|
|
}
|
|
|
|
if isChanged {
|
|
mgr.save()
|
|
self.updateServersMenu()
|
|
} else {
|
|
sendNotify("Not found valid qrcode of shadowsocks profile.", "", "")
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------
|
|
// NSUserNotificationCenterDelegate
|
|
|
|
func userNotificationCenter(_ center: NSUserNotificationCenter
|
|
, shouldPresent notification: NSUserNotification) -> Bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
extension AppDelegate {
|
|
func fadeInHud() -> Void {
|
|
if timerToFadeOut != nil {
|
|
timerToFadeOut?.invalidate()
|
|
timerToFadeOut = nil
|
|
}
|
|
|
|
fadingOut = false
|
|
|
|
hudWindow.orderFrontRegardless()
|
|
|
|
CATransaction.begin()
|
|
CATransaction.setAnimationDuration(kHudFadeInDuration)
|
|
CATransaction.setCompletionBlock { self.didFadeIn() }
|
|
panelView.layer?.opacity = 1.0
|
|
CATransaction.commit()
|
|
}
|
|
|
|
func didFadeIn() -> Void {
|
|
timerToFadeOut = Timer.scheduledTimer(
|
|
timeInterval: kHudDisplayDuration,
|
|
target: self,
|
|
selector: #selector(fadeOutHud),
|
|
userInfo: nil,
|
|
repeats: false)
|
|
}
|
|
|
|
func fadeOutHud() -> Void {
|
|
fadingOut = true
|
|
|
|
CATransaction.begin()
|
|
CATransaction.setAnimationDuration(kHudFadeOutDuration)
|
|
CATransaction.setCompletionBlock { self.didFadeOut() }
|
|
panelView.layer?.opacity = 0.0
|
|
CATransaction.commit()
|
|
}
|
|
|
|
func didFadeOut() -> Void {
|
|
if fadingOut {
|
|
self.hudWindow.orderOut(nil)
|
|
}
|
|
fadingOut = false
|
|
}
|
|
|
|
func setupHud() -> Void {
|
|
isNameTextField.stringValue = "Shadowsocks: Off"
|
|
isNameTextField.sizeToFit()
|
|
|
|
var labelFrame: CGRect = isNameTextField.frame
|
|
var hudWindowFrame: CGRect = hudWindow.frame
|
|
hudWindowFrame.size.width = labelFrame.size.width + kHudHorizontalMargin * 2
|
|
hudWindowFrame.size.height = kHudHeight
|
|
|
|
let screenRect: NSRect = NSScreen.screens()![0].visibleFrame
|
|
hudWindowFrame.origin.x = (screenRect.size.width - hudWindowFrame.size.width) / 2
|
|
hudWindowFrame.origin.y = (screenRect.size.height - hudWindowFrame.size.height) / 2
|
|
hudWindow.setFrame(hudWindowFrame, display: true)
|
|
|
|
var viewFrame: NSRect = hudWindowFrame;
|
|
viewFrame.origin.x = 0
|
|
viewFrame.origin.y = 0
|
|
panelView.frame = viewFrame
|
|
|
|
labelFrame.origin.x = kHudHorizontalMargin
|
|
labelFrame.origin.y = (hudWindowFrame.size.height - labelFrame.size.height) / 2
|
|
isNameTextField.frame = labelFrame
|
|
}
|
|
|
|
func initUIComponent() -> Void {
|
|
hudWindow.isOpaque = false
|
|
hudWindow.backgroundColor = .clear
|
|
hudWindow.level = Int(CGWindowLevelForKey(.utilityWindow)) + 1000
|
|
hudWindow.styleMask = .borderless
|
|
hudWindow.hidesOnDeactivate = false
|
|
hudWindow.collectionBehavior = .canJoinAllSpaces
|
|
|
|
let viewLayer: CALayer = CALayer()
|
|
viewLayer.backgroundColor = CGColor.init(red: 0.05, green: 0.05, blue: 0.05, alpha: kHudAlphaValue)
|
|
viewLayer.cornerRadius = kHudCornerRadius
|
|
panelView.wantsLayer = true
|
|
panelView.layer = viewLayer
|
|
panelView.layer?.opacity = 0.0
|
|
|
|
setupHud()
|
|
}
|
|
|
|
override func awakeFromNib() {
|
|
initUIComponent()
|
|
}
|
|
}
|