From ecdedbdf62df531576393391ddf7ca040ce80324 Mon Sep 17 00:00:00 2001 From: DaFuqtor <46137336+DaFuqtor@users.noreply.github.com> Date: Thu, 20 Feb 2020 03:15:43 +0300 Subject: [PATCH] ability to add multiple accounts v1.3.1 -> v1.4 --- 2FA to Tray.xcodeproj/project.pbxproj | 10 +- 2FA to Tray/AppDelegate.swift | 161 ++++------ 2FA to Tray/Base.lproj/MainMenu.xib | 36 ++- 2FA to Tray/StatusMenuController.swift | 390 ++++++++++++++++++++++--- README.md | 21 +- 5 files changed, 429 insertions(+), 189 deletions(-) diff --git a/2FA to Tray.xcodeproj/project.pbxproj b/2FA to Tray.xcodeproj/project.pbxproj index ee0534d..458a978 100644 --- a/2FA to Tray.xcodeproj/project.pbxproj +++ b/2FA to Tray.xcodeproj/project.pbxproj @@ -299,14 +299,15 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = R2294BC6J8; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "2FA to Tray/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = com.dafuqtor.2FAtoTray; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -322,14 +323,15 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = R2294BC6J8; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "2FA to Tray/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = com.dafuqtor.2FAtoTray; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/2FA to Tray/AppDelegate.swift b/2FA to Tray/AppDelegate.swift index ea675e9..0ec358d 100644 --- a/2FA to Tray/AppDelegate.swift +++ b/2FA to Tray/AppDelegate.swift @@ -11,61 +11,59 @@ import JavaScriptCore let defaults = UserDefaults.standard extension UserDefaults { - func toggleBool(_ forKey: String) { + func boolToggle(_ forKey: String) { self.set(!self.bool(forKey: forKey), forKey: forKey) } } -extension NSMenuItem { - func toggleState() { - self.state = self.state == .on ? .off : .on +extension NSControl.StateValue { + mutating func toggle() { + self = self == .on ? .off : .on } - func stateBy(_ bool: Bool) { - self.state = bool ? .on : .off + mutating func by(_ bool: Bool) { + self = bool ? .on : .off } } import KeychainAccess -let keychain = Keychain(service: "com.dafuqtor.2FAtoTray") +let keychain = Keychain(service: Bundle.main.bundleIdentifier!) extension String { - func condenseWhitespace() -> String { return self.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }.joined(separator: " ") } - } final class EditableNSTextField: NSTextField { - - private let commandKey = NSEvent.ModifierFlags.command.rawValue - private let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue - - override func performKeyEquivalent(with event: NSEvent) -> Bool { - if event.type == NSEvent.EventType.keyDown { - if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey { - switch event.charactersIgnoringModifiers! { - case "x": - if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true } - case "c": - if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true } - case "v": - if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true } - case "z": - if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true } - case "a": - if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true } - default: - break - } - } else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey { - if event.charactersIgnoringModifiers == "Z" { - if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true } - } - } + + private let commandKey = NSEvent.ModifierFlags.command.rawValue + private let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue + + override func performKeyEquivalent(with event: NSEvent) -> Bool { + if event.type == NSEvent.EventType.keyDown { + if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey { + switch event.charactersIgnoringModifiers! { + case "x": + if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true } + case "c": + if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true } + case "v": + if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true } + case "z": + if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true } + case "a": + if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true } + default: + break + } + } else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey { + if event.charactersIgnoringModifiers == "Z" { + if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true } } - return super.performKeyEquivalent(with: event) + } } + return super.performKeyEquivalent(with: event) + } } import AppKit @@ -81,15 +79,18 @@ class Clipboard { } func paste() { + if !AXIsProcessTrusted() { + return + } checkAccessibilityPermissions() - + DispatchQueue.main.async { let vCode = UInt16(kVK_ANSI_V) let source = CGEventSource(stateID: .combinedSessionState) // Disable local keyboard events while pasting source?.setLocalEventsFilterDuringSuppressionState([.permitLocalMouseEvents, .permitSystemDefinedEvents], state: .eventSuppressionStateSuppressionInterval) - + let keyVDown = CGEvent(keyboardEventSource: source, virtualKey: vCode, keyDown: true) let keyVUp = CGEvent(keyboardEventSource: source, virtualKey: vCode, keyDown: false) keyVDown?.flags = .maskCommand @@ -105,18 +106,20 @@ class Clipboard { } } -let otp = OTP() +var otpInstances: [OTP] = [] class OTP { private var fn:JSValue? private var timer:Timer? var token:String var button:NSStatusBarButton? + var displayItem:NSMenuItem? var secret:String init() { fn = nil button = nil + displayItem = nil timer = nil secret = "" token = "" @@ -141,87 +144,26 @@ class OTP { let token = result!.toString()! if self.token != token { self.token = token - self.button!.toolTip = token + self.button?.toolTip = token + self.button?.appearsDisabled = false + self.displayItem?.title = token + self.displayItem?.isHidden = false + self.displayItem?.isEnabled = true } } } func initTimer() { - self.timer = Timer.new(every: 2.second) { + self.timer = Timer.new(every: 1.second) { self.updateTimer() } self.timer!.start() } func copy() { - if self.token.isEmpty { - self.showAlert() - } else { - clipboard.copy(self.token) - } - } - - func showAlert() { - if (NSApplication.shared.modalWindow) != nil { - return - } - let alert = NSAlert() - alert.messageText = "Change secret seed" - alert.informativeText = "Enter a code which should look like this:" - - alert.addButton(withTitle: "OK") - alert.addButton(withTitle: "Cancel") - alert.addButton(withTitle: "Delete secret from disk") - - let textfield = EditableNSTextField(frame: NSRect(x: 0.0, y: 0.0, width: 150.0, height: 22.0)) - textfield.alignment = .center - textfield.placeholderString = "AADEM4YUY5GYZHHP" - alert.accessoryView = textfield - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - textfield.becomeFirstResponder() - } - - let theSecret = keychain["secret"] ?? "" - if !theSecret.isEmpty { - textfield.stringValue = theSecret - } - - let response = alert.runModal() - - if response == .alertFirstButtonReturn { - let value = textfield.stringValue - if !value.isEmpty { - let secret = value.condenseWhitespace() - - do { - try keychain - .synchronizable(true) - .accessibility(.afterFirstUnlock) - .set(secret, key: "secret") - } catch let error { - print("error: \(error)") - } - - self.button?.appearsDisabled = false - self.secret = secret - } else { - print("Empty value") - } - } else if response == .alertThirdButtonReturn { - print("Delete secret") - do { - try keychain.remove("secret") - } catch let error { - print("error: \(error)") - } - self.secret = "" - self.token = "" - self.button?.toolTip = "" - self.button?.appearsDisabled = true - } + clipboard.copy(self.token) + print("copied the token") } - } @NSApplicationMain @@ -229,12 +171,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - otp.copy() + otpInstances[currentlySelectedSeed].copy() if let button = statusItem.button { button.highlight(true) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { button.highlight(false) } + print("highlighted statusItem.button") } print("handled reopen") return true diff --git a/2FA to Tray/Base.lproj/MainMenu.xib b/2FA to Tray/Base.lproj/MainMenu.xib index ee606e2..8662330 100644 --- a/2FA to Tray/Base.lproj/MainMenu.xib +++ b/2FA to Tray/Base.lproj/MainMenu.xib @@ -1,7 +1,8 @@ - + - + + @@ -20,14 +21,13 @@ - - + - + + + diff --git a/2FA to Tray/StatusMenuController.swift b/2FA to Tray/StatusMenuController.swift index 8904dab..96d5406 100644 --- a/2FA to Tray/StatusMenuController.swift +++ b/2FA to Tray/StatusMenuController.swift @@ -10,16 +10,58 @@ import Cocoa import HotKey private extension HotKey { - func handleKeyDown(_ handler: @escaping (() -> Void)) { - keyDownHandler = { - handler() - self.handleKeyDown(handler) - } + func handleKeyDown(_ handler: @escaping (() -> Void)) { + keyDownHandler = { + handler() + self.handleKeyDown(handler) } + } } let statusItem = NSStatusBar.system.statusItem(withLength: 22) +var currentlySelectedSeed: Int { + get { + let QTYofInstances = otpInstances.count + if defaults.integer(forKey: "selected") + 1 > QTYofInstances { + let newSelected = (QTYofInstances - 1 < 0) ? 0 : (QTYofInstances - 1) + defaults.set(newSelected, forKey: "selected") + } + return defaults.integer(forKey: "selected") + } + set { + defaults.set(newValue, forKey: "selected") + reinitializeStates(newValue) + + } +} + +func turnAllStatesOff() { + if !otpInstances.isEmpty { + for instance in otpInstances { + if (instance.displayItem != nil) { + instance.displayItem?.state = .off + } + } + print("turned all states off") + } +} + +func setStateForSelected(_ selected: Int) { + if !otpInstances.isEmpty { + if (otpInstances[selected].displayItem != nil) { + print("set selected state for: \(selected)") + otpInstances[selected].displayItem?.state = .on + } + } +} + +func reinitializeStates(_ select: Int) { + turnAllStatesOff() + setStateForSelected(select) + print("Newly selected instance: \(select)") +} + class StatusMenuController: NSObject, NSMenuDelegate { func resize(image: NSImage, w: Int, h: Int) -> NSImage { @@ -34,10 +76,116 @@ class StatusMenuController: NSObject, NSMenuDelegate { @IBOutlet weak var statusMenu: NSMenu! - @IBAction func changeSecret(_ sender: Any) { - otp.showAlert() + @IBAction func changeSecret(_ sender: NSMenuItem) { + showAlert() } - + + func showAlert() { + if otpInstances.isEmpty { + print("otpInstances is empty") + return + } + if (NSApplication.shared.modalWindow) != nil { + return + } + let currentlySelectedInstance = otpInstances[currentlySelectedSeed] + let alert = NSAlert() + alert.messageText = "Change secret seed" + alert.informativeText = "Enter a code which should look like this:" + + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + alert.addButton(withTitle: "Delete secret from disk") + + let textfield = EditableNSTextField(frame: NSRect(x: 0.0, y: 0.0, width: 180.0, height: 22.0)) + textfield.alignment = .center + textfield.placeholderString = "AADEM4YUY5GYZHHP" + alert.accessoryView = textfield + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + textfield.becomeFirstResponder() + } + + let previousSecret = currentlySelectedInstance.secret + if !previousSecret.isEmpty { + textfield.stringValue = previousSecret + } + + let response = alert.runModal() + if response == .alertFirstButtonReturn { + print("Pressed OK") + let value = textfield.stringValue + if value.isEmpty { + print("Entered value is empty") + removeInstance(currentlySelectedInstance) + } else { + currentlySelectedInstance.secret = value.condenseWhitespace() + } + } else if response == .alertSecondButtonReturn { + print("Pressed Cancel") + if currentlySelectedInstance.secret.isEmpty { + removeInstance(currentlySelectedInstance) + } + } else if response == .alertThirdButtonReturn { + print("Pressed Delete secret") + currentlySelectedInstance.secret = "" + currentlySelectedInstance.token = "" + currentlySelectedInstance.button?.toolTip = "" + removeInstance(currentlySelectedInstance) + } + reinitializeKeychain() + initializeInstances() + } + + func removeInstance(_ instance: OTP) { + if (instance.displayItem != nil) { + statusMenu.removeItem(instance.displayItem!) + } + otpInstances.remove(at: currentlySelectedSeed) + } + + func reinitializeKeychain() { + var secrets: [String] = [] + + print("QTY of instances: \(otpInstances.count)") + + if otpInstances.isEmpty { + print("otpInstances is Empty") + } else { + for inst in otpInstances { + if inst.secret.isEmpty { + removeInstance(inst) + print("removed an instance due to empty secret") + } else { + secrets.append(inst.secret) + } + } + print(secrets) + } + + do { + if secrets.count == 0 { + do { + try keychain.remove("secret") + print("removed keychain item") + } catch let error { + print("error: \(error)") + } + return + } + let stringified = try JSONStringify(value: secrets).stringify() + print(stringified) + do { + try keychain + .synchronizable(true) + .accessibility(.afterFirstUnlock) + .set(stringified, key: "secret") + } catch let error { + print("error: \(error)") + } + } catch let error { print(error) } + } + class mouseHandlerView: NSView { var onLeftMouseDown: (()->())? = nil @@ -60,37 +208,98 @@ class StatusMenuController: NSObject, NSMenuDelegate { } + @objc func tokenDisplayClicked(_ sender: NSMenuItem) { + currentlySelectedSeed = statusMenu.index(of: sender) - 1 + if !otpInstances.isEmpty { + for instance in otpInstances { + instance.displayItem?.state = .off + } + } + sender.state.toggle() + } + + func initializeInstances() { + for instance in otpInstances { + if (instance.displayItem != nil) { + statusMenu.removeItem(instance.displayItem!) + } + } + otpInstances.removeAll() + print("Removed all instances") + + let secrets = keychain["secret"] + if secrets == nil { + print("Keychain 'secret' is empty") + if let button = statusItem.button { + button.appearsDisabled = true + } + return + } + if let datan = secrets?.decodeUrl().data(using: String.Encoding.utf8) { + print("Initializing instances from keychain") + if let jsonc = datan.dataToJSON() { + let dataArray = (jsonc as! NSArray) as Array + print("Stored secrets already: \(dataArray.count)") + + var newInstIndex = 0 + + for secret in dataArray { + print("\(secret) will be initialized") + let theSecret = (secret as! String).condenseWhitespace() + + let newOtpInstance = OTP() + newOtpInstance.secret = theSecret + + newOtpInstance.button = statusItem.button + + let newTokenDisplay = NSMenuItem() + statusMenu.insertItem(newTokenDisplay, at: newInstIndex + 1) + newTokenDisplay.isEnabled = true + newTokenDisplay.isHidden = false + newTokenDisplay.target = self + newTokenDisplay.action = #selector(tokenDisplayClicked(_:)) + newOtpInstance.displayItem = newTokenDisplay + + newOtpInstance.start() + otpInstances.append(newOtpInstance) + newInstIndex += 1 + } + + } + } + reinitializeStates(currentlySelectedSeed) + } + override func awakeFromNib() { - UserDefaults.standard.removeObject(forKey: "secret") + defaults.removeObject(forKey: "secret") statusItem.menu = statusMenu statusMenu.delegate = self statusItem.isVisible = true + if let button = statusItem.button { let statusIcon = resize(image: NSImage(named: "StatusIcon")!, w: 22, h: 22) statusIcon.isTemplate = true button.image = statusIcon button.target = self - otp.button = button - let secret = keychain["secret"]?.condenseWhitespace() ?? "" - if secret.isEmpty { - button.appearsDisabled = true - otp.showAlert() - } else { - otp.secret = secret - } - otp.start() - if UserDefaults.standard.bool(forKey: "instantMode") { - otp.copy() - NSApplication.shared.terminate(self) + + initializeInstances() + + if otpInstances.isEmpty { + tryToAddInstance() } + + let mouseView = mouseHandlerView(frame: button.frame) - + mouseView.onLeftMouseDown = { + if otpInstances.isEmpty { + return + } button.highlight(true) - otp.copy() + otpInstances[currentlySelectedSeed].copy() if defaults.bool(forKey: "pasteOnClick") { clipboard.paste() } @@ -98,24 +307,23 @@ class StatusMenuController: NSObject, NSMenuDelegate { button.highlight(false) } if (NSApp.currentEvent?.clickCount == 2) { -// print("Doubleclick") - if defaults.bool(forKey: "pasteOnDoubleClick") && AXIsProcessTrusted() { + if defaults.bool(forKey: "pasteOnDoubleClick") { clipboard.paste() } else { - otp.showAlert() + self.showAlert() } } } - + mouseView.onRightMouseDown = { button.performClick(NSApp.currentEvent) } - + button.addSubview(mouseView) let hotKey = HotKey(key: .g, modifiers: [.command, .option]) hotKey.handleKeyDown { - otp.copy() + otpInstances[currentlySelectedSeed].copy() if defaults.bool(forKey: "pasteOnHotkey") { clipboard.paste() } @@ -123,25 +331,42 @@ class StatusMenuController: NSObject, NSMenuDelegate { } } - @IBOutlet weak var tokenDisplay: NSMenuItem! - @IBAction func tokenDisplayClicked(_ sender: NSMenuItem) { - otp.copy() + func tryToAddInstance() { + let newInstance = OTP() + newInstance.button = statusItem.button + newInstance.start() + otpInstances.append(newInstance) + currentlySelectedSeed = otpInstances.count - 1 + showAlert() + } + + @IBAction func copyTokenClicked(_ sender: NSMenuItem) { + if otpInstances.isEmpty { + return + } + otpInstances[currentlySelectedSeed].copy() } @IBAction func pasteTokenClicked(_ sender: NSMenuItem) { - otp.copy() + if otpInstances.isEmpty { + return + } + otpInstances[currentlySelectedSeed].copy() clipboard.paste() } + @IBAction func addNewClicked(_ sender: NSMenuItem) { + tryToAddInstance() + } @IBOutlet weak var hotkeyButton: NSMenuItem! @IBAction func hotkeyButtonClicked(_ sender: NSMenuItem) { - defaults.toggleBool("pasteOnHotkey") + defaults.boolToggle("pasteOnHotkey") } @IBOutlet weak var pasteOnClickButton: NSMenuItem! @IBAction func pasteOnClickButtonClicked(_ sender: NSMenuItem) { - defaults.toggleBool("pasteOnClick") + defaults.boolToggle("pasteOnClick") if defaults.bool(forKey: "pasteOnDoubleClick") { - defaults.toggleBool("pasteOnDoubleClick") + defaults.boolToggle("pasteOnDoubleClick") } } @@ -152,24 +377,25 @@ class StatusMenuController: NSObject, NSMenuDelegate { @IBOutlet weak var pasteOnDoubleClickButton: NSMenuItem! @IBAction func pasteOnDoubleClickButtonClicked(_ sender: NSMenuItem) { - defaults.toggleBool("pasteOnDoubleClick") + defaults.boolToggle("pasteOnDoubleClick") if defaults.bool(forKey: "pasteOnClick") { - defaults.toggleBool("pasteOnClick") + defaults.boolToggle("pasteOnClick") } } func menuNeedsUpdate(_ menu: NSMenu) { - let tokenExists = !otp.token.isEmpty - if tokenExists { - tokenDisplay.title = otp.token - } - tokenDisplay.isHidden = !tokenExists - tokenDisplay.isEnabled = tokenExists + // print(currentlySelectedSeed) + // let tokenExists = !otpInstances[currentlySelectedSeed].token.isEmpty + // if tokenExists { + // otpInstances[currentlySelectedSeed].displayItem?.title = otpInstances[currentlySelectedSeed].token + // } + // otpInstances[currentlySelectedSeed].displayItem?.isHidden = !tokenExists + // otpInstances[currentlySelectedSeed].displayItem?.isEnabled = tokenExists - hotkeyButton.stateBy(defaults.bool(forKey: "pasteOnHotkey")) - pasteOnClickButton.stateBy(defaults.bool(forKey: "pasteOnClick")) - pasteOnDoubleClickButton.stateBy(defaults.bool(forKey: "pasteOnDoubleClick")) + hotkeyButton.state.by(defaults.bool(forKey: "pasteOnHotkey")) + pasteOnClickButton.state.by(defaults.bool(forKey: "pasteOnClick")) + pasteOnDoubleClickButton.state.by(defaults.bool(forKey: "pasteOnDoubleClick")) let isProcessTrusted = AXIsProcessTrusted() permissionsButton.isHidden = isProcessTrusted @@ -178,3 +404,73 @@ class StatusMenuController: NSObject, NSMenuDelegate { pasteOnDoubleClickButton.isEnabled = isProcessTrusted } } + +enum StringifyError: Error { + case isNotValidJSONObject +} + +struct JSONStringify { + + let value: Any + + func stringify(prettyPrinted: Bool = false) throws -> String { + let options: JSONSerialization.WritingOptions = prettyPrinted ? .prettyPrinted : .init(rawValue: 0) + if JSONSerialization.isValidJSONObject(self.value) { + let data = try JSONSerialization.data(withJSONObject: self.value, options: options) + if let string = String(data: data, encoding: .utf8) { + return string + + } + } + throw StringifyError.isNotValidJSONObject + } +} +protocol Stringifiable { + func stringify(prettyPrinted: Bool) throws -> String +} + +extension Stringifiable { + func stringify(prettyPrinted: Bool = false) throws -> String { + return try JSONStringify(value: self).stringify(prettyPrinted: prettyPrinted) + } +} + +extension Dictionary: Stringifiable {} +extension Array: Stringifiable {} + +//Usage: +//do { +// let stringified = try JSONStringify(value: ["name":"bob", "age":29]).stringify() +// print(stringified) +//} catch let error { print(error) } +// +////Or +//let dictionary = ["name":"bob", "age":29] as [String: Any] +//let stringifiedDictionary = try dictionary.stringify() +// +//let array = ["name","bob", "age",29] as [Any] +//let stringifiedArray = try array.stringify() +// +//print(stringifiedDictionary) +//print(stringifiedArray) + + +extension String +{ + func decodeUrl() -> String + { + return self.removingPercentEncoding! + } +} + +extension Data +{ + func dataToJSON() -> Any? { + do { + return try JSONSerialization.jsonObject(with: self, options: []) + } catch let myJSONError { + print(myJSONError) + } + return nil + } +} diff --git a/README.md b/README.md index 32ba410..e53edfe 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ brew cask install dafuqtor/tap/2fatotray #### Direct Download · **[Latest Release](//github.com/DaFuqtor/2FAtoTray/releases/latest/download/2FAtoTray.zip) ([![GitHub release](https://img.shields.io/github/release/dafuqtor/2fatotray?label=%20&color=gray)](//github.com/DaFuqtor/2FAtoTray/releases))** -> [![Build Status](https://app.bitrise.io/app/94efe673ad3ac640/status.svg?token=TgJkjwx_27BVoz877mKocQ)](https://app.bitrise.io/app/94efe673ad3ac640) -

:mag: Usage

@@ -29,21 +27,4 @@ brew cask install dafuqtor/tap/2fatotray

Double Click  ·  to change secret

Space  ·  type 2fa  ·  hit Enter ·  to copy

-

All the actions are also available in menu

- -
- -### Advanced settings - -
-For experienced users - -#### Instant Mode - -> After launching, the program copies the code and exits (so, completely minimizing battery usage) - -```powershell -defaults write com.dafuqtor.2FAtoTray instantMode 1 -``` - -
+

All the actions are also available in menu

\ No newline at end of file