diff --git a/ShadowsocksX-NG/AppDelegate.swift b/ShadowsocksX-NG/AppDelegate.swift index 79b625d..2456cdb 100644 --- a/ShadowsocksX-NG/AppDelegate.swift +++ b/ShadowsocksX-NG/AppDelegate.swift @@ -365,6 +365,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele } @IBAction func updateSubscribe(_ sender: NSMenuItem) { + SubscribeManager.instance.updateAllServerFromSubscribe() } @IBAction func updateSubscribeAtLaunch(_ sender: NSMenuItem) { diff --git a/ShadowsocksX-NG/Subscribe.swift b/ShadowsocksX-NG/Subscribe.swift index ed65ba5..ab44661 100644 --- a/ShadowsocksX-NG/Subscribe.swift +++ b/ShadowsocksX-NG/Subscribe.swift @@ -16,6 +16,7 @@ class Subscribe: NSObject{ var maxCount = -1 // -1 is not limited var groupName = "" var token = "" + var cache = "" var profileMgr: ServerProfileManager! @@ -55,6 +56,35 @@ class Subscribe: NSObject{ func getMaxCount() -> Int { return maxCount } + static func fromDictionary(_ data:[String:AnyObject]) -> Subscribe { + var feed:String = "" + var group:String = "" + var token:String = "" + var maxCount:Int = -1 + for (key, value) in data { + switch key { + case "feed": + feed = value as! String + case "group": + group = value as! String + case "token": + token = value as! String + case "maxCount": + maxCount = value as! Int + default: + print("") + } + } + return Subscribe.init(initUrlString: feed, initGroupName: group, initToken: token, initMaxCount: maxCount) + } + static func toDictionary(_ data: Subscribe) -> [String: AnyObject] { + var ret : [String: AnyObject] = [:] + ret["feed"] = data.subscribeFeed as AnyObject + ret["group"] = data.groupName as AnyObject + ret["token"] = data.token as AnyObject + ret["maxCount"] = data.maxCount as AnyObject + return ret + } fileprivate func sendRequest(url: String, options: Any, callback: @escaping (String) -> Void) { let headers: HTTPHeaders = [ // "Authorization": "Basic U2hhZG93c29ja1gtTkctUg==", @@ -76,8 +106,8 @@ class Subscribe: NSObject{ } } func setMaxCount() { - sendRequest(url: self.subscribeFeed, options: "", callback: { resString in - + + func getMaxFromRes(resString: String) { let maxCountReg = "MAX=[0-9]+" let decodeRes = decode64(resString)! let range = decodeRes.range(of: maxCountReg, options: .regularExpression) @@ -88,12 +118,19 @@ class Subscribe: NSObject{ else{ self.maxCount = -1 } - + } + + if cache != "" { + return getMaxFromRes(resString: cache) + } + sendRequest(url: self.subscribeFeed, options: "", callback: { resString in + getMaxFromRes(resString: resString) + self.cache = resString }) } func updateServerFromFeed(){ - if (!isActive){ return } - sendRequest(url: self.subscribeFeed, options: "", callback: { resString in + + func updateServerHandler(resString: String) { let decodeRes = decode64(resString)! let ssrregexp = "ssr://([A-Za-z0-9_-]+)" let urls = splitor(url: decodeRes, regexp: ssrregexp) @@ -108,6 +145,15 @@ class Subscribe: NSObject{ } self.profileMgr.save() (NSApplication.shared().delegate as! AppDelegate).updateServersMenu() + } + + if (!isActive){ return } + if cache != "" { + return updateServerHandler(resString: cache) + } + sendRequest(url: self.subscribeFeed, options: "", callback: { resString in + updateServerHandler(resString: resString) + self.cache = resString }) } func feedValidator() -> Bool{ @@ -117,13 +163,31 @@ class Subscribe: NSObject{ fileprivate func pushNotification(){ } - fileprivate func isExisted() -> Bool{ + fileprivate func isExisted(profile: ServerProfile) -> Bool{ // using to url judge // if existed update + for (_, value) in profileMgr.profiles.enumerated() { + let ret = value.serverHost == profile.serverHost + if ret { + return ret + } + } return false } - fileprivate func isDuplicated() -> Bool{ + fileprivate func isDuplicated(profile: ServerProfile) -> Bool{ // if duplicated skip + for (_, value) in profileMgr.profiles.enumerated() { + let ret = value.serverHost == profile.serverHost + && value.password == profile.password + && value.serverPort == profile.serverPort + && value.ssrProtocol == profile.ssrProtocol + && value.ssrObfs == profile.ssrObfs + && value.ssrObfsParam == value.ssrObfsParam + && value.ssrProtocolParam == value.ssrProtocolParam + if ret { + return ret + } + } return false } fileprivate func isSame(source: Subscribe, target: Subscribe) -> Bool { diff --git a/ShadowsocksX-NG/SubscribeManager.swift b/ShadowsocksX-NG/SubscribeManager.swift index 80b2d9a..758b6f5 100644 --- a/ShadowsocksX-NG/SubscribeManager.swift +++ b/ShadowsocksX-NG/SubscribeManager.swift @@ -12,14 +12,21 @@ class SubscribeManager:NSObject{ static let instance:SubscribeManager = SubscribeManager() var subscribes:[Subscribe] + var subscribesDefault : [[String: AnyObject]] let defaults = UserDefaults.standard fileprivate override init() { - subscribes = defaults.array(forKey: "Subscribes") as! [Subscribe] + subscribes = [] + subscribesDefault = [[:]] + if let subscribesDefault = defaults.array(forKey: "Subscribes") { + for value in subscribesDefault{ + subscribes.append(Subscribe.fromDictionary(value as! [String : AnyObject])) + } + } } func addSubscribe(oneSubscribe: Subscribe) -> Bool { subscribes.append(oneSubscribe) - defaults.set(subscribes, forKey: "Subscribes") + defaults.set(subscribesToDefaults(data: subscribes), forKey: "Subscribes") return true } func deleteSubscribe(toDeleteSubscribe: Subscribe) -> Bool { @@ -28,10 +35,26 @@ class SubscribeManager:NSObject{ subscribes.remove(at: index) } }) - defaults.set(subscribes, forKey: "Subscribes") + defaults.set(subscribesToDefaults(data: subscribes), forKey: "Subscribes") return true } + fileprivate func subscribesToDefaults(data: [Subscribe]) -> [[String: AnyObject]]{ + var ret : [[String: AnyObject]] = [] + for value in data { + ret.append(Subscribe.toDictionary(value)) + } + return ret + } + fileprivate func DefaultsToSubscribes(data:[[String: AnyObject]]) -> [Subscribe] { + var ret : [Subscribe] = [] + for value in data{ + ret.append(Subscribe.fromDictionary(value)) + } + return ret + } func updateAllServerFromSubscribe(){ - + subscribes.forEach{ value in + value.updateServerFromFeed() + } } } diff --git a/ShadowsocksX-NG/SubscribePreferenceWindowController.swift b/ShadowsocksX-NG/SubscribePreferenceWindowController.swift index 6de3f0b..0a2d486 100644 --- a/ShadowsocksX-NG/SubscribePreferenceWindowController.swift +++ b/ShadowsocksX-NG/SubscribePreferenceWindowController.swift @@ -8,7 +8,8 @@ import Cocoa -class SubscribePreferenceWindowController: NSWindowController { +class SubscribePreferenceWindowController: NSWindowController + , NSTableViewDataSource, NSTableViewDelegate { @IBOutlet weak var FeedLabel: NSTextField! @IBOutlet weak var OKButton: NSButton! @@ -17,22 +18,25 @@ class SubscribePreferenceWindowController: NSWindowController { @IBOutlet weak var TokenTextField: NSTextField! @IBOutlet weak var GroupTextField: NSTextField! @IBOutlet weak var MaxCountTextField: NSTextField! + @IBOutlet weak var SubscribeTableView: NSTableView! + var sbMgr: SubscribeManager! var defaults: UserDefaults! + let tableViewDragType: String = "subscribe.host" + var editingSubscribe: Subscribe! override func windowDidLoad() { super.windowDidLoad() - // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. - // TODO load stored subscribes and prepare the manager sbMgr = SubscribeManager.instance defaults = UserDefaults.standard + SubscribeTableView.reloadData() } override func awakeFromNib() { -// profilesTableView.register(forDraggedTypes: [tableViewDragType]) -// profilesTableView.allowsMultipleSelection = true + SubscribeTableView.register(forDraggedTypes: [tableViewDragType]) + SubscribeTableView.allowsMultipleSelection = true } @IBAction func onOk(_ sender: NSButton) { @@ -42,14 +46,167 @@ class SubscribePreferenceWindowController: NSWindowController { _ = sbMgr.addSubscribe(oneSubscribe: subscribe) } subscribe.updateServerFromFeed() + SubscribeTableView.reloadData() window?.performClose(self) } - func bindSubscribe(index: Int){ - if index >= 0 && index <= SubscribeManager.instance.subscribes.count { - var editSubscribe = SubscribeManager.instance.subscribes[index] - FeedTextField.bind("value", to: editSubscribe, withKeyPath: "subscribeFeed", options: [NSContinuouslyUpdatesValueBindingOption: true]) + func bindSubscribe(_ index:Int) { + if index >= 0 && index < sbMgr.subscribes.count { + editingSubscribe = sbMgr.subscribes[index] + + FeedTextField.bind("value", to: editingSubscribe, withKeyPath: "subscribeFeed", options: [NSContinuouslyUpdatesValueBindingOption: true]) + TokenTextField.bind("value", to: editingSubscribe, withKeyPath: "token", options: [NSContinuouslyUpdatesValueBindingOption: true]) + GroupTextField.bind("value", to: editingSubscribe, withKeyPath: "groupName", options: [NSContinuouslyUpdatesValueBindingOption: true]) + MaxCountTextField.bind("value", to: editingSubscribe, withKeyPath: "maxCount", options: [NSContinuouslyUpdatesValueBindingOption: true]) + + } else { + editingSubscribe = nil + FeedTextField.unbind("value") + TokenTextField.unbind("value") + GroupTextField.unbind("value") + MaxCountTextField.unbind("value") } } + func getDataAtRow(_ index:Int) -> String { + return sbMgr.subscribes[index].groupName + } + + // MARK: For NSTableViewDataSource + + func numberOfRows(in tableView: NSTableView) -> Int { + if let mgr = sbMgr { + return mgr.subscribes.count + } + return 0 + } + + func tableView(_ tableView: NSTableView + , objectValueFor tableColumn: NSTableColumn? + , row: Int) -> Any? { + + let title = getDataAtRow(row) + + if tableColumn?.identifier == "main" { + if title != "" {return title} + else {return "S"} + } else if tableColumn?.identifier == "status" { + return NSImage(named: "menu_icon") + } + return "" + } + + // MARK: Drag & Drop reorder rows + + func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? { + let item = NSPasteboardItem() + item.setString(String(row), forType: tableViewDragType) + return item + } + + func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int + , proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation { + if dropOperation == .above { + return .move + } + return NSDragOperation() + } + + func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo + , row: Int, dropOperation: NSTableViewDropOperation) -> Bool { + if let mgr = sbMgr { + var oldIndexes = [Int]() + info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) { + if let str = ($0.0.item as! NSPasteboardItem).string(forType: self.tableViewDragType), let index = Int(str) { + oldIndexes.append(index) + } + } + + var oldIndexOffset = 0 + var newIndexOffset = 0 + + // For simplicity, the code below uses `tableView.moveRowAtIndex` to move rows around directly. + // You may want to move rows in your content array and then call `tableView.reloadData()` instead. + tableView.beginUpdates() + for oldIndex in oldIndexes { + if oldIndex < row { + let o = mgr.subscribes.remove(at: oldIndex + oldIndexOffset) + mgr.subscribes.insert(o, at:row - 1) + tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1) + oldIndexOffset -= 1 + } else { + let o = mgr.subscribes.remove(at: oldIndex) + mgr.subscribes.insert(o, at:row + newIndexOffset) + tableView.moveRow(at: oldIndex, to: row + newIndexOffset) + newIndexOffset += 1 + } + } + tableView.endUpdates() + + return true + } + return false + } + + //-------------------------------------------------- + // For NSTableViewDelegate + + func tableView(_ tableView: NSTableView + , shouldEdit tableColumn: NSTableColumn?, row: Int) -> Bool { + return false + } + + func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { + if row < 0 { + editingSubscribe = nil + return true + } +// if editingSubscribe != nil { +// if !editingSubscribe.isValid() { +// return false +// } +// } + + return true + } + + func tableViewSelectionDidChange(_ notification: Notification) { + if SubscribeTableView.selectedRow >= 0 { + bindSubscribe(SubscribeTableView.selectedRow) + if (SubscribeTableView.selectedRowIndexes.count > 1){ +// duplicateProfileButton.isEnabled = false + } else { +// duplicateProfileButton.isEnabled = true + } + } else { + if !sbMgr.subscribes.isEmpty { + let index = IndexSet(integer: sbMgr.subscribes.count - 1) + SubscribeTableView.selectRowIndexes(index, byExtendingSelection: false) + } + } + } + + func shakeWindows(){ + let numberOfShakes:Int = 8 + let durationOfShake:Float = 0.5 + let vigourOfShake:Float = 0.05 + + let frame:CGRect = (window?.frame)! + let shakeAnimation = CAKeyframeAnimation() + + let shakePath = CGMutablePath() + + shakePath.move(to: CGPoint(x:NSMinX(frame), y:NSMinY(frame))) + + for _ in 1...numberOfShakes{ + shakePath.addLine(to: CGPoint(x: NSMinX(frame) - frame.size.width * CGFloat(vigourOfShake), y: NSMinY(frame))) + shakePath.addLine(to: CGPoint(x: NSMinX(frame) + frame.size.width * CGFloat(vigourOfShake), y: NSMinY(frame))) + } + + shakePath.closeSubpath() + shakeAnimation.path = shakePath + shakeAnimation.duration = CFTimeInterval(durationOfShake) + window?.animations = ["frameOrigin":shakeAnimation] + window?.animator().setFrameOrigin(window!.frame.origin) + } } diff --git a/ShadowsocksX-NG/SubscribePreferenceWindowController.xib b/ShadowsocksX-NG/SubscribePreferenceWindowController.xib index 3c23cdd..26bd490 100644 --- a/ShadowsocksX-NG/SubscribePreferenceWindowController.xib +++ b/ShadowsocksX-NG/SubscribePreferenceWindowController.xib @@ -13,6 +13,7 @@ + @@ -36,32 +37,6 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -131,18 +106,19 @@ - + - + + + - + - @@ -162,33 +138,90 @@ DQ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + + + + + + + - + + + - + - - + -