├── 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 |
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 |
--------------------------------------------------------------------------------