├── Design ├── Orangered.sketch └── Orangered.svg ├── Orangered ├── Assets.xcassets │ ├── Contents.json │ ├── mod.imageset │ │ ├── mod.png │ │ ├── mod@2x.png │ │ └── Contents.json │ ├── active.imageset │ │ ├── active.png │ │ ├── active@2x.png │ │ └── Contents.json │ ├── alt-mod.imageset │ │ ├── alt-mod.png │ │ └── Contents.json │ ├── message.imageset │ │ ├── message.png │ │ ├── message@2x.png │ │ └── Contents.json │ ├── mod-dark.imageset │ │ ├── mod-dark.png │ │ ├── mod-dark@2x.png │ │ └── Contents.json │ ├── logged-in.imageset │ │ ├── logged-in.png │ │ ├── logged-in@2x.png │ │ └── Contents.json │ ├── alt-message.imageset │ │ ├── alt-message.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Orangered128x128.png │ │ ├── Orangered16x16.png │ │ ├── Orangered256x256.png │ │ ├── Orangered32x32.png │ │ ├── Orangered512x512.png │ │ └── Contents.json │ ├── alt-active.imageset │ │ ├── alt-active@2x.png │ │ └── Contents.json │ ├── alt-mod-dark.imageset │ │ ├── alt-mod-dark.png │ │ └── Contents.json │ ├── message-dark.imageset │ │ ├── message-dark.png │ │ ├── message-dark@2x.png │ │ └── Contents.json │ ├── alt-logged-in.imageset │ │ ├── alt-logged-in.png │ │ └── Contents.json │ ├── not-connected.imageset │ │ ├── not-connected.png │ │ ├── not-connected@2x.png │ │ └── Contents.json │ ├── logged-in-dark.imageset │ │ ├── logged-in-dark.png │ │ ├── logged-in-dark@2x.png │ │ └── Contents.json │ ├── alt-message-dark.imageset │ │ ├── alt-message-dark.png │ │ └── Contents.json │ ├── alt-not-connected.imageset │ │ ├── alt-not-connected.png │ │ └── Contents.json │ ├── alt-logged-in-dark.imageset │ │ ├── alt-logged-in-dark.png │ │ └── Contents.json │ ├── not-connected-dark.imageset │ │ ├── not-connected-dark.png │ │ ├── not-connected-dark@2x.png │ │ └── Contents.json │ ├── alt-not-connected-dark.imageset │ │ ├── alt-not-connected-dark.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── main.swift ├── Orangered.entitlements ├── AppDelegate.swift ├── Menu.swift ├── Info.plist ├── PrefViewController.swift ├── UserDefaults+Orangered.swift ├── LoginViewController.swift └── StatusItemController.swift ├── .gitignore ├── README.md └── Orangered.xcodeproj └── project.pbxproj /Design/Orangered.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Design/Orangered.sketch -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/mod.imageset/mod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/mod.imageset/mod.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/mod.imageset/mod@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/mod.imageset/mod@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/active.imageset/active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/active.imageset/active.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/active.imageset/active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/active.imageset/active@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-mod.imageset/alt-mod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-mod.imageset/alt-mod.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/message.imageset/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/message.imageset/message.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/message.imageset/message@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/message.imageset/message@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/mod-dark.imageset/mod-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/mod-dark.imageset/mod-dark.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/logged-in.imageset/logged-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/logged-in.imageset/logged-in.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/mod-dark.imageset/mod-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/mod-dark.imageset/mod-dark@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-message.imageset/alt-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-message.imageset/alt-message.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/logged-in.imageset/logged-in@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/logged-in.imageset/logged-in@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/AppIcon.appiconset/Orangered128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/AppIcon.appiconset/Orangered128x128.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/AppIcon.appiconset/Orangered16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/AppIcon.appiconset/Orangered16x16.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/AppIcon.appiconset/Orangered256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/AppIcon.appiconset/Orangered256x256.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/AppIcon.appiconset/Orangered32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/AppIcon.appiconset/Orangered32x32.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/AppIcon.appiconset/Orangered512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/AppIcon.appiconset/Orangered512x512.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-active.imageset/alt-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-active.imageset/alt-active@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-mod-dark.imageset/alt-mod-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-mod-dark.imageset/alt-mod-dark.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/message-dark.imageset/message-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/message-dark.imageset/message-dark.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-logged-in.imageset/alt-logged-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-logged-in.imageset/alt-logged-in.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/message-dark.imageset/message-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/message-dark.imageset/message-dark@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/not-connected.imageset/not-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/not-connected.imageset/not-connected.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/logged-in-dark.imageset/logged-in-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/logged-in-dark.imageset/logged-in-dark.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/not-connected.imageset/not-connected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/not-connected.imageset/not-connected@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-message-dark.imageset/alt-message-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-message-dark.imageset/alt-message-dark.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/logged-in-dark.imageset/logged-in-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/logged-in-dark.imageset/logged-in-dark@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-not-connected.imageset/alt-not-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-not-connected.imageset/alt-not-connected.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-logged-in-dark.imageset/alt-logged-in-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-logged-in-dark.imageset/alt-logged-in-dark.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/not-connected-dark.imageset/not-connected-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/not-connected-dark.imageset/not-connected-dark.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/not-connected-dark.imageset/not-connected-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/not-connected-dark.imageset/not-connected-dark@2x.png -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-not-connected-dark.imageset/alt-not-connected-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidref/orangered/HEAD/Orangered/Assets.xcassets/alt-not-connected-dark.imageset/alt-not-connected-dark.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *xcodeproj/*mode* 3 | *xcodeproj/*pbxuser 4 | *xcodeproj/*per* 5 | *xcodeproj/project.xcworkspace 6 | *xcodeproj/xcuserdata 7 | *tmproj 8 | .DS_Store 9 | profile 10 | *.pbxuser 11 | *.mode1v3 12 | -------------------------------------------------------------------------------- /Orangered/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Orangered 4 | // 5 | // Created by Alan Westbrook on 5/29/16. 6 | // Copyright © 2016 Rockwood Software. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | autoreleasepool { () -> () in 12 | let app = NSApplication.shared 13 | let delegate = AppDelegate() 14 | app.delegate = delegate 15 | app.run() 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Orangered/Orangered.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-mod.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alt-mod.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-active.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "alt-active@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-logged-in.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alt-logged-in.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-message.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alt-message.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-mod-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alt-mod-dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-logged-in-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alt-logged-in-dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-message-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alt-message-dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-not-connected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alt-not-connected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/alt-not-connected-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "alt-not-connected-dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/mod.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "mod.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "mod@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 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/active.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "active.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "active@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 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/message.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "message.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "message@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 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/mod-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "mod-dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "mod-dark@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 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/logged-in.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logged-in.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "logged-in@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 | } -------------------------------------------------------------------------------- /Orangered/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Orangered-Swift 4 | // 5 | // Created by Alan Westbrook on 5/29/16. 6 | // Copyright © 2016 Rockwood Software. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | var controller:StatusItemController! 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | controller = StatusItemController() 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/message-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "message-dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "message-dark@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 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/not-connected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "not-connected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "not-connected@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 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/logged-in-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logged-in-dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "logged-in-dark@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 | } -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/not-connected-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "not-connected-dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "not-connected-dark@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 | } -------------------------------------------------------------------------------- /Orangered/Menu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Menu.swift 3 | // Orangered 4 | // 5 | // Created by Alan Westbrook on 6/13/16. 6 | // Copyright © 2016 Rockwood Software. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | 13 | class Menu: NSMenu { 14 | 15 | init() { 16 | super.init(title: "Orangered!") 17 | 18 | setup() 19 | } 20 | 21 | required init(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | fileprivate func setup() { 26 | autoenablesItems = false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Orangered! 3 | 4 | This small application is designed to sit up in your Mac OS X menu bar and poll every 60 seconds to see if you have any reddit.com messages awaiting your feverent attention. 5 | 6 | This is for testing a PR, do not merge. 7 | 8 | For the reddit obsessed. 9 | 10 | # UFO Colors 11 | 12 | * Outline : Not logged in 13 | * Filled : Logged in, no messages 14 | * Orange Canopy: You have a message! Make with the clicking! 15 | * Magenta Canopy: Mod mail, look at you, Mr. Importante! 16 | 17 | 18 | # Special Thanks to the following redditors: 19 | * Traviscat 20 | * ashleyw 21 | * Condawg 22 | * dawnerd 23 | * despideme 24 | * derekaw 25 | * EthicalReasoning 26 | * giftedmunchkin 27 | * kevinhoagland 28 | * loggedout 29 | * polyGone 30 | * RamenStein 31 | * shinratdr 32 | * sporadicmonster 33 | * pkamb 34 | * sammcj 35 | 36 | # Features 37 | 38 | This application is deliberately as featureless as possible, if you are looking for a commercially supported expierence, you may look at Peter Kamb's excellent Orangered notifier for Reddit: https://itunes.apple.com/us/app/orangered-notifier-for-reddit/id468366517?mt=12 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Orangered/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleGetInfoString 10 | 2.0, http://www.voidref.com/orangered/Orangered!.html 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 3.0.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | LSUIElement 30 | 31 | NSHumanReadableCopyright 32 | Copyright © 2018 Rockwood Software. All rights reserved. 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /Orangered/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "Orangered16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "idiom" : "mac", 11 | "size" : "16x16", 12 | "scale" : "2x" 13 | }, 14 | { 15 | "size" : "32x32", 16 | "idiom" : "mac", 17 | "filename" : "Orangered32x32.png", 18 | "scale" : "1x" 19 | }, 20 | { 21 | "idiom" : "mac", 22 | "size" : "32x32", 23 | "scale" : "2x" 24 | }, 25 | { 26 | "size" : "128x128", 27 | "idiom" : "mac", 28 | "filename" : "Orangered128x128.png", 29 | "scale" : "1x" 30 | }, 31 | { 32 | "idiom" : "mac", 33 | "size" : "128x128", 34 | "scale" : "2x" 35 | }, 36 | { 37 | "size" : "256x256", 38 | "idiom" : "mac", 39 | "filename" : "Orangered256x256.png", 40 | "scale" : "1x" 41 | }, 42 | { 43 | "idiom" : "mac", 44 | "size" : "256x256", 45 | "scale" : "2x" 46 | }, 47 | { 48 | "size" : "512x512", 49 | "idiom" : "mac", 50 | "filename" : "Orangered512x512.png", 51 | "scale" : "1x" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "size" : "512x512", 56 | "scale" : "2x" 57 | } 58 | ], 59 | "info" : { 60 | "version" : 1, 61 | "author" : "xcode" 62 | } 63 | } -------------------------------------------------------------------------------- /Orangered/PrefViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefViewController.swift 3 | // Orangered 4 | // 5 | // Created by Alan Westbrook on 6/17/16. 6 | // Copyright © 2016 Rockwood Software. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ServiceManagement 11 | 12 | class PrefViewController: NSViewController { 13 | 14 | let startAtLogin = NSButton(cbWithTitle: "Start at Login", target: nil, action: #selector(salClicked)) 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | setup() 20 | } 21 | 22 | override func loadView() { 23 | // Don't call into super, as we don't want it to try to load from a nib 24 | view = NSView() 25 | view.translatesAutoresizingMaskIntoConstraints = false 26 | 27 | } 28 | 29 | fileprivate func setup() { 30 | startAtLogin.target = self 31 | startAtLogin.translatesAutoresizingMaskIntoConstraints = false 32 | 33 | title = "Orangered! Preferences" 34 | 35 | view.addSubview(startAtLogin) 36 | 37 | NSLayoutConstraint.activate([ 38 | startAtLogin.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10), 39 | startAtLogin.topAnchor.constraint(equalTo: view.topAnchor, constant: 10), 40 | view.bottomAnchor.constraint(equalTo: startAtLogin.bottomAnchor, constant: 10), 41 | view.widthAnchor.constraint(equalTo: startAtLogin.widthAnchor, constant: 20) 42 | ]) 43 | } 44 | 45 | @objc fileprivate func salClicked() { 46 | print(SMLoginItemSetEnabled("com.rockwood.Orangered" as CFString, true)) 47 | } 48 | } 49 | 50 | extension NSButton { 51 | convenience init(cbWithTitle title: String, target: NSObject?, action: Selector) { 52 | if #available(OSX 10.12, *) { 53 | self.init(checkboxWithTitle: title, target: target, action: action) 54 | } else { 55 | self.init() 56 | setButtonType(.switch) 57 | self.title = title 58 | self.target = target 59 | self.action = action 60 | } 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Orangered/UserDefaults+Orangered.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Orangered.swift 3 | // Orangered 4 | // 5 | // Created by Alan Westbrook on 6/13/16. 6 | // Copyright © 2016 Rockwood Software. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | private let kUserNameKey = "username" 13 | private let kLoggedInKey = "logged in" 14 | private let kUseAltImagesKey = "use alt images" 15 | private let kMailCountKey = "unread inbox count" 16 | private let kServiceName = "Orangered!" 17 | 18 | extension UserDefaults { 19 | var username:String? { 20 | get { 21 | return string(forKey: kUserNameKey) 22 | } 23 | set { 24 | set(newValue, forKey: kUserNameKey) 25 | } 26 | } 27 | 28 | var password:String? { 29 | get { 30 | let pass = getPassword() 31 | UserDefaults.keychainItem = nil 32 | return pass 33 | } 34 | set { 35 | setPassword(newValue!) 36 | } 37 | } 38 | 39 | var loggedIn:Bool { 40 | get { 41 | return bool(forKey: kLoggedInKey) 42 | } 43 | set { 44 | set(newValue, forKey: kLoggedInKey) 45 | } 46 | } 47 | 48 | var useAltImages:Bool { 49 | get { 50 | return bool(forKey: kUseAltImagesKey) 51 | } 52 | set { 53 | set(newValue, forKey: kUseAltImagesKey) 54 | } 55 | } 56 | 57 | var mailCount:Int { 58 | get { 59 | return integer(forKey: kMailCountKey) 60 | } 61 | set { 62 | set(newValue, forKey: kMailCountKey) 63 | } 64 | } 65 | 66 | // C APIs are *the worst* 67 | static fileprivate var keychainItem:SecKeychainItem? = nil 68 | 69 | fileprivate func getPassword() -> String? { 70 | 71 | guard let uname = username else { 72 | print("no user name set") 73 | return nil 74 | } 75 | 76 | var passwordLength:UInt32 = 0 77 | var passwordData:UnsafeMutableRawPointer? = nil 78 | 79 | let err = SecKeychainFindGenericPassword(nil, 80 | UInt32(kServiceName.count), 81 | kServiceName, 82 | UInt32(uname.count), 83 | uname, 84 | &passwordLength, 85 | &passwordData, 86 | &UserDefaults.keychainItem) 87 | if let pass = passwordData, err == errSecSuccess { 88 | let password = String(bytesNoCopy: pass, length: Int(passwordLength), encoding: String.Encoding.utf8, freeWhenDone: true) 89 | return password 90 | } 91 | else { 92 | print("Error grabbing password: \(err)") 93 | } 94 | 95 | return nil 96 | } 97 | 98 | fileprivate func setPassword(_ pass:String) { 99 | guard let uname = username else { 100 | print("No username") 101 | return 102 | } 103 | 104 | if let _ = getPassword() { 105 | // have to update instead of setting 106 | updatePassword(pass) 107 | return 108 | } 109 | 110 | let result = SecKeychainAddGenericPassword(nil, 111 | UInt32(kServiceName.count), 112 | kServiceName, 113 | UInt32(uname.count), 114 | uname, 115 | UInt32(pass.count), 116 | pass, 117 | nil) 118 | 119 | if result != errSecSuccess { 120 | print("error setting key: \(result)") 121 | } 122 | } 123 | 124 | fileprivate func updatePassword(_ password:String) { 125 | guard let itemActual = UserDefaults.keychainItem else { 126 | print("Must grab a password to init the item before updatint it, bleah") 127 | return 128 | } 129 | 130 | let result = SecKeychainItemModifyAttributesAndData(itemActual, 131 | nil, 132 | UInt32(password.count), 133 | password) 134 | 135 | if result != errSecSuccess { 136 | print("error updating the keychain: \(result)") 137 | } 138 | 139 | UserDefaults.keychainItem = nil 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Orangered/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // Orangered 4 | // 5 | // Created by Alan Westbrook on 6/13/16. 6 | // Copyright © 2016 Rockwood Software. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | let kHelpURL = URL(string: "https://www.github.com/voidref/orangered") 12 | 13 | typealias LoginAction = (_:String, _ password:String) -> Void 14 | 15 | class LoginViewController: NSViewController { 16 | 17 | fileprivate let nameLabel = NSTextField() 18 | fileprivate let passwordLabel = NSTextField() 19 | fileprivate let nameField = NSTextField() 20 | fileprivate let passwordField = NSSecureTextField() 21 | fileprivate let loginButton = NSButton() 22 | fileprivate let helpButton = NSButton() 23 | fileprivate let loginAction:LoginAction 24 | 25 | init(_ action: @escaping LoginAction) { 26 | loginAction = action 27 | 28 | super.init(nibName: nil, bundle: nil) 29 | 30 | title = NSLocalizedString("Orangered! Login", comment: "The login window title") 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | setup() 40 | } 41 | 42 | override func loadView() { 43 | // Don't call into super, as we don't want it to try to load from a nib 44 | view = NSVisualEffectView() 45 | view.translatesAutoresizingMaskIntoConstraints = false 46 | } 47 | 48 | fileprivate func setup() { 49 | [nameLabel, 50 | nameField, 51 | passwordLabel, 52 | passwordField, 53 | loginButton, 54 | helpButton].forEach { add($0) } 55 | 56 | func confgure(label:NSTextField, text:String) { 57 | label.stringValue = text 58 | label.isEditable = false 59 | label.backgroundColor = #colorLiteral(red: 0.6470588235, green: 0.631372549, blue: 0.7725490196, alpha: 0) 60 | label.drawsBackground = false 61 | label.sizeToFit() 62 | label.isBezeled = false 63 | } 64 | 65 | confgure(label: nameLabel, 66 | text: NSLocalizedString("User Name:", comment: "reddit user name field label")) 67 | confgure(label: passwordLabel, 68 | text: NSLocalizedString("Password:", comment: "password field label")) 69 | 70 | let pref = UserDefaults.standard 71 | nameField.stringValue = pref.username ?? "" 72 | passwordField.stringValue = pref.password ?? "" 73 | 74 | loginButton.title = NSLocalizedString("Login", comment: "login button title on the login window") 75 | loginButton.bezelStyle = .rounded 76 | loginButton.keyEquivalent = "\r" 77 | loginButton.target = self 78 | loginButton.action = #selector(loginClicked) 79 | 80 | helpButton.bezelStyle = .helpButton 81 | helpButton.title = "" 82 | helpButton.target = self 83 | helpButton.action = #selector(helpClicked) 84 | 85 | let space:CGFloat = 16 86 | let fieldWidth:CGFloat = 160 87 | 88 | let fieldGuide = NSLayoutGuide() 89 | view.addLayoutGuide(fieldGuide) 90 | 91 | 92 | NSLayoutConstraint.activate([ 93 | fieldGuide.topAnchor.constraint(equalTo: nameLabel.topAnchor), 94 | fieldGuide.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), 95 | fieldGuide.trailingAnchor.constraint(equalTo: nameField.trailingAnchor), 96 | fieldGuide.centerXAnchor.constraint(equalTo: view.centerXAnchor), 97 | fieldGuide.bottomAnchor.constraint(equalTo: passwordLabel.bottomAnchor), 98 | 99 | nameLabel.trailingAnchor.constraint(equalTo: nameField.leadingAnchor, constant: -space / 2), 100 | nameField.firstBaselineAnchor.constraint(equalTo: nameLabel.firstBaselineAnchor), 101 | nameField.widthAnchor.constraint(equalToConstant: fieldWidth), 102 | 103 | passwordLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: space / 2), 104 | passwordLabel.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor), 105 | passwordField.firstBaselineAnchor.constraint(equalTo: passwordLabel.firstBaselineAnchor), 106 | passwordField.leadingAnchor.constraint(equalTo: nameField.leadingAnchor), 107 | passwordField.trailingAnchor.constraint(equalTo: nameField.trailingAnchor), 108 | 109 | loginButton.topAnchor.constraint(equalTo: fieldGuide.bottomAnchor, constant: space), 110 | loginButton.trailingAnchor.constraint(equalTo: fieldGuide.trailingAnchor), 111 | 112 | helpButton.leadingAnchor.constraint(equalTo: fieldGuide.leadingAnchor), 113 | helpButton.centerYAnchor.constraint(equalTo: loginButton.centerYAnchor), 114 | 115 | view.topAnchor.constraint(equalTo: nameLabel.topAnchor, constant: -space), 116 | view.bottomAnchor.constraint(equalTo: loginButton.bottomAnchor, constant: space), 117 | view.widthAnchor.constraint(equalTo: fieldGuide.widthAnchor, constant: space * 2) 118 | ]) 119 | } 120 | 121 | fileprivate func add(_ sub:NSView) { 122 | sub.translatesAutoresizingMaskIntoConstraints = false 123 | view.addSubview(sub) 124 | } 125 | 126 | @objc private func loginClicked() { 127 | loginAction(nameField.stringValue, passwordField.stringValue) 128 | } 129 | 130 | @objc fileprivate func helpClicked() { 131 | if let urlActual = kHelpURL { 132 | NSWorkspace.shared.open(urlActual) 133 | } 134 | else { 135 | print("Umm, fix your url?") 136 | } 137 | } 138 | } 139 | 140 | -------------------------------------------------------------------------------- /Design/Orangered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | 30 | 31 | 32 | 33 | 35 | 42 | 49 | 56 | 63 | 64 | 84 | 97 | 107 | 117 | 130 | 140 | 150 | 160 | 170 | 174 | 187 | 200 | 204 | 207 | 216 | 220 | 224 | 228 | 229 | 242 | 255 | 256 | -------------------------------------------------------------------------------- /Orangered.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E7D03FA62884BA8600D8A600 /* Base.lproj in Resources */ = {isa = PBXBuildFile; fileRef = E7D03F9B2884BA8600D8A600 /* Base.lproj */; }; 11 | E7D03FA72884BA8600D8A600 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = E7D03F9C2884BA8600D8A600 /* Info.plist */; }; 12 | E7D03FA82884BA8600D8A600 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D03F9D2884BA8600D8A600 /* main.swift */; }; 13 | E7D03FA92884BA8600D8A600 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E7D03F9E2884BA8600D8A600 /* Assets.xcassets */; }; 14 | E7D03FAA2884BA8600D8A600 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D03FA02884BA8600D8A600 /* AppDelegate.swift */; }; 15 | E7D03FAB2884BA8600D8A600 /* StatusItemController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D03FA12884BA8600D8A600 /* StatusItemController.swift */; }; 16 | E7D03FAC2884BA8600D8A600 /* PrefViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D03FA22884BA8600D8A600 /* PrefViewController.swift */; }; 17 | E7D03FAD2884BA8600D8A600 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D03FA32884BA8600D8A600 /* LoginViewController.swift */; }; 18 | E7D03FAE2884BA8600D8A600 /* Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D03FA42884BA8600D8A600 /* Menu.swift */; }; 19 | E7D03FAF2884BA8600D8A600 /* UserDefaults+Orangered.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D03FA52884BA8600D8A600 /* UserDefaults+Orangered.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | E7D03F882884B94800D8A600 /* Orangered.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Orangered.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | E7D03F9B2884BA8600D8A600 /* Base.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Base.lproj; path = Orangered/Base.lproj; sourceTree = ""; }; 25 | E7D03F9C2884BA8600D8A600 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Orangered/Info.plist; sourceTree = ""; }; 26 | E7D03F9D2884BA8600D8A600 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = Orangered/main.swift; sourceTree = ""; }; 27 | E7D03F9E2884BA8600D8A600 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Orangered/Assets.xcassets; sourceTree = ""; }; 28 | E7D03F9F2884BA8600D8A600 /* Orangered.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = Orangered.entitlements; path = Orangered/Orangered.entitlements; sourceTree = ""; }; 29 | E7D03FA02884BA8600D8A600 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Orangered/AppDelegate.swift; sourceTree = ""; }; 30 | E7D03FA12884BA8600D8A600 /* StatusItemController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StatusItemController.swift; path = Orangered/StatusItemController.swift; sourceTree = ""; }; 31 | E7D03FA22884BA8600D8A600 /* PrefViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PrefViewController.swift; path = Orangered/PrefViewController.swift; sourceTree = ""; }; 32 | E7D03FA32884BA8600D8A600 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoginViewController.swift; path = Orangered/LoginViewController.swift; sourceTree = ""; }; 33 | E7D03FA42884BA8600D8A600 /* Menu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Menu.swift; path = Orangered/Menu.swift; sourceTree = ""; }; 34 | E7D03FA52884BA8600D8A600 /* UserDefaults+Orangered.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UserDefaults+Orangered.swift"; path = "Orangered/UserDefaults+Orangered.swift"; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | E7D03F852884B94800D8A600 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | E7D03F7F2884B94800D8A600 = { 49 | isa = PBXGroup; 50 | children = ( 51 | E7D03FA02884BA8600D8A600 /* AppDelegate.swift */, 52 | E7D03F9E2884BA8600D8A600 /* Assets.xcassets */, 53 | E7D03F9B2884BA8600D8A600 /* Base.lproj */, 54 | E7D03F9C2884BA8600D8A600 /* Info.plist */, 55 | E7D03FA32884BA8600D8A600 /* LoginViewController.swift */, 56 | E7D03F9D2884BA8600D8A600 /* main.swift */, 57 | E7D03FA42884BA8600D8A600 /* Menu.swift */, 58 | E7D03F9F2884BA8600D8A600 /* Orangered.entitlements */, 59 | E7D03FA22884BA8600D8A600 /* PrefViewController.swift */, 60 | E7D03FA12884BA8600D8A600 /* StatusItemController.swift */, 61 | E7D03FA52884BA8600D8A600 /* UserDefaults+Orangered.swift */, 62 | E7D03F892884B94800D8A600 /* Products */, 63 | ); 64 | sourceTree = ""; 65 | }; 66 | E7D03F892884B94800D8A600 /* Products */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | E7D03F882884B94800D8A600 /* Orangered.app */, 70 | ); 71 | name = Products; 72 | sourceTree = ""; 73 | }; 74 | /* End PBXGroup section */ 75 | 76 | /* Begin PBXNativeTarget section */ 77 | E7D03F872884B94800D8A600 /* Orangered */ = { 78 | isa = PBXNativeTarget; 79 | buildConfigurationList = E7D03F952884B94900D8A600 /* Build configuration list for PBXNativeTarget "Orangered" */; 80 | buildPhases = ( 81 | E7D03F842884B94800D8A600 /* Sources */, 82 | E7D03F852884B94800D8A600 /* Frameworks */, 83 | E7D03F862884B94800D8A600 /* Resources */, 84 | ); 85 | buildRules = ( 86 | ); 87 | dependencies = ( 88 | ); 89 | name = Orangered; 90 | productName = Orangered; 91 | productReference = E7D03F882884B94800D8A600 /* Orangered.app */; 92 | productType = "com.apple.product-type.application"; 93 | }; 94 | /* End PBXNativeTarget section */ 95 | 96 | /* Begin PBXProject section */ 97 | E7D03F802884B94800D8A600 /* Project object */ = { 98 | isa = PBXProject; 99 | attributes = { 100 | BuildIndependentTargetsInParallel = 1; 101 | LastSwiftUpdateCheck = 1400; 102 | LastUpgradeCheck = 1400; 103 | TargetAttributes = { 104 | E7D03F872884B94800D8A600 = { 105 | CreatedOnToolsVersion = 14.0; 106 | LastSwiftMigration = 1400; 107 | }; 108 | }; 109 | }; 110 | buildConfigurationList = E7D03F832884B94800D8A600 /* Build configuration list for PBXProject "Orangered" */; 111 | compatibilityVersion = "Xcode 14.0"; 112 | developmentRegion = en; 113 | hasScannedForEncodings = 0; 114 | knownRegions = ( 115 | en, 116 | Base, 117 | ); 118 | mainGroup = E7D03F7F2884B94800D8A600; 119 | productRefGroup = E7D03F892884B94800D8A600 /* Products */; 120 | projectDirPath = ""; 121 | projectRoot = ""; 122 | targets = ( 123 | E7D03F872884B94800D8A600 /* Orangered */, 124 | ); 125 | }; 126 | /* End PBXProject section */ 127 | 128 | /* Begin PBXResourcesBuildPhase section */ 129 | E7D03F862884B94800D8A600 /* Resources */ = { 130 | isa = PBXResourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | E7D03FA72884BA8600D8A600 /* Info.plist in Resources */, 134 | E7D03FA92884BA8600D8A600 /* Assets.xcassets in Resources */, 135 | E7D03FA62884BA8600D8A600 /* Base.lproj in Resources */, 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | /* End PBXResourcesBuildPhase section */ 140 | 141 | /* Begin PBXSourcesBuildPhase section */ 142 | E7D03F842884B94800D8A600 /* Sources */ = { 143 | isa = PBXSourcesBuildPhase; 144 | buildActionMask = 2147483647; 145 | files = ( 146 | E7D03FAE2884BA8600D8A600 /* Menu.swift in Sources */, 147 | E7D03FAA2884BA8600D8A600 /* AppDelegate.swift in Sources */, 148 | E7D03FAF2884BA8600D8A600 /* UserDefaults+Orangered.swift in Sources */, 149 | E7D03FA82884BA8600D8A600 /* main.swift in Sources */, 150 | E7D03FAB2884BA8600D8A600 /* StatusItemController.swift in Sources */, 151 | E7D03FAD2884BA8600D8A600 /* LoginViewController.swift in Sources */, 152 | E7D03FAC2884BA8600D8A600 /* PrefViewController.swift in Sources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXSourcesBuildPhase section */ 157 | 158 | /* Begin XCBuildConfiguration section */ 159 | E7D03F932884B94900D8A600 /* Debug */ = { 160 | isa = XCBuildConfiguration; 161 | buildSettings = { 162 | ALWAYS_SEARCH_USER_PATHS = NO; 163 | CLANG_ANALYZER_NONNULL = YES; 164 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 165 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 166 | CLANG_ENABLE_MODULES = YES; 167 | CLANG_ENABLE_OBJC_ARC = YES; 168 | CLANG_ENABLE_OBJC_WEAK = YES; 169 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 170 | CLANG_WARN_BOOL_CONVERSION = YES; 171 | CLANG_WARN_COMMA = YES; 172 | CLANG_WARN_CONSTANT_CONVERSION = YES; 173 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 174 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 175 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 176 | CLANG_WARN_EMPTY_BODY = YES; 177 | CLANG_WARN_ENUM_CONVERSION = YES; 178 | CLANG_WARN_INFINITE_RECURSION = YES; 179 | CLANG_WARN_INT_CONVERSION = YES; 180 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 181 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 182 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 183 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 184 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 185 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 186 | CLANG_WARN_STRICT_PROTOTYPES = YES; 187 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 188 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 189 | CLANG_WARN_UNREACHABLE_CODE = YES; 190 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 191 | COPY_PHASE_STRIP = NO; 192 | DEBUG_INFORMATION_FORMAT = dwarf; 193 | ENABLE_STRICT_OBJC_MSGSEND = YES; 194 | ENABLE_TESTABILITY = YES; 195 | GCC_C_LANGUAGE_STANDARD = gnu11; 196 | GCC_DYNAMIC_NO_PIC = NO; 197 | GCC_NO_COMMON_BLOCKS = YES; 198 | GCC_OPTIMIZATION_LEVEL = 0; 199 | GCC_PREPROCESSOR_DEFINITIONS = ( 200 | "DEBUG=1", 201 | "$(inherited)", 202 | ); 203 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 204 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 205 | GCC_WARN_UNDECLARED_SELECTOR = YES; 206 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 207 | GCC_WARN_UNUSED_FUNCTION = YES; 208 | GCC_WARN_UNUSED_VARIABLE = YES; 209 | MACOSX_DEPLOYMENT_TARGET = 12.4; 210 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 211 | MTL_FAST_MATH = YES; 212 | ONLY_ACTIVE_ARCH = YES; 213 | SDKROOT = macosx; 214 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 215 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 216 | }; 217 | name = Debug; 218 | }; 219 | E7D03F942884B94900D8A600 /* Release */ = { 220 | isa = XCBuildConfiguration; 221 | buildSettings = { 222 | ALWAYS_SEARCH_USER_PATHS = NO; 223 | CLANG_ANALYZER_NONNULL = YES; 224 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 225 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 226 | CLANG_ENABLE_MODULES = YES; 227 | CLANG_ENABLE_OBJC_ARC = YES; 228 | CLANG_ENABLE_OBJC_WEAK = YES; 229 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 230 | CLANG_WARN_BOOL_CONVERSION = YES; 231 | CLANG_WARN_COMMA = YES; 232 | CLANG_WARN_CONSTANT_CONVERSION = YES; 233 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 236 | CLANG_WARN_EMPTY_BODY = YES; 237 | CLANG_WARN_ENUM_CONVERSION = YES; 238 | CLANG_WARN_INFINITE_RECURSION = YES; 239 | CLANG_WARN_INT_CONVERSION = YES; 240 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 241 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 244 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 245 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 246 | CLANG_WARN_STRICT_PROTOTYPES = YES; 247 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 248 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 249 | CLANG_WARN_UNREACHABLE_CODE = YES; 250 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 251 | COPY_PHASE_STRIP = NO; 252 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 253 | ENABLE_NS_ASSERTIONS = NO; 254 | ENABLE_STRICT_OBJC_MSGSEND = YES; 255 | GCC_C_LANGUAGE_STANDARD = gnu11; 256 | GCC_NO_COMMON_BLOCKS = YES; 257 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 258 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 259 | GCC_WARN_UNDECLARED_SELECTOR = YES; 260 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 261 | GCC_WARN_UNUSED_FUNCTION = YES; 262 | GCC_WARN_UNUSED_VARIABLE = YES; 263 | MACOSX_DEPLOYMENT_TARGET = 12.4; 264 | MTL_ENABLE_DEBUG_INFO = NO; 265 | MTL_FAST_MATH = YES; 266 | SDKROOT = macosx; 267 | SWIFT_COMPILATION_MODE = wholemodule; 268 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 269 | }; 270 | name = Release; 271 | }; 272 | E7D03F962884B94900D8A600 /* Debug */ = { 273 | isa = XCBuildConfiguration; 274 | buildSettings = { 275 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 276 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 277 | CLANG_ENABLE_MODULES = YES; 278 | CODE_SIGN_ENTITLEMENTS = Orangered/Orangered.entitlements; 279 | CODE_SIGN_STYLE = Automatic; 280 | COMBINE_HIDPI_IMAGES = YES; 281 | CURRENT_PROJECT_VERSION = 1; 282 | DEVELOPMENT_TEAM = SR7K2S8GE4; 283 | ENABLE_HARDENED_RUNTIME = YES; 284 | GENERATE_INFOPLIST_FILE = YES; 285 | INFOPLIST_KEY_CFBundleDisplayName = Orangered; 286 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 287 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 288 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 289 | LD_RUNPATH_SEARCH_PATHS = ( 290 | "$(inherited)", 291 | "@executable_path/../Frameworks", 292 | ); 293 | MARKETING_VERSION = 3.0; 294 | PRODUCT_BUNDLE_IDENTIFIER = com.rockwood.Orangered; 295 | PRODUCT_NAME = "$(TARGET_NAME)"; 296 | SWIFT_EMIT_LOC_STRINGS = YES; 297 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 298 | SWIFT_VERSION = 5.0; 299 | }; 300 | name = Debug; 301 | }; 302 | E7D03F972884B94900D8A600 /* Release */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 306 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 307 | CLANG_ENABLE_MODULES = YES; 308 | CODE_SIGN_ENTITLEMENTS = Orangered/Orangered.entitlements; 309 | CODE_SIGN_STYLE = Automatic; 310 | COMBINE_HIDPI_IMAGES = YES; 311 | CURRENT_PROJECT_VERSION = 1; 312 | DEVELOPMENT_TEAM = SR7K2S8GE4; 313 | ENABLE_HARDENED_RUNTIME = YES; 314 | GENERATE_INFOPLIST_FILE = YES; 315 | INFOPLIST_KEY_CFBundleDisplayName = Orangered; 316 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 317 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 318 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 319 | LD_RUNPATH_SEARCH_PATHS = ( 320 | "$(inherited)", 321 | "@executable_path/../Frameworks", 322 | ); 323 | MARKETING_VERSION = 3.0; 324 | PRODUCT_BUNDLE_IDENTIFIER = com.rockwood.Orangered; 325 | PRODUCT_NAME = "$(TARGET_NAME)"; 326 | SWIFT_EMIT_LOC_STRINGS = YES; 327 | SWIFT_VERSION = 5.0; 328 | }; 329 | name = Release; 330 | }; 331 | /* End XCBuildConfiguration section */ 332 | 333 | /* Begin XCConfigurationList section */ 334 | E7D03F832884B94800D8A600 /* Build configuration list for PBXProject "Orangered" */ = { 335 | isa = XCConfigurationList; 336 | buildConfigurations = ( 337 | E7D03F932884B94900D8A600 /* Debug */, 338 | E7D03F942884B94900D8A600 /* Release */, 339 | ); 340 | defaultConfigurationIsVisible = 0; 341 | defaultConfigurationName = Release; 342 | }; 343 | E7D03F952884B94900D8A600 /* Build configuration list for PBXNativeTarget "Orangered" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | E7D03F962884B94900D8A600 /* Debug */, 347 | E7D03F972884B94900D8A600 /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | /* End XCConfigurationList section */ 353 | }; 354 | rootObject = E7D03F802884B94800D8A600 /* Project object */; 355 | } 356 | -------------------------------------------------------------------------------- /Orangered/StatusItemController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusItemController.swift 3 | // Orangered 4 | // 5 | // Created by Alan Westbrook on 6/13/16. 6 | // Copyright © 2016 Rockwood Software. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | import UserNotifications 12 | 13 | private let kUpdateURL = URL(string: "http://voidref.com/orangered/version") 14 | private let kRedditCookieURL = URL(string: "https://reddit.com") 15 | private let kLoginMenuTitle = NSLocalizedString("Login…", comment: "Menu item title for bringing up the login window") 16 | private let kLogoutMenuTitle = NSLocalizedString("Log Out", comment: "Menu item title for logging out") 17 | private let kAttemptingLoginTitle = NSLocalizedString("Attempting Login…", comment: "Title of the login menu item while it's attemping to log in") 18 | private let kOpenMailboxRecheckDelay = 5.0 19 | 20 | class StatusItemController: NSObject, UNUserNotificationCenterDelegate { 21 | 22 | enum State { 23 | case loggedout 24 | case invalidcredentials 25 | case disconnected 26 | case mailfree 27 | case orangered 28 | case modmail 29 | case update 30 | 31 | fileprivate static let urlMap = [ 32 | loggedout: nil, 33 | invalidcredentials: nil, 34 | disconnected: nil, 35 | mailfree: URL(string: "https://www.reddit.com/message/inbox/"), 36 | orangered: URL(string: "https://www.reddit.com/message/unread/"), 37 | modmail: URL(string: "https://www.reddit.com/message/moderator/"), 38 | update: nil 39 | ] 40 | 41 | func image(forAppearance appearanceName: String, useAlt:Bool = false) -> NSImage { 42 | let imageMap = [ 43 | State.loggedout: "not-connected", 44 | State.invalidcredentials: "not-connected", 45 | State.disconnected: "not-connected", 46 | State.mailfree: "logged-in", 47 | State.orangered: "message", 48 | State.modmail: "mod", 49 | State.update: "BlueEnvelope" // TODO: Sort this out 50 | ] 51 | 52 | guard let basename = imageMap[self] else { 53 | fatalError("you really messed up this time, missing case: imageMap for \(self)") 54 | } 55 | 56 | var name = basename 57 | 58 | if useAlt { 59 | name = "alt-\(basename)" 60 | } 61 | 62 | if appearanceName == NSAppearance.Name.vibrantDark.rawValue { 63 | name = "\(name)-dark" 64 | } 65 | 66 | 67 | guard let image = NSImage(named: name) else { 68 | fatalError("fix yo assets, missing image: \(name)") 69 | } 70 | 71 | return image 72 | } 73 | 74 | func mailboxUrl() -> URL? { 75 | return State.urlMap[self]! 76 | } 77 | } 78 | 79 | fileprivate var state = State.disconnected { 80 | willSet { 81 | if newValue != state { 82 | // In order to avoid having to set flags for handling values set that were already set, we check `willSet`. This, however, necessitates we reschedule handling until the value is actually set as there doesn't seem to be a way to let it set and then call a method synchronously 83 | DispatchQueue.main.async(execute: { 84 | self.handleStateChanged() 85 | }) 86 | } 87 | } 88 | } 89 | 90 | fileprivate let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) 91 | 92 | fileprivate var statusPoller:Timer? 93 | fileprivate let prefs = UserDefaults.standard 94 | fileprivate var statusConnection:URLSession? 95 | fileprivate let session = URLSession.shared 96 | fileprivate var loginWindowController:NSWindowController? 97 | fileprivate var prefWindowController:NSWindowController? 98 | fileprivate var mailboxItem:NSMenuItem? 99 | fileprivate var loginItem:NSMenuItem? 100 | fileprivate var mailCount:Int { 101 | get { 102 | return prefs.mailCount 103 | } 104 | set { 105 | prefs.mailCount = newValue 106 | } 107 | } 108 | 109 | override init() { 110 | super.init() 111 | 112 | prefs.useAltImages = false 113 | setup() 114 | if prefs.loggedIn { 115 | login() 116 | } 117 | } 118 | 119 | fileprivate func setup() { 120 | UNUserNotificationCenter.current().delegate = self 121 | setupMenu() 122 | } 123 | 124 | private func setupMenu() { 125 | let menu = Menu() 126 | 127 | let mailbox = NSMenuItem(title: NSLocalizedString("Mailbox…", comment:"Menu item for opening the reddit mailbox"), 128 | action: #selector(handleMailboxItemSelected), keyEquivalent: "") 129 | mailbox.isEnabled = false 130 | menu.addItem(mailbox) 131 | 132 | mailboxItem = mailbox 133 | 134 | menu.addItem(NSMenuItem.separator()) 135 | let login = NSMenuItem(title: kLoginMenuTitle, 136 | action: #selector(handleLoginItemSelected), keyEquivalent: "") 137 | menu.addItem(login) 138 | loginItem = login 139 | 140 | #if PrefsDone 141 | let prefsItem = NSMenuItem(title: NSLocalizedString("Preferences…", comment:"Menu item title for opening the preferences window"), 142 | action: #selector(handlePrefItemSelected), keyEquivalent: "") 143 | menu.addItem(prefsItem) 144 | #endif 145 | let quitItem = NSMenuItem(title: NSLocalizedString("Quit", comment:"Quit menu item title"), 146 | action: #selector(quit), keyEquivalent: "") 147 | menu.addItem(quitItem) 148 | 149 | menu.items.forEach { (item) in 150 | item.target = self 151 | } 152 | 153 | statusItem.menu = menu 154 | 155 | var altImageName = "active" 156 | if prefs.useAltImages { 157 | altImageName = "alt-\(altImageName)" 158 | } 159 | 160 | statusItem.button?.alternateImage = NSImage(named: altImageName) 161 | updateIcon() 162 | } 163 | 164 | private func login() { 165 | guard let url = URL(string: "https://ssl.reddit.com/api/login") else { 166 | print("Error bad url, wat?") 167 | return 168 | } 169 | 170 | guard let uname = prefs.username, let password = prefs.password else { 171 | showLoginWindow() 172 | return 173 | } 174 | 175 | var request = URLRequest(url: url) 176 | request.httpMethod = "POST" 177 | request.httpBody = "user=\(uname)&passwd=\(password)".data(using: String.Encoding.utf8) 178 | 179 | loginItem?.title = kAttemptingLoginTitle 180 | 181 | let task = session.dataTask(with: request) { (data, response, error) in 182 | self.handleLogin(response: response, data:data, error: error) 183 | } 184 | 185 | task.resume() 186 | } 187 | 188 | fileprivate func handleLogin(response:URLResponse?, data:Data?, error:Error?) { 189 | if let dataActual = data, let 190 | dataString = String(data:dataActual, encoding:String.Encoding.utf8) { 191 | if dataString.contains("wrong password") { 192 | DispatchQueue.main.async { 193 | // There seems to be a problem with showing another window while this one has just been dismissed, rescheduling on the main thread solves this. 194 | 195 | // TODO: wrong password error 196 | self.state = .invalidcredentials 197 | let alert = NSAlert() 198 | alert.messageText = NSLocalizedString("Username and password do not match any recognized by Reddit", comment: "username/password mismatch error") 199 | alert.addButton(withTitle: NSLocalizedString("Lemme fix that...", comment:"Wrong password dialog acknowledgement button")) 200 | alert.runModal() 201 | 202 | self.showLoginWindow() 203 | } 204 | 205 | return 206 | } 207 | } 208 | 209 | guard let responseActual = response as? HTTPURLResponse else { 210 | print("Response is not an HTTPURLResponse, somehow: \(String(describing: response))") 211 | return 212 | } 213 | 214 | guard let headers = responseActual.allHeaderFields as NSDictionary? as! [String:String]? else { 215 | print("wrong headers ... or so: \(responseActual.allHeaderFields)") 216 | return 217 | } 218 | 219 | guard let url = responseActual.url else { 220 | print("missing url from response: \(responseActual)") 221 | return 222 | } 223 | 224 | let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: url) 225 | 226 | if cookies.count < 1 { 227 | print("Login error: \(String(describing: response))") 228 | state = .disconnected 229 | } 230 | else { 231 | HTTPCookieStorage.shared.setCookies(cookies, for: kRedditCookieURL, mainDocumentURL: nil) 232 | 233 | prefs.loggedIn = true 234 | state = .mailfree 235 | setupStatusPoller() 236 | } 237 | } 238 | 239 | private func setupStatusPoller() { 240 | statusPoller?.invalidate() 241 | let interval:TimeInterval = 60 242 | statusPoller = Timer(timeInterval: interval, target: self, selector: #selector(checkReddit), userInfo: nil, repeats: true) 243 | RunLoop.main.add(statusPoller!, forMode: .default) 244 | statusPoller?.fire() 245 | } 246 | 247 | private func showLoginWindow() { 248 | let login = LoginViewController { [weak self] (name, password) in 249 | self?.loginWindowController?.close() 250 | self?.prefs.username = name 251 | self?.prefs.password = password 252 | self?.login() 253 | } 254 | 255 | let window = NSPanel(contentViewController: login) 256 | window.appearance = NSAppearance(named: NSAppearance.Name.vibrantDark) 257 | loginWindowController = NSWindowController(window: window) 258 | 259 | NSApp.activate(ignoringOtherApps: true) 260 | loginWindowController?.showWindow(self) 261 | } 262 | 263 | private func showPrefWindow() { 264 | let pref = NSWindowController(window: NSPanel(contentViewController: PrefViewController())) 265 | 266 | prefWindowController = pref 267 | NSApp.activate(ignoringOtherApps: true) 268 | pref.showWindow(self) 269 | } 270 | 271 | private func interpretResponse(json: Any) { 272 | // Crude, but remarkably immune to data restructuring as long as the key value pairs don't change. 273 | 274 | // WTF Swift 3... 275 | guard let jsonDict = json as AnyObject? else { 276 | return 277 | } 278 | 279 | guard let jsonActual = jsonDict["data"] as? [String:AnyObject] else { 280 | print("response json unexpected format: \(json)") 281 | return 282 | } 283 | 284 | if let newMailCount = jsonActual["inbox_count"] as? Int { 285 | if newMailCount != mailCount { 286 | mailCount = newMailCount 287 | 288 | if mailCount > 0 { 289 | notifyMail() 290 | } 291 | else { 292 | // We had mail, and now we don't. Safe to assume it's all been read. 293 | UNUserNotificationCenter.current().removeAllDeliveredNotifications() 294 | } 295 | } 296 | } 297 | 298 | if let modMailState = jsonActual["has_mod_mail"] as? Bool, modMailState == true { 299 | state = .modmail 300 | return 301 | } 302 | 303 | if let mailState = jsonActual["has_mail"] as? Bool { 304 | if mailState { 305 | state = .orangered 306 | } 307 | else { 308 | state = .mailfree 309 | } 310 | } 311 | else { 312 | // probably login error 313 | state = .disconnected 314 | } 315 | } 316 | 317 | private func handleStateChanged() { 318 | updateIcon() 319 | mailboxItem?.isEnabled = true 320 | loginItem?.title = prefs.loggedIn ? kLogoutMenuTitle : kLoginMenuTitle 321 | 322 | switch state { 323 | case .disconnected: 324 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10, execute: { 325 | self.login() 326 | }) 327 | fallthrough 328 | case .loggedout, .invalidcredentials: 329 | mailboxItem?.isEnabled = false 330 | 331 | 332 | case .orangered, .modmail, .mailfree, .update: 333 | break 334 | } 335 | } 336 | 337 | private func updateIcon() { 338 | statusItem.button?.image = state.image(forAppearance: statusItem.button!.effectiveAppearance.name.rawValue, useAlt: prefs.useAltImages) 339 | } 340 | 341 | private func notifyMail() { 342 | let note = UNMutableNotificationContent() 343 | note.title = "Orangered!" 344 | note.body = NSLocalizedString("You have a new message on reddit!", comment: "new message notification text") 345 | // note.actionButtonTitle = NSLocalizedString("Read", comment: "notification call to action button") 346 | 347 | if mailCount > 1 { 348 | note.body = String 349 | .localizedStringWithFormat(NSLocalizedString("You have %i unread messages on reddit", 350 | comment: "plural message notification text"), 351 | mailCount) 352 | } 353 | 354 | UNUserNotificationCenter 355 | .current() 356 | .add(UNNotificationRequest(identifier: "Orangered!", content: note, trigger: nil)) 357 | } 358 | 359 | private func openMailbox() { 360 | if let url = state.mailboxUrl() { 361 | NSWorkspace.shared.open(url) 362 | } 363 | 364 | // There's no reasonable way to know when reddit has cleared the unread flag, so we just wait a bit and check again. 365 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + kOpenMailboxRecheckDelay) { 366 | self.checkReddit() 367 | } 368 | } 369 | 370 | private func logout() { 371 | prefs.loggedIn = false 372 | statusPoller?.invalidate() 373 | let storage = HTTPCookieStorage.shared 374 | storage.cookies(for: kRedditCookieURL!)?.forEach { storage.deleteCookie($0) } 375 | state = .loggedout 376 | } 377 | 378 | @objc private func checkReddit() { 379 | guard let uname = prefs.username, 380 | let url = URL(string: "http://www.reddit.com/user/\(uname)/about.json") else { 381 | print("User name empty") 382 | return 383 | } 384 | 385 | let task = session.dataTask(with: url) { (data, response, error) in 386 | if let dataActual = data { 387 | do { 388 | try self.interpretResponse(json: JSONSerialization.jsonObject(with: dataActual, options: .allowFragments)) 389 | } catch let error { 390 | print("Error reading response json: \(error)") 391 | } 392 | } 393 | else { 394 | print("Failure: \(String(describing: response))") 395 | } 396 | } 397 | 398 | task.resume() 399 | } 400 | 401 | @objc private func quit() { 402 | NSApplication.shared.stop(nil) 403 | } 404 | 405 | @objc func handleLoginItemSelected() { 406 | if prefs.loggedIn { 407 | logout() 408 | } 409 | else { 410 | showLoginWindow() 411 | } 412 | } 413 | 414 | @objc func handlePrefItemSelected() { 415 | showPrefWindow() 416 | } 417 | 418 | @objc func handleMailboxItemSelected() { 419 | openMailbox() 420 | } 421 | 422 | 423 | // MARK: User Notification Center 424 | 425 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { 426 | openMailbox() 427 | } 428 | // @objc func userNotificationCenter(did) { 429 | // openMailbox() 430 | // } 431 | } 432 | 433 | --------------------------------------------------------------------------------